Files
transcription/tests/test_provider_boundaries.py
Jim Lancaster 0b48c80d87
Quality Gate / gate (push) Successful in 2m34s
Update instructions - Part 2, phase 3
2026-09-02 14:42:20 -05:00

75 lines
3.0 KiB
Python

"""Structural rules for the providers package.
`.github/instructions/providers.instructions.md` requires provider specifics to stop at the
adapter boundary: adapters translate an external API into `TranscriptionResult` and
`ProviderError`, and know nothing about persistence, services, or the UI. Without this guard the
rule is only advice, and a single convenience import of a service or model would invert the
dependency direction the architecture depends on.
"""
from __future__ import annotations
import ast
from pathlib import Path
PROVIDERS_DIR = Path(__file__).resolve().parents[1] / "src" / "transcription" / "providers"
# Application packages an adapter must never reach into. `config` is intentionally absent:
# adapters read Settings for timeouts and credentials.
FORBIDDEN_PACKAGES = frozenset({"services", "db", "ui", "api", "worker", "worker_service"})
def _module_paths() -> list[Path]:
return sorted(PROVIDERS_DIR.glob("*.py"))
def _imported_application_packages(tree: ast.Module) -> set[str]:
"""Return first-level `transcription.<package>` names imported by this module."""
imported: set[str] = set()
for node in ast.walk(tree):
if isinstance(node, ast.ImportFrom) and node.module:
parts = node.module.split(".")
if node.level == 0 and parts[0] == "transcription" and len(parts) > 1:
imported.add(parts[1])
elif isinstance(node, ast.Import):
for alias in node.names:
parts = alias.name.split(".")
if parts[0] == "transcription" and len(parts) > 1:
imported.add(parts[1])
return imported
def test_provider_modules_are_discovered():
"""Guard the guard: the rules below are meaningless if nothing is scanned."""
assert {path.stem for path in _module_paths()} >= {"base", "evidence", "openrouter"}
def test_provider_modules_do_not_import_application_layers():
"""A provider adapter must not depend on services, persistence, UI, API, or the worker."""
violations: dict[str, list[str]] = {}
for path in _module_paths():
tree = ast.parse(path.read_text(encoding="utf-8"))
found = sorted(_imported_application_packages(tree) & FORBIDDEN_PACKAGES)
if found:
violations[path.name] = found
assert violations == {}
def test_response_headers_are_filtered_through_the_allowlist():
"""Header persistence must be allowlist-based, not capture-then-redact."""
from transcription.providers.evidence import SAFE_RESPONSE_HEADERS
from transcription.providers.evidence import filter_safe_response_headers
filtered = filter_safe_response_headers(
{
"Content-Type": "application/json",
"Authorization": "Bearer super-secret",
"Set-Cookie": "session=super-secret",
"X-Unknown-Future-Header": "unreviewed",
}
)
assert filtered == {"content-type": "application/json"}
assert "authorization" not in SAFE_RESPONSE_HEADERS
assert "set-cookie" not in SAFE_RESPONSE_HEADERS