generated from john/python-template
75 lines
3.0 KiB
Python
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
|