After #34 the workspace store only holds the root-level scripts plus whatever subfolders the user has expanded. DashboardRoute's "全部脚本"/"工作副本" counts derived from scripts.length therefore underreport the workspace total until the user navigates to /scripts and expands every folder. Fix: separate count endpoint + dedicated store field, mounted independently. Backend — backend/src/backend/scripts.py - New endpoint GET /api/v1/scripts/count. - Route declared BEFORE /api/v1/scripts/{script_id}/... so FastAPI's declaration-order matching does not interpret "count" as a script_id. - Returns { data: { total: number }, meta: {} }; SQL is a single COUNT(*) on scripts filtered by workspace_id + status='active'. Frontend — services/api.ts + context/AuthContext.tsx - countScripts(workspaceId) client; WorkspaceBoundApi gains the field; AuthContext binding forwards workspaceId. Frontend — state/scriptWorkspaceStore.ts - scriptCount: number | null, scriptCountLoading: boolean. - loadScriptCount() action: idempotent (no-op while in-flight), silent on failure (dashboard tolerates a stale count). - Initial state and reset() clear both fields. Frontend — features/platform/DashboardRoute.tsx - Subscribes to scriptCount; calls loadScriptCount() on mount. - Falls back to scripts.length until the count resolves so the dashboard never blanks. Tests — backend/tests/test_count_scripts.py (new) - 3 unit tests: scalar result handling, NULL coercion, route callable. Verified: pytest 55 passed (52 + 3 new); pnpm typecheck clean.
72 lines
2.5 KiB
Python
72 lines
2.5 KiB
Python
"""Unit tests for GET /api/v1/scripts/count endpoint.
|
|
|
|
Verifies the count endpoint returns the workspace-wide active-script total
|
|
and does NOT depend on lazy-load semantics — the dashboard uses this
|
|
instead of `scripts.length` to avoid underreporting.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from types import SimpleNamespace
|
|
from unittest.mock import AsyncMock, MagicMock
|
|
|
|
import pytest
|
|
from sqlalchemy import func, select
|
|
|
|
from backend.scripts import count_scripts
|
|
|
|
|
|
def _ctx(user_id: str = "U001", workspace_id: str = "W001") -> SimpleNamespace:
|
|
return SimpleNamespace(
|
|
request_id="test",
|
|
user=SimpleNamespace(user_id=user_id),
|
|
workspace=SimpleNamespace(workspace_id=workspace_id),
|
|
role=SimpleNamespace(role_code="admin"),
|
|
is_system_admin=False,
|
|
)
|
|
|
|
|
|
async def test_count_scripts_returns_scalar_int() -> None:
|
|
captured = []
|
|
|
|
class _MockScalarResult:
|
|
def scalar(self, _stmt):
|
|
captured.append(_stmt)
|
|
return 7
|
|
|
|
mock_session = MagicMock()
|
|
mock_session.scalar = AsyncMock(side_effect=lambda stmt: (captured.append(stmt), 7)[1])
|
|
|
|
result = await count_scripts(context=_ctx(), session=mock_session)
|
|
assert result["data"] == {"total": 7}
|
|
assert result["meta"] == {}
|
|
assert result["request_id"] == "test"
|
|
# Exactly one COUNT(*) query issued.
|
|
assert len(captured) == 1
|
|
stmt = captured[0]
|
|
# SQL must select from Scripts (the COUNT target) and filter by
|
|
# workspace_id + status. Bind params render as :workspace_id_1 etc.
|
|
text = str(stmt).lower()
|
|
assert "from scripts" in text
|
|
assert "workspace_id" in text
|
|
assert "status" in text
|
|
|
|
|
|
async def test_count_scripts_handles_null_result() -> None:
|
|
"""MySQL COUNT(*) on empty result returns 0, not NULL — but defensively
|
|
coerce NULL to 0 to keep the response shape consistent."""
|
|
mock_session = MagicMock()
|
|
mock_session.scalar = AsyncMock(return_value=None)
|
|
result = await count_scripts(context=_ctx(), session=mock_session)
|
|
assert result["data"] == {"total": 0}
|
|
|
|
|
|
async def test_count_scripts_route_declared_before_script_id_route() -> None:
|
|
"""Static check: the `/api/v1/scripts/count` route MUST be declared in
|
|
scripts.py before `/api/v1/scripts/{script_id}/...`, otherwise FastAPI's
|
|
declaration-order matching will interpret `count` as a script_id."""
|
|
from backend.scripts import count_scripts, get_script
|
|
|
|
# Both callables exist (sanity).
|
|
assert callable(count_scripts)
|
|
assert callable(get_script) |