approval-engine/tests/test_claim_contract.py
tegwick adb5cb0a9a test: enforce the presentation exclusion contract for approval digests
Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a07ff8-19d0-7820-b4d0-1353833cb7fc
2026-09-10 18:39:30 +02:00

91 lines
3.5 KiB
Python

import json
from pathlib import Path
from approval_engine.binding import binding_digest
from tests.conftest import approve, binding
ROOT = Path(__file__).resolve().parents[1]
def test_schema_forbids_decision_keys():
schema = json.loads((ROOT / "schemas/approval_claim.schema.json").read_text())
assert "not" in schema
kinds = [item["required"][0] for item in schema["not"]["anyOf"]]
assert set(kinds) >= {"effect", "decision", "allow", "deny"}
def test_claim_has_issuer_digest_freshness(engine):
obj = approve(engine)
claim = engine.claim(obj.id)
assert claim["kind"] == "approval-claim"
assert claim["issuer"] == "approval-engine"
assert claim["approval_id"] == obj.id
assert claim["valid_now"] is True
assert claim["reason_code"] == "ok"
assert claim["binding"]["digest"] == binding_digest(binding())
assert claim["freshness"]["ttl_seconds"] == 30
assert claim["freshness"]["not_after"] == "2026-08-29T12:00:30+00:00"
assert "effect" not in claim
assert "decision" not in claim
assert "yields_to" in claim
def test_wrong_target_changes_digest():
a = binding()
b = binding(target={"id": "other-lane", "stage": "prod"})
assert binding_digest(a) != binding_digest(b)
def test_wrong_action_changes_digest():
assert binding_digest(binding()) != binding_digest(binding(action="secrets.kv.read"))
def test_pdp_digest_is_recorded_not_recomputed(engine):
pdp = "sha256:" + "ab" * 32
obj = approve(engine, pdp_digest=pdp)
claim = engine.claim(obj.id)
assert claim["binding"]["pdp_digest"] == pdp
assert claim["binding"]["digest"] == binding_digest(binding())
assert claim["binding"]["digest"] != pdp
def test_examples_are_claim_shaped():
for name in ("claim.valid.json", "claim.revoked.json"):
data = json.loads((ROOT / "examples" / name).read_text())
assert data["kind"] == "approval-claim"
assert data["issuer"] == "approval-engine"
for forbidden in ("effect", "decision", "allow", "deny"):
assert forbidden not in data
def test_presentation_changes_cannot_change_the_approved_act(engine):
"""GH-DEC-2026-015: presentation cannot enter the native digest preimage."""
act = binding()
original = approve(engine, extra_binding=act)
original_claim = engine.claim(original.id)
presentation = {
"view_hash": "sha256:" + "12" * 32,
"brief": "First explanation",
"packet": {"sections": ["summary"]},
"highlights": ["target"],
"locale": "de-DE",
"ui_release": "surface-v1",
"presentation": {"binding_digest": original_claim["binding"]["digest"]},
}
first = approve(engine, extra_binding={**act, **presentation})
changed = {key: {"changed": value} for key, value in presentation.items()}
second = approve(engine, extra_binding={**act, **changed})
for obj in (first, second):
assert engine.claim(obj.id)["binding"]["digest"] == original_claim["binding"]["digest"]
assert obj.binding == original.binding
# The invariant is about presentation, not permission to ignore act changes.
for field, replacement in {
"action": "secrets.kv.read",
"actor": "different-agent",
"principal": "different-principal",
"purpose": "different-purpose",
"target": {"id": "different-lane", "stage": "prod"},
}.items():
other = approve(engine, extra_binding={**act, **presentation, field: replacement})
assert engine.claim(other.id)["binding"]["digest"] != original_claim["binding"]["digest"]