audit-core/tests/test_run_receipt_findings.py
tegwick 2b8c9097ad
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 3s
AUDIT-WP-0018-T03: report bounded-executor runs that started without a receipt
A sender may declare closing_classes; nk-bounded-executor declares
execution.run.started closed by execution.attribution.receipt within
7200s. GET /v1/stream-findings reports opening_without_closing past the
bound, counted from the receiver's accepted_at plus the heartbeat grace
allowance; a per-run data.window.not_after tightens but never extends.
Identifiers and timing only. The overlay may add or shorten, never
remove, rebind or lengthen. Both backends: 403 passed (1 unrelated
live-catalog env failure).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 482098@bnt-lap001
Assistant-Session: 9643c95a-a398-4f4a-b56a-6119be4e845e
2026-11-03 22:04:14 +01:00

294 lines
11 KiB
Python

"""AUDIT-WP-0018-T03. A started run with no receipt is a stream finding.
The bounded executor emits ``execution.run.started`` once it has consumed its
approval, and ``execution.attribution.receipt`` when the run ends. A crash in
between leaves only the start in custody. The scope overlay declares the pair
(``closing_classes``) and its bound; ``GET /v1/stream-findings`` reports every
start whose receipt has not arrived by then. Audit-core does not infer what
the run did.
"""
import copy
import hashlib
import json
import os
import uuid
from datetime import datetime, timedelta, timezone
import pytest
from audit_core import stream_findings
from audit_core.interface import AuditEvent
from audit_core.senders import SenderIdentity, SenderRegistry
from audit_core.sqlite_backend import SQLiteAuditBackend
from audit_core.stream_findings import UNCLOSED_OPENING, evaluate_closing
from test_bounded_executor_sender import ( # noqa: E402 (tests/ is on sys.path)
RUN_ID,
app, # noqa: F401 (fixture)
post,
receipt_event,
started_event,
)
from test_stream_findings import invoke # noqa: E402
SOURCE = "net-kingdom.bounded-executor"
STARTED = "execution.run.started"
RECEIPT = "execution.attribution.receipt"
PG_URL = os.environ.get("AUDIT_CORE_TEST_DATABASE_URL")
def _postgres_available() -> bool:
if not PG_URL:
return False
try:
import psycopg
with psycopg.connect(PG_URL, connect_timeout=3):
return True
except Exception:
return False
HAVE_PG = _postgres_available()
@pytest.fixture(params=["sqlite", "postgres"])
def backend(request, tmp_path):
if request.param == "sqlite":
yield SQLiteAuditBackend(str(tmp_path / "runs.db"))
return
if not HAVE_PG:
pytest.skip("no PostgreSQL reachable (set AUDIT_CORE_TEST_DATABASE_URL)")
from audit_core.postgres_backend import PostgresAuditBackend
schema = f"runs_{uuid.uuid4().hex[:12]}"
instance = PostgresAuditBackend(PG_URL, schema=schema)
try:
yield instance
finally:
with instance.pool.connection() as conn:
conn.execute(f'DROP SCHEMA IF EXISTS "{schema}" CASCADE')
instance.close()
def store(backend, event_id, action, correlation_id, *, source=SOURCE, data=None):
details = {"data": data or {"k": "v"}}
if correlation_id is not None:
details["correlation_id"] = correlation_id
event = AuditEvent(
event_id=event_id, source=source, action=action, resource="admission:a",
outcome="recorded", tenant="platform", scope="tenant", details=details,
observed_at="2026-11-03T10:00:00+00:00",
)
digest = hashlib.sha256(json.dumps(event.as_record(), sort_keys=True).encode()).hexdigest()
backend.accept(event, digest)
EPOCH = (datetime.now(timezone.utc) - timedelta(days=1)).isoformat()
# --- backend query, both backends --------------------------------------------
def test_a_start_without_a_receipt_is_unclosed(backend):
store(backend, "run:r1:started", STARTED, "r1")
rows = backend.unclosed_openings(SOURCE, STARTED, RECEIPT, EPOCH)
assert [(r["event_id"], r["correlation_id"]) for r in rows] == [("run:r1:started", "r1")]
assert rows[0]["accepted_at"]
assert rows[0]["window_not_after"] is None
def test_a_receipt_on_the_same_correlation_closes_the_start(backend):
store(backend, "run:r1:started", STARTED, "r1")
store(backend, "run:r1:receipt", RECEIPT, "r1")
store(backend, "run:r2:started", STARTED, "r2")
rows = backend.unclosed_openings(SOURCE, STARTED, RECEIPT, EPOCH)
assert [r["correlation_id"] for r in rows] == ["r2"]
def test_a_receipt_from_another_source_does_not_close(backend):
store(backend, "run:r1:started", STARTED, "r1")
store(backend, "other:r1:receipt", RECEIPT, "r1", source="someone-else")
rows = backend.unclosed_openings(SOURCE, STARTED, RECEIPT, EPOCH)
assert [r["correlation_id"] for r in rows] == ["r1"]
def test_a_start_without_a_correlation_id_can_never_close(backend):
store(backend, "run:x:started", STARTED, None)
store(backend, "run:x:receipt", RECEIPT, None)
rows = backend.unclosed_openings(SOURCE, STARTED, RECEIPT, EPOCH)
assert [r["event_id"] for r in rows] == ["run:x:started"]
def test_the_per_run_window_is_surfaced_and_a_malformed_one_is_not(backend):
store(backend, "run:r1:started", STARTED, "r1",
data={"window": {"not_after": "2026-11-03T12:00:00Z"}})
store(backend, "run:r2:started", STARTED, "r2", data={"window": {"not_after": 5}})
rows = {r["correlation_id"]: r for r in
backend.unclosed_openings(SOURCE, STARTED, RECEIPT, EPOCH)}
assert rows["r1"]["window_not_after"] == "2026-11-03T12:00:00Z"
assert rows["r2"]["window_not_after"] is None
def test_only_starts_accepted_since_the_lookback_are_examined(backend):
store(backend, "run:r1:started", STARTED, "r1")
future = (datetime.now(timezone.utc) + timedelta(hours=1)).isoformat()
assert backend.unclosed_openings(SOURCE, STARTED, RECEIPT, future) == []
# --- evaluator ---------------------------------------------------------------
EXECUTOR = SenderIdentity(
name="nk-bounded-executor", tokens=("t",), sources=frozenset({SOURCE}),
tenants=frozenset({"platform"}), evidence_kind="load-bearing",
closing_classes=((STARTED, RECEIPT, 7200),),
)
T0 = datetime(2026, 11, 3, 10, 0, tzinfo=timezone.utc)
def rows(**extra):
row = {"event_id": "run:r1:started", "correlation_id": "r1",
"accepted_at": T0.isoformat(), "window_not_after": None}
row.update(extra)
return lambda source, opening, closing, since: [row]
def test_no_finding_inside_the_bound_and_its_grace():
# 2 h bound plus the 1.5x grace allowance (1 h): due at 13:00.
assert evaluate_closing([EXECUTOR], rows(), now=T0 + timedelta(hours=2, minutes=59)) == []
def test_a_finding_past_the_bound_and_grace():
findings = evaluate_closing([EXECUTOR], rows(), now=T0 + timedelta(hours=3, minutes=1))
assert len(findings) == 1
body = findings[0].as_dict()
assert body["kind"] == UNCLOSED_OPENING
assert body["class"] == STARTED and body["closed_by"] == RECEIPT
assert body["correlation_id"] == "r1"
assert body["bound"] == "declared"
assert body["overdue_seconds"] == 60
assert "does not infer what the work did" in body["means"]
def test_a_per_run_window_tightens_the_deadline():
early = rows(window_not_after=(T0 + timedelta(minutes=30)).isoformat())
findings = evaluate_closing([EXECUTOR], early, now=T0 + timedelta(hours=1, minutes=31))
assert [f.bound for f in findings] == ["window.not_after"]
def test_a_per_run_window_never_extends_the_declared_bound():
late = rows(window_not_after=(T0 + timedelta(days=1)).isoformat())
findings = evaluate_closing([EXECUTOR], late, now=T0 + timedelta(hours=3, minutes=1))
assert [f.bound for f in findings] == ["declared"]
def test_the_lookback_is_the_custody_window():
seen = {}
def spy(source, opening, closing, since):
seen["since"] = since
return []
evaluate_closing([EXECUTOR], spy, now=T0)
assert seen["since"] == (T0 - timedelta(days=30)).isoformat()
def test_a_wildcard_source_is_not_held_to_a_close():
wild = SenderIdentity(
name="w", tokens=("w",), sources=frozenset({"*"}),
closing_classes=((STARTED, RECEIPT, 60),),
)
assert evaluate_closing([wild], rows(), now=T0 + timedelta(days=2)) == []
# --- declaration and overlay -------------------------------------------------
def _overlay(tmp_path, closing):
scope = tmp_path / "scope.json"
scope.write_text(json.dumps([{"name": "x", "closing_classes": closing}]))
secret = json.dumps([{
"name": "x", "tokens": ["x"], "sources": [SOURCE],
"closing_classes": {STARTED: {"closed_by": RECEIPT, "within_seconds": 7200}},
}])
return SenderRegistry.from_env({
"AUDIT_CORE_SENDERS": secret, "AUDIT_CORE_SENDERS_SCOPE_PATH": str(scope),
})
def test_the_overlay_may_shorten_the_bound(tmp_path):
reg = _overlay(tmp_path, {STARTED: {"closed_by": RECEIPT, "within_seconds": 3600}})
assert reg.authenticate("Bearer x").closing_classes == ((STARTED, RECEIPT, 3600),)
@pytest.mark.parametrize("closing", [
{STARTED: {"closed_by": RECEIPT, "within_seconds": 7201}},
{STARTED: {"closed_by": "something.else", "within_seconds": 7200}},
{"other.start": {"closed_by": RECEIPT, "within_seconds": 60}},
])
def test_the_overlay_may_not_lengthen_rebind_or_remove(tmp_path, closing):
with pytest.raises(ValueError):
_overlay(tmp_path, closing)
@pytest.mark.parametrize("closing", [
{STARTED: {"closed_by": RECEIPT}},
{STARTED: {"closed_by": RECEIPT, "within_seconds": 0}},
{STARTED: {"closed_by": STARTED, "within_seconds": 60}},
[STARTED, RECEIPT, 60],
])
def test_a_malformed_declaration_is_refused(closing):
secret = json.dumps([{"name": "x", "tokens": ["x"], "sources": [SOURCE],
"closing_classes": closing}])
with pytest.raises(ValueError):
SenderRegistry.from_env({"AUDIT_CORE_SENDERS": secret})
# --- end to end on the executor's own envelopes and the Git overlay ----------
@pytest.fixture
def three_hours_later(monkeypatch):
real = stream_findings.evaluate_closing
later = datetime.now(timezone.utc) + timedelta(hours=3, minutes=1)
monkeypatch.setattr(stream_findings, "evaluate_closing",
lambda ids, unclosed: real(ids, unclosed, now=later))
def _closing_findings(app): # noqa: F811
status, body = invoke(app, "/v1/stream-findings", token="operator")
assert status.startswith("200")
return [f for f in body["stream_findings"] if f["kind"] == UNCLOSED_OPENING]
def test_a_fresh_start_is_not_yet_a_finding(app): # noqa: F811
assert post(app, started_event())[0] == 202
assert _closing_findings(app) == []
def test_a_start_with_no_receipt_past_the_bound_is_reported(app, three_hours_later): # noqa: F811
assert post(app, started_event())[0] == 202
findings = _closing_findings(app)
assert [(f["sender"], f["correlation_id"]) for f in findings] == [
("nk-bounded-executor", RUN_ID)
]
assert findings[0]["event_id"] == f"run:{RUN_ID}:started"
# Identifiers and timing only: nothing from the run's data is disclosed.
assert "data" not in findings[0] and "details" not in findings[0]
def test_the_receipt_clears_the_finding(app, three_hours_later): # noqa: F811
assert post(app, started_event())[0] == 202
assert post(app, receipt_event())[0] == 202
assert _closing_findings(app) == []
def test_another_run_receipt_does_not_clear_this_one(app, three_hours_later): # noqa: F811
assert post(app, started_event())[0] == 202
other = copy.deepcopy(receipt_event())
other["id"] = "run:other:receipt"
other["correlation_id"] = "other"
other["data"]["run_id"] = "other"
assert post(app, other)[0] == 202
assert [f["correlation_id"] for f in _closing_findings(app)] == [RUN_ID]