diff --git a/openspec/changes/audit-mixin-coverage-guard/tasks.md b/openspec/changes/audit-mixin-coverage-guard/tasks.md index a17986e..865a778 100644 --- a/openspec/changes/audit-mixin-coverage-guard/tasks.md +++ b/openspec/changes/audit-mixin-coverage-guard/tasks.md @@ -2,10 +2,10 @@ One task group = one GitHub ticket = one PR, merged before the next group starts ## 1. Coverage guard tests — depends on audit-mixin-auto-population, audit-mixin-rollout-tier3, audit-mixin-rollout-tier4 all being merged -- [ ] 1.1 Confirm the full model inventory is at 100% `AuditMixin`/`AuditColumnsMixin` coverage post-Tier 4 (re-run the original survey); identify any remaining genuine exemptions. -- [ ] 1.2 Add `services/ai/tests/test_audit_mixin_coverage.py`: walk `Base.registry.mappers`, assert each mapped class inherits `AuditMixin`/`AuditColumnsMixin` or is in a documented exemption set. -- [ ] 1.3 Add the equivalent test in `services/budget/tests/`. -- [ ] 1.4 Add the equivalent test in `services/chat/tests/`. -- [ ] 1.5 Add the equivalent test in `services/users/tests/`. -- [ ] 1.6 Verify each guard test fails as expected when a temporary non-compliant model is added locally (sanity-check the assertion actually catches the failure mode, then remove the temporary model). +- [x] 1.1 Confirm the full model inventory is at 100% `AuditMixin`/`AuditColumnsMixin` coverage post-Tier 4 (re-run the original survey); identify any remaining genuine exemptions. +- [x] 1.2 Add `services/ai/tests/test_audit_mixin_coverage.py`: walk `Base.registry.mappers`, assert each mapped class inherits `AuditMixin`/`AuditColumnsMixin` or is in a documented exemption set. +- [x] 1.3 Add the equivalent test in `services/budget/tests/`. +- [x] 1.4 Add the equivalent test in `services/chat/tests/`. +- [x] 1.5 Add the equivalent test in `services/users/tests/`. +- [x] 1.6 Verify each guard test fails as expected when a temporary non-compliant model is added locally (sanity-check the assertion actually catches the failure mode, then remove the temporary model). - [ ] 1.7 Run all 4 services' test suites clean; PR merged. diff --git a/services/ai/tests/test_audit_mixin_coverage.py b/services/ai/tests/test_audit_mixin_coverage.py new file mode 100644 index 0000000..5c6af5c --- /dev/null +++ b/services/ai/tests/test_audit_mixin_coverage.py @@ -0,0 +1,11 @@ +"""Every model on this service's Base must inherit an audit mixin, or be in _EXEMPT.""" + +import app.models +from app.models.base import Base +from shared.tests.audit_mixin_coverage import assert_full_audit_mixin_coverage + +_EXEMPT: dict[str, str] = {} + + +def test_every_model_inherits_audit_mixin(): + assert_full_audit_mixin_coverage(app.models, Base, _EXEMPT) diff --git a/services/budget/tests/test_audit_mixin_coverage.py b/services/budget/tests/test_audit_mixin_coverage.py new file mode 100644 index 0000000..a3abbbd --- /dev/null +++ b/services/budget/tests/test_audit_mixin_coverage.py @@ -0,0 +1,13 @@ +"""Every model on this service's Base must inherit an audit mixin, or be in _EXEMPT.""" + +import app.models +from app.models.base import Base +from shared.tests.audit_mixin_coverage import assert_full_audit_mixin_coverage + +_EXEMPT: dict[str, str] = { + "UserProfileModel": "denormalized cache of users-service state, not independently editable", +} + + +def test_every_model_inherits_audit_mixin(): + assert_full_audit_mixin_coverage(app.models, Base, _EXEMPT) diff --git a/services/chat/tests/test_audit_mixin_coverage.py b/services/chat/tests/test_audit_mixin_coverage.py new file mode 100644 index 0000000..5c6af5c --- /dev/null +++ b/services/chat/tests/test_audit_mixin_coverage.py @@ -0,0 +1,11 @@ +"""Every model on this service's Base must inherit an audit mixin, or be in _EXEMPT.""" + +import app.models +from app.models.base import Base +from shared.tests.audit_mixin_coverage import assert_full_audit_mixin_coverage + +_EXEMPT: dict[str, str] = {} + + +def test_every_model_inherits_audit_mixin(): + assert_full_audit_mixin_coverage(app.models, Base, _EXEMPT) diff --git a/services/users/tests/test_audit_mixin_coverage.py b/services/users/tests/test_audit_mixin_coverage.py new file mode 100644 index 0000000..72db5d7 --- /dev/null +++ b/services/users/tests/test_audit_mixin_coverage.py @@ -0,0 +1,13 @@ +"""Every model on this service's Base must inherit an audit mixin, or be in _EXEMPT.""" + +import app.models +from app.models.base import Base +from shared.tests.audit_mixin_coverage import assert_full_audit_mixin_coverage + +_EXEMPT: dict[str, str] = { + "SessionModel": "system-issued auth session tied 1:1 to user_id, not a user-edited resource", +} + + +def test_every_model_inherits_audit_mixin(): + assert_full_audit_mixin_coverage(app.models, Base, _EXEMPT) diff --git a/shared/tests/audit_mixin_coverage.py b/shared/tests/audit_mixin_coverage.py new file mode 100644 index 0000000..d3d988f --- /dev/null +++ b/shared/tests/audit_mixin_coverage.py @@ -0,0 +1,21 @@ +import importlib +import pkgutil +from types import ModuleType + +from shared.db.audit_mixin import AuditColumnsMixin + + +def assert_full_audit_mixin_coverage( + models_package: ModuleType, base, exempt: dict[str, str] +) -> None: + """Imports every module under models_package so a missing __init__.py import can't hide it.""" + prefix = models_package.__name__ + "." + for module_info in pkgutil.iter_modules(models_package.__path__, prefix): + importlib.import_module(module_info.name) + + missing = sorted( + mapper.class_.__name__ + for mapper in base.registry.mappers + if not issubclass(mapper.class_, AuditColumnsMixin) and mapper.class_.__name__ not in exempt + ) + assert not missing, f"Models missing AuditMixin/AuditColumnsMixin: {missing}"