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

@@ -28,7 +28,7 @@ def client():
@pytest.fixture
def job_id(client):
"""Create a job and return its ID."""
response = client.post("/api/openapi/import", json={"url": _SAMPLE_URL})
response = client.post("/api/v1/openapi/import", json={"url": _SAMPLE_URL})
assert response.status_code == 202
return response.json()["job_id"]
@@ -61,11 +61,11 @@ def job_with_classifications(client, job_id):
class TestImportEndpoint:
"""Tests for POST /api/openapi/import."""
"""Tests for POST /api/v1/openapi/import."""
def test_post_import_returns_job_id(self, client) -> None:
"""POST /import returns 202 with a job_id."""
response = client.post("/api/openapi/import", json={"url": _SAMPLE_URL})
response = client.post("/api/v1/openapi/import", json={"url": _SAMPLE_URL})
assert response.status_code == 202
data = response.json()
assert "job_id" in data
@@ -73,38 +73,38 @@ class TestImportEndpoint:
def test_post_import_empty_url_returns_422(self, client) -> None:
"""POST /import with empty URL returns 422 validation error."""
response = client.post("/api/openapi/import", json={"url": ""})
response = client.post("/api/v1/openapi/import", json={"url": ""})
assert response.status_code == 422
def test_post_import_missing_url_returns_422(self, client) -> None:
"""POST /import with missing URL field returns 422."""
response = client.post("/api/openapi/import", json={})
response = client.post("/api/v1/openapi/import", json={})
assert response.status_code == 422
def test_post_import_invalid_scheme_returns_422(self, client) -> None:
"""POST /import with non-http URL returns 422."""
response = client.post("/api/openapi/import", json={"url": "ftp://evil.com/spec"})
response = client.post("/api/v1/openapi/import", json={"url": "ftp://evil.com/spec"})
assert response.status_code == 422
def test_post_import_returns_pending_status(self, client) -> None:
"""Newly created job has pending status."""
response = client.post("/api/openapi/import", json={"url": _SAMPLE_URL})
response = client.post("/api/v1/openapi/import", json={"url": _SAMPLE_URL})
data = response.json()
assert data["status"] == "pending"
def test_post_import_returns_spec_url(self, client) -> None:
"""Response includes the original spec URL."""
response = client.post("/api/openapi/import", json={"url": _SAMPLE_URL})
response = client.post("/api/v1/openapi/import", json={"url": _SAMPLE_URL})
data = response.json()
assert data["spec_url"] == _SAMPLE_URL
class TestGetJobEndpoint:
"""Tests for GET /api/openapi/jobs/{job_id}."""
"""Tests for GET /api/v1/openapi/jobs/{job_id}."""
def test_get_job_returns_status(self, client, job_id) -> None:
"""GET /jobs/{id} returns job status."""
response = client.get(f"/api/openapi/jobs/{job_id}")
response = client.get(f"/api/v1/openapi/jobs/{job_id}")
assert response.status_code == 200
data = response.json()
assert "status" in data
@@ -112,23 +112,23 @@ class TestGetJobEndpoint:
def test_get_unknown_job_returns_404(self, client) -> None:
"""GET /jobs/nonexistent returns 404."""
response = client.get("/api/openapi/jobs/nonexistent-id")
response = client.get("/api/v1/openapi/jobs/nonexistent-id")
assert response.status_code == 404
def test_get_job_includes_spec_url(self, client, job_id) -> None:
"""Job response includes the spec URL."""
response = client.get(f"/api/openapi/jobs/{job_id}")
response = client.get(f"/api/v1/openapi/jobs/{job_id}")
data = response.json()
assert data["spec_url"] == _SAMPLE_URL
class TestGetClassificationsEndpoint:
"""Tests for GET /api/openapi/jobs/{job_id}/classifications."""
"""Tests for GET /api/v1/openapi/jobs/{job_id}/classifications."""
def test_get_classifications_returns_list(self, client, job_with_classifications) -> None:
"""GET /classifications returns a list."""
response = client.get(
f"/api/openapi/jobs/{job_with_classifications}/classifications"
f"/api/v1/openapi/jobs/{job_with_classifications}/classifications"
)
assert response.status_code == 200
data = response.json()
@@ -137,13 +137,13 @@ class TestGetClassificationsEndpoint:
def test_get_classifications_unknown_job_returns_404(self, client) -> None:
"""GET /classifications for unknown job returns 404."""
response = client.get("/api/openapi/jobs/unknown/classifications")
response = client.get("/api/v1/openapi/jobs/unknown/classifications")
assert response.status_code == 404
def test_classification_has_expected_fields(self, client, job_with_classifications) -> None:
"""Each classification item has access_type and endpoint fields."""
response = client.get(
f"/api/openapi/jobs/{job_with_classifications}/classifications"
f"/api/v1/openapi/jobs/{job_with_classifications}/classifications"
)
item = response.json()[0]
assert "access_type" in item
@@ -152,12 +152,12 @@ class TestGetClassificationsEndpoint:
class TestUpdateClassificationEndpoint:
"""Tests for PUT /api/openapi/jobs/{job_id}/classifications/{idx}."""
"""Tests for PUT /api/v1/openapi/jobs/{job_id}/classifications/{idx}."""
def test_update_classification_succeeds(self, client, job_with_classifications) -> None:
"""PUT /classifications/0 updates the classification."""
response = client.put(
f"/api/openapi/jobs/{job_with_classifications}/classifications/0",
f"/api/v1/openapi/jobs/{job_with_classifications}/classifications/0",
json={"access_type": "write", "needs_interrupt": True, "agent_group": "write_agent"},
)
assert response.status_code == 200
@@ -165,7 +165,7 @@ class TestUpdateClassificationEndpoint:
def test_update_unknown_job_returns_404(self, client) -> None:
"""PUT /classifications/0 for unknown job returns 404."""
response = client.put(
"/api/openapi/jobs/unknown/classifications/0",
"/api/v1/openapi/jobs/unknown/classifications/0",
json={"access_type": "write", "needs_interrupt": True, "agent_group": "write_agent"},
)
assert response.status_code == 404
@@ -173,7 +173,7 @@ class TestUpdateClassificationEndpoint:
def test_update_invalid_access_type_returns_422(self, client, job_with_classifications) -> None:
"""PUT /classifications/0 with invalid access_type returns 422."""
response = client.put(
f"/api/openapi/jobs/{job_with_classifications}/classifications/0",
f"/api/v1/openapi/jobs/{job_with_classifications}/classifications/0",
json={"access_type": "admin", "needs_interrupt": True, "agent_group": "x"},
)
assert response.status_code == 422
@@ -181,7 +181,7 @@ class TestUpdateClassificationEndpoint:
def test_update_invalid_agent_group_returns_422(self, client, job_with_classifications) -> None:
"""PUT /classifications/0 with invalid agent_group returns 422."""
response = client.put(
f"/api/openapi/jobs/{job_with_classifications}/classifications/0",
f"/api/v1/openapi/jobs/{job_with_classifications}/classifications/0",
json={"access_type": "read", "needs_interrupt": False, "agent_group": "evil group!"},
)
assert response.status_code == 422
@@ -189,31 +189,31 @@ class TestUpdateClassificationEndpoint:
def test_update_out_of_range_index_returns_404(self, client, job_with_classifications) -> None:
"""PUT /classifications/999 returns 404 for out-of-range index."""
response = client.put(
f"/api/openapi/jobs/{job_with_classifications}/classifications/999",
f"/api/v1/openapi/jobs/{job_with_classifications}/classifications/999",
json={"access_type": "read", "needs_interrupt": False, "agent_group": "read_agent"},
)
assert response.status_code == 404
class TestApproveEndpoint:
"""Tests for POST /api/openapi/jobs/{job_id}/approve."""
"""Tests for POST /api/v1/openapi/jobs/{job_id}/approve."""
def test_approve_job_succeeds(self, client, job_with_classifications) -> None:
"""POST /approve transitions job to approved status."""
response = client.post(
f"/api/openapi/jobs/{job_with_classifications}/approve"
f"/api/v1/openapi/jobs/{job_with_classifications}/approve"
)
assert response.status_code == 200
def test_approve_unknown_job_returns_404(self, client) -> None:
"""POST /approve for unknown job returns 404."""
response = client.post("/api/openapi/jobs/unknown/approve")
response = client.post("/api/v1/openapi/jobs/unknown/approve")
assert response.status_code == 404
def test_approve_returns_job_status(self, client, job_with_classifications) -> None:
"""POST /approve returns updated job status."""
response = client.post(
f"/api/openapi/jobs/{job_with_classifications}/approve"
f"/api/v1/openapi/jobs/{job_with_classifications}/approve"
)
data = response.json()
assert "status" in data