tech-debt: mypy --strict config + integration tests for audit routes

Phase 1 Step 4 follow-up addressing the debt flagged in the worked-example
commit (4a91814).

## mypy --strict policy

Adds backend-compliance/mypy.ini declaring the strict-mode scope:

  Fully strict (enforced today):
    - compliance/domain/
    - compliance/schemas/
    - compliance/api/_http_errors.py
    - compliance/api/audit_routes.py        (refactored in Step 4)
    - compliance/services/audit_session_service.py
    - compliance/services/audit_signoff_service.py

  Loose (ignore_errors=True) with a migration path:
    - compliance/db/*                        — SQLAlchemy 1.x Column[] vs
                                               runtime T; unblocks Phase 1
                                               until a Mapped[T] migration.
    - compliance/api/<route>.py              — each route file flips to
                                               strict as its own Step 4
                                               refactor lands.
    - compliance/services/<legacy util>      — 14 utility services
                                               (llm_provider, pdf_extractor,
                                               seeder, ...) that predate the
                                               clean-arch refactor.
    - compliance/tests/                      — excluded (legacy placeholder
                                               style). The new TestClient-
                                               based integration suite is
                                               type-annotated.

The two new service files carry a scoped `# mypy: disable-error-code="arg-type,assignment"`
header for the ORM Column[T] issue — same underlying SQLAlchemy limitation,
narrowly scoped rather than wholesale ignore_errors.

Flow: `cd backend-compliance && mypy compliance/` -> clean on 119 files.
CI yaml updated to use the config instead of ad-hoc package lists.

## Bugs fixed while enabling strict

mypy --strict surfaced two latent bugs in the pre-refactor code. Both
were invisible because the old `compliance/tests/test_audit_routes.py`
is a placeholder suite that asserts on request-data shape and never
calls the handlers:

  - AuditSessionResponse.updated_at is a required field in the schema,
    but the original handler didn't pass it. Fixed in
    AuditSessionService._to_response.

  - PaginationMeta requires has_next + has_prev. The original audit
    checklist handler didn't compute them. Fixed in
    AuditSignOffService.get_checklist.

Both are behavior-preserving at the HTTP level because the old code
would have raised Pydantic ValidationError at response serialization
had the endpoint actually been exercised.

## Integration test suite

Adds backend-compliance/tests/test_audit_routes_integration.py — 26
real TestClient tests against an in-memory sqlite backend (StaticPool).
Replaces the coverage gap left by the placeholder suite.

Covers:
  - Session CRUD + lifecycle transitions (draft -> in_progress -> completed
    -> archived), including the 409 paths for illegal transitions
  - Checklist pagination, filtering, search
  - Sign-off create / update / auto-start-session / count-flipping
  - Sign-off 400 (invalid result), 404 (missing requirement), 409 (completed session)
  - Get-signoff 404 / 200 round-trip

Uses a module-scoped schema fixture + per-test DELETE-sweep so the
suite runs in ~2.3s despite the ~50-table ORM surface.

Verified:
  - 199/199 pytest (173 original + 26 new audit integration) pass
  - tests/contracts/test_openapi_baseline.py green, OpenAPI 360/484 unchanged
  - mypy compliance/ -> Success: no issues found in 119 source files

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sharang Parnerkar
2026-04-07 18:39:40 +02:00
parent 4a91814bfc
commit 883ef702ac
6 changed files with 490 additions and 31 deletions

View File

@@ -14,9 +14,10 @@ the services are translated to HTTPException via
"""
import logging
from typing import List, Optional
from typing import Any, List, Optional
from fastapi import APIRouter, Depends, Query
from fastapi.responses import StreamingResponse
from sqlalchemy.orm import Session
from classroom_engine.database import get_db
@@ -57,7 +58,7 @@ def get_audit_signoff_service(db: Session = Depends(get_db)) -> AuditSignOffServ
async def create_audit_session(
request: CreateAuditSessionRequest,
service: AuditSessionService = Depends(get_audit_session_service),
):
) -> AuditSessionResponse:
"""Create a new audit session for structured compliance reviews."""
with translate_domain_errors():
return service.create(request)
@@ -67,7 +68,7 @@ async def create_audit_session(
async def list_audit_sessions(
status: Optional[str] = None,
service: AuditSessionService = Depends(get_audit_session_service),
):
) -> List[AuditSessionSummary]:
"""List all audit sessions, optionally filtered by status."""
with translate_domain_errors():
return service.list(status)
@@ -77,7 +78,7 @@ async def list_audit_sessions(
async def get_audit_session(
session_id: str,
service: AuditSessionService = Depends(get_audit_session_service),
):
) -> AuditSessionDetailResponse:
"""Get detailed information about a specific audit session."""
with translate_domain_errors():
return service.get(session_id)
@@ -87,7 +88,7 @@ async def get_audit_session(
async def start_audit_session(
session_id: str,
service: AuditSessionService = Depends(get_audit_session_service),
):
) -> dict[str, Any]:
"""Start an audit session (draft -> in_progress)."""
with translate_domain_errors():
return service.start(session_id)
@@ -97,7 +98,7 @@ async def start_audit_session(
async def complete_audit_session(
session_id: str,
service: AuditSessionService = Depends(get_audit_session_service),
):
) -> dict[str, Any]:
"""Complete an audit session (in_progress -> completed)."""
with translate_domain_errors():
return service.complete(session_id)
@@ -107,7 +108,7 @@ async def complete_audit_session(
async def archive_audit_session(
session_id: str,
service: AuditSessionService = Depends(get_audit_session_service),
):
) -> dict[str, Any]:
"""Archive a completed audit session."""
with translate_domain_errors():
return service.archive(session_id)
@@ -117,7 +118,7 @@ async def archive_audit_session(
async def delete_audit_session(
session_id: str,
service: AuditSessionService = Depends(get_audit_session_service),
):
) -> dict[str, Any]:
"""Delete a draft or archived audit session and all its sign-offs."""
with translate_domain_errors():
return service.delete(session_id)
@@ -136,7 +137,7 @@ async def get_audit_checklist(
regulation_filter: Optional[str] = None,
search: Optional[str] = None,
service: AuditSignOffService = Depends(get_audit_signoff_service),
):
) -> AuditChecklistResponse:
"""Get the paginated audit checklist for a session."""
with translate_domain_errors():
return service.get_checklist(
@@ -158,7 +159,7 @@ async def sign_off_item(
requirement_id: str,
request: SignOffRequest,
service: AuditSignOffService = Depends(get_audit_signoff_service),
):
) -> SignOffResponse:
"""Sign off on a specific requirement in an audit session."""
with translate_domain_errors():
return service.sign_off(session_id, requirement_id, request)
@@ -172,7 +173,7 @@ async def get_sign_off(
session_id: str,
requirement_id: str,
service: AuditSignOffService = Depends(get_audit_signoff_service),
):
) -> SignOffResponse:
"""Get the current sign-off status for a specific requirement."""
with translate_domain_errors():
return service.get_sign_off(session_id, requirement_id)
@@ -188,7 +189,7 @@ async def generate_audit_pdf_report(
language: str = Query("de", pattern="^(de|en)$"),
include_signatures: bool = Query(True),
service: AuditSessionService = Depends(get_audit_session_service),
):
) -> StreamingResponse:
"""Generate a PDF report for an audit session."""
with translate_domain_errors():
return service.generate_pdf(