feat: wire frontend pages to live APIs and standardize response contracts (P1)

- Backend: Add COUNT query and paginated response shape to conversations endpoint
  Returns { conversations: [...], total, page, per_page } instead of flat array
- Frontend: Replace mock data in DashboardPage with fetchAnalytics() API calls
- Frontend: Replace mock data in ReplayListPage with fetchConversations() API calls
- Frontend: Replace mock data in ReplayPage with fetchReplay() API calls
- Add proper loading, empty, and error states to all three pages
- Align ConversationSummary type with actual DB columns (created_at, status)
- Update unit and E2E tests for new paginated conversation response shape
- Add fetchone() to FakeCursor for COUNT query support in E2E tests
This commit is contained in:
Yaojia Wang
2026-04-05 23:06:00 +02:00
parent e55ec42ae5
commit e0931daece
8 changed files with 327 additions and 233 deletions

View File

@@ -19,13 +19,35 @@ def _build_app() -> FastAPI:
return app
def _make_mock_pool(fetchall_result: list[dict]) -> MagicMock:
"""Build a mock pool that returns the given rows from fetchall."""
mock_cursor = AsyncMock()
mock_cursor.fetchall = AsyncMock(return_value=fetchall_result)
def _make_mock_pool(
fetchall_result: list[dict],
*,
count: int | None = None,
) -> MagicMock:
"""Build a mock pool that returns the given rows from fetchall.
mock_conn = AsyncMock()
mock_conn.execute = AsyncMock(return_value=mock_cursor)
When *count* is provided, the first execute() call returns a cursor
whose fetchone() yields ``(count,)`` (for the COUNT query) and the
second call returns the rows via fetchall(). When *count* is None
(the default), a single cursor backed by *fetchall_result* is used
for all calls.
"""
if count is not None:
count_cursor = AsyncMock()
count_cursor.fetchone = AsyncMock(return_value=(count,))
rows_cursor = AsyncMock()
rows_cursor.fetchall = AsyncMock(return_value=fetchall_result)
mock_conn = AsyncMock()
mock_conn.execute = AsyncMock(side_effect=[count_cursor, rows_cursor])
else:
mock_cursor = AsyncMock()
mock_cursor.fetchall = AsyncMock(return_value=fetchall_result)
mock_cursor.fetchone = AsyncMock(return_value=None)
mock_conn = AsyncMock()
mock_conn.execute = AsyncMock(return_value=mock_cursor)
mock_ctx = AsyncMock()
mock_ctx.__aenter__ = AsyncMock(return_value=mock_conn)
@@ -39,14 +61,17 @@ def _make_mock_pool(fetchall_result: list[dict]) -> MagicMock:
class TestListConversations:
def test_returns_200_with_empty_list(self) -> None:
app = _build_app()
app.state.pool = _make_mock_pool([])
app.state.pool = _make_mock_pool([], count=0)
with TestClient(app) as client:
resp = client.get("/api/conversations")
assert resp.status_code == 200
body = resp.json()
assert body["success"] is True
assert isinstance(body["data"], list)
data = body["data"]
assert isinstance(data["conversations"], list)
assert data["total"] == 0
assert data["page"] == 1
assert body["error"] is None
def test_returns_conversations_list(self) -> None:
@@ -61,18 +86,20 @@ class TestListConversations:
"total_cost_usd": 0.01,
}
]
app.state.pool = _make_mock_pool(mock_rows)
app.state.pool = _make_mock_pool(mock_rows, count=1)
with TestClient(app) as client:
resp = client.get("/api/conversations")
body = resp.json()
assert resp.status_code == 200
assert len(body["data"]) == 1
assert body["data"][0]["thread_id"] == "t1"
data = body["data"]
assert len(data["conversations"]) == 1
assert data["conversations"][0]["thread_id"] == "t1"
assert data["total"] == 1
def test_pagination_defaults(self) -> None:
app = _build_app()
app.state.pool = _make_mock_pool([])
app.state.pool = _make_mock_pool([], count=0)
with TestClient(app) as client:
resp = client.get("/api/conversations")
@@ -80,7 +107,7 @@ class TestListConversations:
def test_pagination_custom_params(self) -> None:
app = _build_app()
app.state.pool = _make_mock_pool([])
app.state.pool = _make_mock_pool([], count=0)
with TestClient(app) as client:
resp = client.get("/api/conversations?page=2&per_page=10")
@@ -88,7 +115,7 @@ class TestListConversations:
def test_per_page_max_capped_at_100(self) -> None:
app = _build_app()
app.state.pool = _make_mock_pool([])
app.state.pool = _make_mock_pool([], count=0)
with TestClient(app) as client:
resp = client.get("/api/conversations?per_page=200")