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:
@@ -174,7 +174,7 @@ def create_e2e_app(
|
||||
app.state.analytics_recorder = AsyncMock()
|
||||
app.state.conversation_tracker = AsyncMock()
|
||||
|
||||
@app.get("/api/health")
|
||||
@app.get("/api/v1/health")
|
||||
def health_check() -> dict:
|
||||
return {"status": "ok", "version": "test"}
|
||||
|
||||
|
||||
@@ -341,7 +341,7 @@ class TestChatEdgeCases:
|
||||
def test_health_endpoint(self) -> None:
|
||||
app = create_e2e_app()
|
||||
with TestClient(app) as client:
|
||||
resp = client.get("/api/health")
|
||||
resp = client.get("/api/v1/health")
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["status"] == "ok"
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ class TestFlow5OpenAPIImport:
|
||||
with TestClient(app) as client:
|
||||
# Step 1: Start import job
|
||||
resp = client.post(
|
||||
"/api/openapi/import",
|
||||
"/api/v1/openapi/import",
|
||||
json={"url": "https://api.example.com/openapi.json"},
|
||||
)
|
||||
assert resp.status_code == 202
|
||||
@@ -71,7 +71,7 @@ class TestFlow5OpenAPIImport:
|
||||
job_id = body["job_id"]
|
||||
|
||||
# Step 2: Check job status (still pending since background task hasn't run)
|
||||
resp = client.get(f"/api/openapi/jobs/{job_id}")
|
||||
resp = client.get(f"/api/v1/openapi/jobs/{job_id}")
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["job_id"] == job_id
|
||||
|
||||
@@ -99,7 +99,7 @@ class TestFlow5OpenAPIImport:
|
||||
|
||||
with TestClient(app) as client:
|
||||
# Step 1: Get classifications
|
||||
resp = client.get(f"/api/openapi/jobs/{job_id}/classifications")
|
||||
resp = client.get(f"/api/v1/openapi/jobs/{job_id}/classifications")
|
||||
assert resp.status_code == 200
|
||||
classifications = resp.json()
|
||||
assert len(classifications) == 2
|
||||
@@ -118,7 +118,7 @@ class TestFlow5OpenAPIImport:
|
||||
|
||||
# Step 2: Update a classification
|
||||
resp = client.put(
|
||||
f"/api/openapi/jobs/{job_id}/classifications/0",
|
||||
f"/api/v1/openapi/jobs/{job_id}/classifications/0",
|
||||
json={
|
||||
"access_type": "write",
|
||||
"needs_interrupt": True,
|
||||
@@ -132,7 +132,7 @@ class TestFlow5OpenAPIImport:
|
||||
assert updated["agent_group"] == "order_actions"
|
||||
|
||||
# Step 3: Approve the job
|
||||
resp = client.post(f"/api/openapi/jobs/{job_id}/approve")
|
||||
resp = client.post(f"/api/v1/openapi/jobs/{job_id}/approve")
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["status"] == "approved"
|
||||
|
||||
@@ -140,14 +140,14 @@ class TestFlow5OpenAPIImport:
|
||||
app = create_e2e_app()
|
||||
|
||||
with TestClient(app) as client:
|
||||
resp = client.get("/api/openapi/jobs/nonexistent")
|
||||
resp = client.get("/api/v1/openapi/jobs/nonexistent")
|
||||
assert resp.status_code == 404
|
||||
|
||||
def test_import_invalid_url_returns_422(self) -> None:
|
||||
app = create_e2e_app()
|
||||
|
||||
with TestClient(app) as client:
|
||||
resp = client.post("/api/openapi/import", json={"url": "not-a-url"})
|
||||
resp = client.post("/api/v1/openapi/import", json={"url": "not-a-url"})
|
||||
assert resp.status_code == 422
|
||||
|
||||
def test_classification_index_out_of_range(self) -> None:
|
||||
@@ -166,7 +166,7 @@ class TestFlow5OpenAPIImport:
|
||||
|
||||
with TestClient(app) as client:
|
||||
resp = client.put(
|
||||
f"/api/openapi/jobs/{job_id}/classifications/99",
|
||||
f"/api/v1/openapi/jobs/{job_id}/classifications/99",
|
||||
json={
|
||||
"access_type": "read",
|
||||
"needs_interrupt": False,
|
||||
@@ -191,7 +191,7 @@ class TestFlow5OpenAPIImport:
|
||||
|
||||
with TestClient(app) as client:
|
||||
resp = client.put(
|
||||
f"/api/openapi/jobs/{job_id}/classifications/0",
|
||||
f"/api/v1/openapi/jobs/{job_id}/classifications/0",
|
||||
json={
|
||||
"access_type": "read",
|
||||
"needs_interrupt": False,
|
||||
|
||||
@@ -98,7 +98,7 @@ class TestFlow6ReplayConversation:
|
||||
app = create_e2e_app(pool=pool)
|
||||
|
||||
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
|
||||
@@ -124,7 +124,7 @@ class TestFlow6ReplayConversation:
|
||||
app = create_e2e_app(pool=pool)
|
||||
|
||||
with TestClient(app) as client:
|
||||
resp = client.get("/api/conversations", params={"page": 1, "per_page": 2})
|
||||
resp = client.get("/api/v1/conversations", params={"page": 1, "per_page": 2})
|
||||
assert resp.status_code == 200
|
||||
body = resp.json()
|
||||
assert body["success"] is True
|
||||
@@ -139,7 +139,7 @@ class TestFlow6ReplayConversation:
|
||||
app = create_e2e_app(pool=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_replay_invalid_thread_id_format(self) -> None:
|
||||
@@ -147,7 +147,7 @@ class TestFlow6ReplayConversation:
|
||||
|
||||
with TestClient(app) as client:
|
||||
# Thread ID with special chars fails regex validation
|
||||
resp = client.get("/api/replay/invalid%20thread%21%40")
|
||||
resp = client.get("/api/v1/replay/invalid%20thread%21%40")
|
||||
assert resp.status_code == 400
|
||||
|
||||
|
||||
@@ -158,21 +158,21 @@ class TestAnalyticsDashboard:
|
||||
app = create_e2e_app()
|
||||
|
||||
with TestClient(app) as client:
|
||||
resp = client.get("/api/analytics", params={"range": "invalid"})
|
||||
resp = client.get("/api/v1/analytics", params={"range": "invalid"})
|
||||
assert resp.status_code == 400
|
||||
|
||||
def test_analytics_range_too_large(self) -> None:
|
||||
app = create_e2e_app()
|
||||
|
||||
with TestClient(app) as client:
|
||||
resp = client.get("/api/analytics", params={"range": "999d"})
|
||||
resp = client.get("/api/v1/analytics", params={"range": "999d"})
|
||||
assert resp.status_code == 400
|
||||
|
||||
def test_analytics_range_zero_rejected(self) -> None:
|
||||
app = create_e2e_app()
|
||||
|
||||
with TestClient(app) as client:
|
||||
resp = client.get("/api/analytics", params={"range": "0d"})
|
||||
resp = client.get("/api/v1/analytics", params={"range": "0d"})
|
||||
assert resp.status_code == 400
|
||||
|
||||
|
||||
@@ -216,7 +216,7 @@ class TestFullUserJourney:
|
||||
assert any(m["type"] == "message_complete" for m in messages)
|
||||
|
||||
# Step 2: Check conversations endpoint
|
||||
resp = client.get("/api/conversations")
|
||||
resp = client.get("/api/v1/conversations")
|
||||
assert resp.status_code == 200
|
||||
body = resp.json()
|
||||
assert body["success"] is True
|
||||
@@ -226,5 +226,5 @@ class TestFullUserJourney:
|
||||
)
|
||||
|
||||
# Step 3: Health check still works
|
||||
resp = client.get("/api/health")
|
||||
resp = client.get("/api/v1/health")
|
||||
assert resp.status_code == 200
|
||||
|
||||
Reference in New Issue
Block a user