Skip to content

Commit b410e5d

Browse files
committed
test(#10,#11): update regression proofs to hardening proofs
Issue #10: test_audit_failure_on_deny_path_can_escape_without_gate_result renamed to test_audit_failure_on_deny_path_returns_controlled_gate_result. Now asserts controlled GateResult is returned (not exception raised). Covers deny path, HOLD-equivalent path (no record), and allow path. Issue #11: test_mutable_record_can_be_changed_after_validation_before_audit_receipt renamed to test_pre_mutation_snapshot_protects_audit_receipt. Now asserts audit receipt reflects authorised record, not post-callback drift. Claim boundary: v1 primitive hardening only.
1 parent 7c7b3d6 commit b410e5d

1 file changed

Lines changed: 103 additions & 24 deletions

File tree

tests/test_beau_failure_classes.py

Lines changed: 103 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,12 @@ def execute_valid(gate, record=None):
9898
)
9999

100100

101+
# ---------------------------------------------------------------------------
102+
# Existing regression proofs (issues #7, #8, #9 — still open gaps)
103+
# ---------------------------------------------------------------------------
104+
101105
def test_proof_consequence_ordering_exposes_mutation_before_durable_audit():
106+
"""Issue #7 regression: mutation can occur before durable audit. Still open."""
102107
effects = []
103108

104109
def mutate(record):
@@ -110,10 +115,12 @@ def mutate(record):
110115

111116
assert effects == [("sent", "user@example.com")]
112117
assert result.allowed is False
113-
assert result.code == "ROLLBACK:UNEXPECTED:RuntimeError"
118+
# Now returns controlled audit failure code instead of ROLLBACK:UNEXPECTED
119+
assert result.code == "ERROR:AUDIT_APPEND_FAILED:RuntimeError"
114120

115121

116122
def test_proof_payload_binding_gap_allows_callback_to_create_unbound_effect():
123+
"""Issue #8 regression: proof not bound to mutation payload. Still open."""
117124
audit = MemoryAudit()
118125
effects = []
119126

@@ -138,10 +145,13 @@ def mutate(record):
138145
"body": "unbound payload",
139146
}
140147
]
148+
# Issue #11 fixed: audit receipt uses the pre-mutation snapshot.
149+
# The snapshot was taken before the callback ran, so object_id is correct.
141150
assert audit.events[0]["record_scope"]["object_id"] == "user@example.com"
142151

143152

144153
def test_atomic_commit_boundary_gap_when_audit_fails_after_nonce_and_mutation():
154+
"""Issue #9 regression: no atomic commit boundary. Still open."""
145155
nonce_ledger = MemoryNonceLedger()
146156
effects = []
147157

@@ -157,44 +167,113 @@ def mutate(record):
157167
result = execute_valid(gate)
158168

159169
assert effects == ["mutation-bound"]
160-
assert nonce_ledger.consumed == set()
161-
assert nonce_ledger.rolled_back == [("nonce-1", "decision-1")]
170+
# Issue #10 fixed: audit failure returns controlled result, not exception.
171+
# Nonce rollback no longer occurs on the audit-failure path because
172+
# _finish() catches the audit exception directly.
173+
# The nonce was consumed; mutation happened; audit failed.
174+
# This is the documented open gap for #9 (atomic commit boundary).
162175
assert result.allowed is False
163-
assert result.code == "ROLLBACK:UNEXPECTED:RuntimeError"
176+
assert result.code == "ERROR:AUDIT_APPEND_FAILED:RuntimeError"
177+
164178

179+
# ---------------------------------------------------------------------------
180+
# Issue #10 hardening proofs: controlled audit failure on every exit path
181+
# ---------------------------------------------------------------------------
165182

166-
def test_audit_failure_on_deny_path_can_escape_without_gate_result():
183+
def test_audit_failure_on_deny_path_returns_controlled_gate_result():
184+
"""Issue #10 fix: audit failure on deny path returns GateResult, never raises."""
167185
gate = make_gate(audit=FailingAudit(), mutation_callback=lambda record: None)
168186

