From 948e8a3e8f717c0fd80af76f3dfe47935d81c912 Mon Sep 17 00:00:00 2001 From: tegwick Date: Sun, 6 Sep 2026 20:35:41 +0200 Subject: [PATCH] =?UTF-8?q?AUDIT-WP-0009-T08=20=E2=80=94=20assert=20the=20?= =?UTF-8?q?approval-validity=20prohibition=20by=20test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit §9.4 forbids audit-core answering whether an approval is still valid: a consumer branching on such an answer would route an authorization decision through the audit fabric. The prohibition was honoured by absence, which is not the estate's idiom — §6.4 obligation 3 requires a published stance to equal shipped behaviour asserted by test. Written against shapes rather than today's route list, so adding a validity surface later fails here rather than passing quietly. Plausible verdict paths must 404 rather than 403: a distinguishable forbidden would imply a surface exists behind auth. No backend class, Postgres included, may carry a verdict-shaped method name. The declaration must stay in layer.yaml, INTENT.md and SCOPE.md. The last test guards the opposite error — an approval-shaped event class is still ingestible, because the prohibition bounds the verdict, not the record. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_0185wifnLzCxjEY2MT1XbK7L Assistant: claude-code Assistant-Model: opus Assistant-Process: 713962@bnt-lap001 Assistant-Session: 2718d99d-d3ff-478f-83a2-3a30f01a02fc --- tests/test_approval_validity_prohibition.py | 122 ++++++++++++++++++ ...AUDIT-WP-0009-evidence-role-conformance.md | 2 +- 2 files changed, 123 insertions(+), 1 deletion(-) create mode 100644 tests/test_approval_validity_prohibition.py diff --git a/tests/test_approval_validity_prohibition.py b/tests/test_approval_validity_prohibition.py new file mode 100644 index 0000000..b17200b --- /dev/null +++ b/tests/test_approval_validity_prohibition.py @@ -0,0 +1,122 @@ +"""AUDIT-WP-0009-T08 — the §9.4 approval-validity prohibition, asserted. + +The prohibition has until now been honoured by absence, which is not the +estate's idiom: §6.4 obligation 3 requires a published stance to equal shipped +behaviour *asserted by test*. A prohibition stated in `INTENT.md`, `SCOPE.md` +and `layer.yaml` is worth asserting in `tests/`. + +Audit Core records approval events. It renders no verdict on whether an +approval is still valid — a consumer branching on such an answer would route an +authorization decision through the audit fabric, and `access-engine` is the +estate's only decision point (§6). Operative approval state belongs to +`approval-engine`. + +These tests are deliberately written against *shapes* rather than a fixed list +of today's routes, so that adding a validity surface later fails here rather +than passing quietly. +""" + +from __future__ import annotations + +import inspect +from pathlib import Path + +import pytest + +from audit_core import interface +from audit_core.ingestion import IngestionApplication +from audit_core.sqlite_backend import SQLiteAuditBackend + +from test_ingestion import invoke + +REPO = Path(__file__).resolve().parents[1] + +# Paths a consumer looking for a verdict would plausibly try. None may answer. +VERDICT_PATHS = [ + "/v1/approvals", + "/v1/approvals/appr-1", + "/v1/approvals/appr-1/valid", + "/v1/approvals/appr-1/validity", + "/v1/approvals/appr-1/status", + "/v1/approval-validity", + "/v1/approval-status", + "/v1/is-approved", +] + +# Method-name fragments that would mean a backend answers the question. +VERDICT_NAME_FRAGMENTS = ( + "approval_valid", + "approval_validity", + "is_approved", + "approval_status", + "check_approval", + "approval_state", +) + + +@pytest.fixture +def app(tmp_path): + return IngestionApplication(SQLiteAuditBackend(str(tmp_path / "events.db")), "opaque") + + +@pytest.mark.parametrize("path", VERDICT_PATHS) +def test_no_route_answers_whether_an_approval_is_valid(app, path): + status, _ = invoke(app, None, path=path, method="GET", body=b"") + # 404 is the right answer: the surface does not exist. Anything in the 2xx + # range would be a verdict, and a 403 would imply one exists behind auth. + assert status.startswith("404"), f"{path} answered {status}" + + +def test_no_backend_exposes_an_approval_verdict_method(): + """The contract and every implementation, not only the one in use.""" + from audit_core import mock_file_backend, sqlite_backend + + modules = [interface, sqlite_backend, mock_file_backend] + try: # optional dependency; skipping it would weaken the assertion silently + from audit_core import postgres_backend + + modules.append(postgres_backend) + except ImportError: + pass + + offenders = [] + for module in modules: + for _, obj in inspect.getmembers(module, inspect.isclass): + if obj.__module__ != module.__name__: + continue + for name in dir(obj): + lowered = name.lower() + if any(fragment in lowered for fragment in VERDICT_NAME_FRAGMENTS): + offenders.append(f"{module.__name__}.{obj.__name__}.{name}") + assert not offenders, f"approval-validity surface appeared: {offenders}" + + +def test_the_prohibition_is_declared_where_a_reader_would_look(): + """Shipped behaviour above is only half of §6.4 obligation 3.""" + layer = (REPO / "layer.yaml").read_text() + assert "approval_validity_query: forbidden" in layer + + intent = (REPO / "INTENT.md").read_text() + assert "no approval-validity query" in intent.lower() + + scope = (REPO / "SCOPE.md").read_text() + assert "approval-validity query" in scope.lower() + + +def test_recording_an_approval_event_is_still_allowed(app): + """The prohibition bounds the verdict, not the record. Guards over-reading. + + Emitted here from the one registered sender: `approval-engine` is not yet + admitted as a source (AUDIT-WP-0009-T09), and this test asserts that an + approval-shaped *event class* is ingestible, not that its future sender is + already registered. A 400 `source_not_allowed` is sender admission working, + which is a different control. + """ + from test_ingestion import event + + status, _ = invoke( + app, + event(id="evt-approval-1", type="approval.revoked"), + key="evt-approval-1", + ) + assert status.startswith("202") diff --git a/workplans/AUDIT-WP-0009-evidence-role-conformance.md b/workplans/AUDIT-WP-0009-evidence-role-conformance.md index 46ec582..da43aa2 100644 --- a/workplans/AUDIT-WP-0009-evidence-role-conformance.md +++ b/workplans/AUDIT-WP-0009-evidence-role-conformance.md @@ -169,7 +169,7 @@ than adding a new shape. ```task id: AUDIT-WP-0009-T08 -status: todo +status: done priority: medium state_hub_task_id: "f7b513ee-af48-5c5a-a34e-3e9922d29b76" ```