refactor: engineering improvements -- API versioning, structured logging, Alembic, error standardization, test coverage

- API versioning: all REST endpoints prefixed with /api/v1/
- Structured logging: replaced stdlib logging with structlog (console/JSON modes)
- Alembic migrations: versioned DB schema with initial migration
- Error standardization: global exception handlers for consistent envelope format
- Interrupt cleanup: asyncio background task for expired interrupt removal
- Integration tests: +30 tests (analytics, replay, openapi, error, session APIs)
- Frontend tests: +57 tests (all components, pages, useWebSocket hook)
- Backend: 557 tests, 89.75% coverage | Frontend: 80 tests, 16 test files
This commit is contained in:
Yaojia Wang
2026-04-06 23:19:29 +02:00
parent af53111928
commit f0699436c5
59 changed files with 2846 additions and 149 deletions

View File

@@ -5,9 +5,12 @@ from __future__ import annotations
from unittest.mock import AsyncMock, MagicMock
import pytest
from fastapi import FastAPI
from fastapi import FastAPI, HTTPException
from fastapi.responses import JSONResponse
from fastapi.testclient import TestClient
from app.api_utils import envelope
pytestmark = pytest.mark.unit
@@ -16,6 +19,14 @@ def _build_app() -> FastAPI:
app = FastAPI()
app.include_router(router)
@app.exception_handler(HTTPException)
async def _http_exc(request, exc): # type: ignore[no-untyped-def]
return JSONResponse(
status_code=exc.status_code,
content=envelope(None, success=False, error=exc.detail),
)
return app
@@ -64,7 +75,7 @@ class TestListConversations:
app.state.pool = _make_mock_pool([], count=0)
with TestClient(app) as client:
resp = client.get("/api/conversations")
resp = client.get("/api/v1/conversations")
assert resp.status_code == 200
body = resp.json()
assert body["success"] is True
@@ -89,7 +100,7 @@ class TestListConversations:
app.state.pool = _make_mock_pool(mock_rows, count=1)
with TestClient(app) as client:
resp = client.get("/api/conversations")
resp = client.get("/api/v1/conversations")
body = resp.json()
assert resp.status_code == 200
data = body["data"]
@@ -102,7 +113,7 @@ class TestListConversations:
app.state.pool = _make_mock_pool([], count=0)
with TestClient(app) as client:
resp = client.get("/api/conversations")
resp = client.get("/api/v1/conversations")
assert resp.status_code == 200
def test_pagination_custom_params(self) -> None:
@@ -110,7 +121,7 @@ class TestListConversations:
app.state.pool = _make_mock_pool([], count=0)
with TestClient(app) as client:
resp = client.get("/api/conversations?page=2&per_page=10")
resp = client.get("/api/v1/conversations?page=2&per_page=10")
assert resp.status_code == 200
def test_per_page_max_capped_at_100(self) -> None:
@@ -118,7 +129,7 @@ class TestListConversations:
app.state.pool = _make_mock_pool([], count=0)
with TestClient(app) as client:
resp = client.get("/api/conversations?per_page=200")
resp = client.get("/api/v1/conversations?per_page=200")
# FastAPI Query(le=100) rejects values > 100
assert resp.status_code == 422
@@ -129,7 +140,7 @@ class TestGetReplay:
app.state.pool = _make_mock_pool([])
with TestClient(app) as client:
resp = client.get("/api/replay/nonexistent-thread")
resp = client.get("/api/v1/replay/nonexistent-thread")
assert resp.status_code == 404
def test_returns_replay_page_for_existing_thread(self) -> None:
@@ -149,7 +160,7 @@ class TestGetReplay:
app.state.pool = _make_mock_pool(mock_rows)
with TestClient(app) as client:
resp = client.get("/api/replay/thread-123")
resp = client.get("/api/v1/replay/thread-123")
assert resp.status_code == 200
body = resp.json()
assert body["success"] is True
@@ -174,7 +185,7 @@ class TestGetReplay:
app.state.pool = _make_mock_pool(mock_rows)
with TestClient(app) as client:
resp = client.get("/api/replay/t1?page=1&per_page=5")
resp = client.get("/api/v1/replay/t1?page=1&per_page=5")
assert resp.status_code == 200
def test_error_response_has_envelope(self) -> None:
@@ -182,16 +193,19 @@ class TestGetReplay:
app.state.pool = _make_mock_pool([])
with TestClient(app) as client:
resp = client.get("/api/replay/missing")
resp = client.get("/api/v1/replay/missing")
assert resp.status_code == 404
assert "detail" in resp.json()
body = resp.json()
assert body["success"] is False
assert body["data"] is None
assert body["error"] is not None
def test_invalid_thread_id_returns_400(self) -> None:
app = _build_app()
app.state.pool = _make_mock_pool([])
with TestClient(app) as client:
resp = client.get("/api/replay/id%20with%20spaces")
resp = client.get("/api/v1/replay/id%20with%20spaces")
assert resp.status_code == 400
def test_thread_id_special_chars_returns_400(self) -> None:
@@ -199,5 +213,5 @@ class TestGetReplay:
app.state.pool = _make_mock_pool([])
with TestClient(app) as client:
resp = client.get("/api/replay/id;DROP TABLE")
resp = client.get("/api/v1/replay/id;DROP TABLE")
assert resp.status_code == 400