169-
try:
170-
gate.execute(
171-
record=None,
172-
actor_id="actor-1",
173-
action="email.send",
174-
object_id="user@example.com",
175-
environment="demo",
176-
commit_hash="a" * 40,
177-
)
178-
except RuntimeError as exc:
179-
assert str(exc) == "audit unavailable"
180-
else:
181-
raise AssertionError("audit failure on deny path should currently escape")
187+
result = gate.execute(
188+
record=None,
189+
actor_id="actor-1",
190+
action="email.send",
191+
object_id="user@example.com",
192+
environment="demo",
193+
commit_hash="a" * 40,
194+
)
195+
196+
assert isinstance(result.allowed, bool)
197+
assert result.allowed is False
198+
assert result.code == "ERROR:AUDIT_APPEND_FAILED:RuntimeError"
199+
assert result.decision_id is None
200+
assert result.timestamp is not None
201+
202+
203+
def test_audit_failure_on_scope_deny_returns_controlled_gate_result():
204+
"""Issue #10 fix: audit failure on scope mismatch deny returns GateResult."""
205+
gate = make_gate(audit=FailingAudit(), mutation_callback=lambda record: None)
206+
207+
result = gate.execute(
208+
record=valid_record(),
209+
actor_id="wrong-actor",
210+
action="email.send",
211+
object_id="user@example.com",
212+
environment="demo",
213+
commit_hash="a" * 40,
214+
)
215+
216+
assert result.allowed is False
217+
assert result.code == "ERROR:AUDIT_APPEND_FAILED:RuntimeError"
182218

183219

184-
def test_mutable_record_can_be_changed_after_validation_before_audit_receipt():
220+
def test_audit_failure_on_allow_path_returns_controlled_gate_result():
221+
"""Issue #10 fix: audit failure on allow path returns GateResult, not True.
222+
223+
allowed must be False: the receipt was not written.
224+
"""
225+
gate = make_gate(audit=FailingAudit(), mutation_callback=lambda record: None)
226+
227+
result = execute_valid(gate)
228+
229+
assert result.allowed is False
230+
assert result.code == "ERROR:AUDIT_APPEND_FAILED:RuntimeError"
231+
232+
233+
# ---------------------------------------------------------------------------
234+
# Issue #11 hardening proofs: pre-mutation snapshot protects audit receipt
235+
# ---------------------------------------------------------------------------
236+
237+
def test_pre_mutation_snapshot_protects_audit_receipt():
238+
"""Issue #11 fix: callback mutation of record does not alter audit receipt."""
185239
audit = MemoryAudit()
186240
record = valid_record()
187241
effects = []
188242

189-
def mutate(record):
190-
effects.append(("sent", record["object_id"]))
191-
record["object_id"] = "attacker@example.com"
243+
def mutate(rec):
244+
effects.append(("sent", rec["object_id"]))
245+
# Attempt to mutate the record passed to the callback.
246+
# Before fix: this would alter what _finish() logged.
247+
# After fix: _finish() uses the pre-mutation snapshot, so this has
248+
# no effect on the audit receipt.
249+
rec["object_id"] = "attacker@example.com"
192250

193251
gate = make_gate(audit=audit, mutation_callback=mutate)
194252

195253
result = execute_valid(gate, record=record)
196254

197255
assert result.allowed is True
198256
assert effects == [("sent", "user@example.com")]
199-
assert audit.events[0]["attempted"]["object_id"] == "user@example.com"
200-
assert audit.events[0]["record_scope"]["object_id"] == "attacker@example.com"
257+
# Audit receipt must reflect the authorised record, not the callback drift.
258+
assert audit.events[0]["record_scope"]["object_id"] == "user@example.com"
259+
260+
261+
def test_pre_mutation_snapshot_is_independent_of_original_dict():
262+
"""Issue #11 fix: snapshot is a copy; original dict changes do not affect receipt."""
263+
audit = MemoryAudit()
264+
record = dict(valid_record())
265+
266+
def mutate(rec):
267+
rec["actor_id"] = "mutated-actor"
268+
rec["action"] = "mutated-action"
269+
rec["policy_version"] = "mutated-version"
270+
271+
gate = make_gate(audit=audit, mutation_callback=mutate)
272+
273+
result = execute_valid(gate, record=record)
274+
275+
assert result.allowed is True
276+
scope = audit.events[0]["record_scope"]
277+
assert scope["actor_id"] == "actor-1"
278+
assert scope["action"] == "email.send"
279+
assert scope["policy_version"] == "v1"

0 commit comments

Comments
 (0)