"""Audit loop driver: pure decision logic for the project-level audit rework loop (track 1 of the production built-in loop). Pins the driver interface the next (wiring) track must use verbatim, and the counting rules shared by #244 / #241 / #247: pushbacks cover failures 1-3, the 4th failure (> 3, never >= 3) rebuilds, rebuilds are 1 per module per contract version, the 3rd failure is triaged, a pass clears failures but not budgets, a contract-version change clears everything, quota blocks consume nothing, a correction to pass does not unblock, and ledger events match the #241 field table. Style follows the other cloud tests: unittest, stdlib only, import with a sys.path fallback so the file also runs standalone. """ import sys import unittest from pathlib import Path try: from cloud.audit_loop_driver import ( ACTION_KINDS, LEDGER_EVENT_TYPES, Action, LoopState, apply_correction, build_ledger_event, confirm_rebuild, decide_audit, emit, new_loop_state, validate_ledger_event, after_fix, ) except ImportError: sys.path.insert(0, str(Path(__file__).resolve().parents[2])) from cloud.audit_loop_driver import ( ACTION_KINDS, LEDGER_EVENT_TYPES, Action, LoopState, apply_correction, build_ledger_event, confirm_rebuild, decide_audit, emit, new_loop_state, validate_ledger_event, after_fix, ) def fail_verdict(**overrides): verdict = { "source": "audit", "verdict": "fail", "verdict_text": "audit fail: ci red", "failures": [{"check": "ci_green", "detail": "ci red"}], "passed_items": [], "previous_artifacts": [{"path": "app.py", "sha256": "a" * 64}], "reproduction_steps": ["run ci"], } verdict.update(overrides) return verdict def pass_verdict(**overrides): verdict = { "source": "audit", "verdict": "pass", "verdict_text": "audit pass", "failures": [], "passed_items": [], } verdict.update(overrides) return verdict def near_complete_verdict(**overrides): verdict = fail_verdict( failures=[{"check": "edge-case"}], passed_items=[{"check": "p1"}, {"check": "p2"}, {"check": "p3"}], broken_passed_items=[], verdict_text="audit fail: one edge case left", ) verdict.update(overrides) return verdict def drive(state, verdict, **kwargs): return decide_audit(state, verdict, **kwargs) class PushbackCountingTestCase(unittest.TestCase): def test_fail_pushbacks_and_blocks(self): state = new_loop_state("m1", "v1") action = drive(state, fail_verdict()) self.assertEqual(action.kind, "pushback") self.assertTrue(action.merge_blocked) self.assertTrue(action.release_blocked) self.assertEqual(action.next_state.failure_count, 1) self.assertEqual(action.next_state.pushback_round, 1) self.assertEqual(action.next_state.status, "pushback") package = action.pushback_package self.assertEqual(package["audit_verdict_text"], "audit fail: ci red") self.assertEqual(package["failures"][0]["check"], "ci_green") self.assertEqual(package["reproduction_steps"], ["run ci"]) # Full re-audit criteria, never just the failed items. self.assertIn("ci_green", package["reaudit_criteria"]) self.assertIn("audit_signer_separation", package["reaudit_criteria"]) self.assertEqual(package["previous_artifacts"][0]["path"], "app.py") self.assertEqual( [e["event_type"] for e in action.ledger_events], ["audit_verdict", "audit_pushback_round"]) def test_exactly_three_still_pushes_back(self): state = new_loop_state("m1", "v1") for _ in range(3): action = drive(state, fail_verdict()) state = action.next_state self.assertEqual(action.kind, "pushback") self.assertEqual(action.next_state.failure_count, 3) self.assertEqual(action.next_state.pushback_round, 3) def test_fourth_failure_rebuilds(self): state = new_loop_state("m1", "v1") for _ in range(3): state = drive(state, fail_verdict()).next_state action = drive(state, fail_verdict(retained_tests=["t1", "t2"])) self.assertEqual(action.kind, "rebuild") self.assertEqual(action.rebuild_no, 1) self.assertEqual(action.next_state.rebuild_no, 1) self.assertEqual(action.next_state.failure_count, 0) self.assertEqual(action.next_state.pushback_round, 0) self.assertEqual(action.next_state.status, "rebuilding") self.assertEqual(action.next_state.retained_tests, ["t1", "t2"]) retained = action.pushback_package["retained_assets"] self.assertEqual(retained["tests"], ["t1", "t2"]) self.assertTrue(retained["contract_unchanged"]) self.assertTrue(retained["design_unchanged"]) def test_rebuild_budget_exhausted_escalates(self): state = new_loop_state("m1", "v1") for _ in range(3): state = drive(state, fail_verdict()).next_state state = drive(state, fail_verdict()).next_state # rebuild 1 self.assertEqual(state.rebuild_no, 1) for _ in range(3): state = drive(state, fail_verdict()).next_state action = drive(state, fail_verdict()) self.assertEqual(action.kind, "escalate") self.assertEqual(action.escalation_target, "coordinator") self.assertEqual(action.next_state.rebuild_no, 1) self.assertEqual(action.next_state.status, "escalated") self.assertFalse(action.escalation_grants_user_acceptance) self.assertFalse(hasattr(action, "tick_acceptance")) # Full history, not just the last round. texts = [entry.get("verdict_text") for entry in action.escalation_history] self.assertIn("audit fail: ci red", texts) self.assertGreaterEqual(len(action.escalation_history), 4) def test_audit_and_acceptance_counts_combine_but_stay_separate(self): state = new_loop_state("m1", "v1") state = drive(state, fail_verdict()).next_state state = drive(state, fail_verdict(source="acceptance")).next_state action = drive(state, fail_verdict()) self.assertEqual(action.next_state.failure_count, 3) self.assertEqual(action.next_state.audit_fail_count, 2) self.assertEqual(action.next_state.acceptance_fail_count, 1) def test_pass_clears_failures_keeps_rebuild_budget(self): state = new_loop_state("m1", "v1") for _ in range(3): state = drive(state, fail_verdict()).next_state state = drive(state, fail_verdict()).next_state # rebuild 1 state = drive(state, fail_verdict()).next_state # failure 1, new episode action = drive(state, pass_verdict()) self.assertEqual(action.kind, "pass") self.assertFalse(action.merge_blocked) self.assertEqual(action.next_state.failure_count, 0) self.assertEqual(action.next_state.pushback_round, 0) self.assertEqual(action.next_state.rebuild_no, 1) self.assertEqual(action.next_state.status, "passed") def test_contract_version_change_clears_everything(self): state = new_loop_state("m1", "v1") for _ in range(3): state = drive(state, fail_verdict()).next_state state = drive(state, fail_verdict()).next_state # rebuild 1 state = drive(state, fail_verdict()).next_state # failure 1, v1 action = drive(state, fail_verdict(contract_version="v2")) self.assertEqual(action.kind, "pushback") self.assertEqual(action.next_state.contract_version, "v2") self.assertEqual(action.next_state.failure_count, 1) self.assertEqual(action.next_state.pushback_round, 1) self.assertEqual(action.next_state.rebuild_no, 0) self.assertFalse(action.next_state.targeted_used) class ThirdFailureTriageTestCase(unittest.TestCase): def _two_failures(self): state = new_loop_state("m1", "v1") for _ in range(2): state = drive(state, fail_verdict()).next_state return state def test_structural_third_failure_early_rebuilds(self): state = self._two_failures() action = drive(state, fail_verdict( failures=[{"check": "arch", "structural_kind": "architecture_boundary"}], verdict_text="audit fail: architecture boundary crossed")) self.assertEqual(action.kind, "rebuild") self.assertEqual(action.rebuild_no, 1) assessments = [e for e in action.ledger_events if e["event_type"] == "audit_third_failure_assessment"] self.assertEqual(len(assessments), 1) self.assertEqual(assessments[0]["payload"]["determination"], "early_rebuild") self.assertEqual(assessments[0]["payload"]["failure_total"], 3) self.assertEqual(assessments[0]["payload"]["max_extra_rounds"], 0) def test_contract_root_third_failure_goes_to_refreeze(self): state = self._two_failures() action = drive(state, fail_verdict(root_cause_hint="contract")) self.assertEqual(action.kind, "refreeze") self.assertEqual(action.next_state.status, "refreeze") self.assertEqual(action.next_state.rebuild_no, 0) self.assertEqual(action.compat_hint, "audit_wiring_event:escalate_refreeze") # No rebuild event, and no assessment row forced into the # wrong determination (see #247: early_refreeze has no # assessment determination). kinds = [e["event_type"] for e in action.ledger_events] self.assertNotIn("audit_rebuild", kinds) self.assertNotIn("audit_third_failure_assessment", kinds) def test_near_complete_gets_one_targeted_round_then_rebuilds(self): state = self._two_failures() action = drive(state, near_complete_verdict()) self.assertEqual(action.kind, "pushback") self.assertEqual(action.scope, "targeted") self.assertEqual(action.targeted_items, ["edge-case"]) self.assertTrue(action.next_state.targeted_used) self.assertEqual(action.next_state.pushback_round, 3) assessments = [e for e in action.ledger_events if e["event_type"] == "audit_third_failure_assessment"] self.assertEqual(assessments[0]["payload"]["determination"], "continuation_round") self.assertEqual(assessments[0]["payload"]["max_extra_rounds"], 1) # One more failure is the 4th: rebuild, not a 4th pushback. action = drive(action.next_state, fail_verdict()) self.assertEqual(action.kind, "rebuild") def test_targeted_budget_not_replenished_by_pass(self): state = self._two_failures() state = drive(state, near_complete_verdict()).next_state self.assertTrue(state.targeted_used) state = drive(state, pass_verdict()).next_state self.assertTrue(state.targeted_used) for _ in range(2): state = drive(state, fail_verdict()).next_state action = drive(state, near_complete_verdict()) self.assertEqual(action.kind, "pushback") self.assertEqual(action.scope, "normal") self.assertIsNone(action.targeted_items) def test_unknown_broken_passed_disqualifies_near_complete(self): state = self._two_failures() # broken_passed_items absent (unknown, not []) -> normal. action = drive(state, fail_verdict( failures=[{"check": "edge-case"}], passed_items=[{"check": "p1"}, {"check": "p2"}, {"check": "p3"}])) self.assertEqual(action.scope, "normal") class RebuildGateAndAcceptanceTestCase(unittest.TestCase): def _rebuilding_state(self): state = new_loop_state("m1", "v1") for _ in range(3): state = drive(state, fail_verdict()).next_state return drive(state, fail_verdict(retained_tests=["t1", "t2"])).next_state def test_old_test_gate_missing_result_counts_as_failure(self): state = self._rebuilding_state() action = after_fix(state, {"reaudit_verdict": pass_verdict()}) self.assertEqual(action.kind, "pushback") self.assertEqual(action.next_state.failure_count, 1) self.assertEqual(action.next_state.status, "pushback") checks = [item.get("check") for item in action.pushback_package["failures"]] self.assertIn("rebuild_first_gate", checks) def test_old_test_gate_failing_result_counts_as_failure(self): state = self._rebuilding_state() action = after_fix(state, { "reaudit_verdict": pass_verdict(), "retained_test_results": {"t1": True, "t2": False}, }) self.assertEqual(action.kind, "pushback") self.assertEqual(action.next_state.failure_count, 1) def test_old_test_gate_passing_allows_reaudit_pass(self): state = self._rebuilding_state() action = after_fix(state, { "reaudit_verdict": pass_verdict(), "retained_test_results": {"t1": True, "t2": True}, }) self.assertEqual(action.kind, "pass") self.assertEqual(action.next_state.status, "passed") def _acceptance_pending(self): state = new_loop_state("m1", "v1") for _ in range(3): state = drive(state, fail_verdict(source="acceptance")).next_state return drive(state, fail_verdict(source="acceptance")) def test_acceptance_rebuild_unconfirmed_does_not_start(self): action = self._acceptance_pending() self.assertEqual(action.kind, "pending_user_confirmation") self.assertTrue(action.next_state.pending_confirmation) self.assertEqual(action.next_state.rebuild_no, 0) self.assertEqual(action.next_state.status, "pending_user_confirmation") notices = [e for e in action.ledger_events if e["event_type"] == "audit_rebuild_notice"] self.assertEqual(notices[0]["payload"]["confirmation_status"], "pending") def test_acceptance_rebuild_confirmed_then_starts(self): pending = self._acceptance_pending().next_state action = confirm_rebuild(pending, user_confirmed=True) self.assertEqual(action.kind, "rebuild") self.assertEqual(action.rebuild_no, 1) self.assertEqual(action.next_state.status, "rebuilding") self.assertFalse(action.next_state.pending_confirmation) notices = [e for e in action.ledger_events if e["event_type"] == "audit_rebuild_notice"] self.assertEqual(notices[0]["payload"]["confirmation_status"], "confirmed") self.assertEqual(notices[0]["payload"]["confirmed_by"], "user") def test_acceptance_rebuild_rejected_escalates_without_budget(self): pending = self._acceptance_pending().next_state action = confirm_rebuild(pending, user_confirmed=False) self.assertEqual(action.kind, "escalate") self.assertEqual(action.escalation_target, "coordinator") self.assertEqual(action.next_state.rebuild_no, 0) def test_confirm_without_pending_raises(self): state = new_loop_state("m1", "v1") with self.assertRaises(ValueError): confirm_rebuild(state, user_confirmed=True) class AfterFixTestCase(unittest.TestCase): def test_after_fix_pass_clears(self): state = new_loop_state("m1", "v1") state = drive(state, fail_verdict()).next_state action = after_fix(state, {"reaudit_verdict": pass_verdict()}) self.assertEqual(action.kind, "pass") self.assertEqual(action.next_state.failure_count, 0) def test_after_fix_failing_reaudit_goes_to_next_pushback(self): state = new_loop_state("m1", "v1") state = drive(state, fail_verdict()).next_state action = after_fix(state, {"reaudit_verdict": fail_verdict()}) self.assertEqual(action.kind, "pushback") self.assertEqual(action.next_state.failure_count, 2) self.assertEqual(action.next_state.pushback_round, 2) def test_after_fix_diff_only_raises(self): state = new_loop_state("m1", "v1") state = drive(state, fail_verdict()).next_state with self.assertRaises(ValueError): after_fix(state, {"diff": "--- a/x\n+++ b/x\n"}) with self.assertRaises(ValueError): after_fix(state, {"diff_only": True, "reaudit_verdict": pass_verdict()}) class CorrectionTestCase(unittest.TestCase): def test_correction_to_pass_does_not_unblock(self): state = new_loop_state("m1", "v1") for _ in range(2): state = drive(state, fail_verdict()).next_state action = apply_correction(state, { "original_verdict": "fail", "corrected_verdict": "pass", "reviewer": "auditor-b", "original_entry_id": "e1", "reason": "second look: evidence was sufficient", }) # Counting view: effective count is 0 ... self.assertEqual(action.next_state.failure_count, 0) # ... but the gate view is still blocked until a fresh pass. self.assertTrue(action.merge_blocked) self.assertTrue(action.release_blocked) self.assertEqual(action.next_state.status, "blocked") self.assertTrue(action.requires_reaudit) self.assertEqual(action.correction_action, "schedule_reaudit") self.assertEqual(action.ledger_events[0]["event_type"], "audit_meta_review") # Only a fresh passing verdict unblocks. action = drive(action.next_state, pass_verdict()) self.assertEqual(action.kind, "pass") self.assertFalse(action.merge_blocked) def test_correction_to_fail_reblocks_at_once(self): state = drive(new_loop_state("m1", "v1"), pass_verdict()).next_state action = apply_correction(state, { "original_verdict": "pass", "corrected_verdict": "fail", "reviewer": "auditor-b", "original_entry_id": "e1", "reason": "missed a failing check", }) self.assertEqual(action.kind, "pushback") self.assertTrue(action.merge_blocked) self.assertEqual(action.next_state.failure_count, 1) self.assertEqual(action.correction_action, "retrigger_pushback_and_reblock") class QuotaAndCostTestCase(unittest.TestCase): def test_quota_block_consumes_nothing(self): state = new_loop_state("m1", "v1") for _ in range(2): state = drive(state, fail_verdict()).next_state action = drive(state, fail_verdict(), quota_check=lambda estimate: False) self.assertEqual(action.kind, "quota_blocked") self.assertIs(action.next_state, state) self.assertEqual(action.next_state.failure_count, 2) self.assertEqual(action.next_state.pushback_round, 2) def test_quota_block_at_rebuild_consumes_no_rebuild(self): state = new_loop_state("m1", "v1") for _ in range(3): state = drive(state, fail_verdict()).next_state action = drive(state, fail_verdict(), quota_check=lambda estimate: False) self.assertEqual(action.kind, "quota_blocked") self.assertEqual(action.next_state.rebuild_no, 0) self.assertEqual(action.next_state.failure_count, 3) def test_quota_check_exception_fails_closed(self): state = new_loop_state("m1", "v1") def _boom(estimate): raise RuntimeError("quota service down") action = drive(state, fail_verdict(), quota_check=_boom) self.assertEqual(action.kind, "quota_blocked") self.assertEqual(action.next_state.failure_count, 0) def test_cost_threshold_escalates_rebuild_to_user(self): state = new_loop_state("m1", "v1") for _ in range(3): state = drive(state, fail_verdict()).next_state action = drive(state, fail_verdict(cost_estimate=500), cost_threshold=100) self.assertEqual(action.kind, "escalate") self.assertEqual(action.escalation_target, "user") self.assertEqual(action.next_state.rebuild_no, 0) self.assertEqual(action.cost_estimate, 500) self.assertGreaterEqual(len(action.escalation_history), 4) self.assertFalse(action.escalation_grants_user_acceptance) def test_cost_threshold_not_applied_to_pushback(self): # #244 scope preserved: the threshold gates rebuild steps, # a fix pushback is quota-checked only. state = new_loop_state("m1", "v1") action = drive(state, fail_verdict(cost_estimate=500), cost_threshold=100) self.assertEqual(action.kind, "pushback") class TestChangeTestCase(unittest.TestCase): def test_test_deletion_violation_does_not_advance(self): state = new_loop_state("m1", "v1") state = drive(state, fail_verdict()).next_state action = drive(state, fail_verdict(test_changes=[ {"action": "delete", "test_id": "t1"}])) self.assertEqual(action.kind, "escalate") self.assertTrue(action.violations) self.assertEqual(action.next_state.failure_count, 1) self.assertEqual(action.next_state.pushback_round, 1) def test_test_deletion_allowed_with_refreeze_and_audit(self): state = new_loop_state("m1", "v1") action = drive(state, fail_verdict(test_changes=[ {"action": "delete", "test_id": "t1", "separate_audit_id": "audit-9"}], refreeze_approved=True, separate_audit_ids=["audit-9"])) self.assertEqual(action.kind, "pushback") self.assertEqual(action.violations, []) class LedgerEventTestCase(unittest.TestCase): def test_all_seven_event_types_validate(self): cases = { "audit_verdict": {"module_id": "m1", "contract_version": "v1", "verdict": "pass", "source": "audit", "failures": []}, "audit_pushback_round": {"module_id": "m1", "contract_version": "v1", "round_no": 1, "trigger_items": ["ci_green"], "pushback_package": {}, "round_status": "pushed"}, "audit_rebuild": {"module_id": "m1", "contract_version": "v1", "rebuild_no": 1, "root_cause": "implementation", "status": "decided", "retained_assets": {}}, "audit_third_failure_assessment": { "module_id": "m1", "contract_version": "v1", "determination": "continuation_round", "basis": {}, "failure_total": 3, "max_extra_rounds": 1}, "audit_test_change": {"module_id": "m1", "contract_version": "v1", "test_changes": {"added": ["t9"], "modified": [], "deleted": []}, "auditor": "auditor-a", "contract_refrozen": False, "reason": "add only"}, "audit_meta_review": {"original_entry_id": "e1", "original_verdict": "fail", "corrected_verdict": "pass", "misjudgment_found": True, "reviewer": "auditor-b", "findings": [], "triggered_process": "re-review"}, "audit_rebuild_notice": {"module_id": "m1", "contract_version": "v1", "rebuild_no": 1, "notification_content": "notice text", "notified_party": "user", "confirmation_status": "pending"}, } for event_type, payload in cases.items(): self.assertEqual(validate_ledger_event(event_type, payload), [], event_type) event = emit(event_type, payload) self.assertEqual(event["event_type"], event_type) self.assertEqual(set(LEDGER_EVENT_TYPES), set(cases)) def test_unknown_event_type_and_missing_field_rejected(self): self.assertTrue(validate_ledger_event("nope", {})) with self.assertRaises(ValueError): emit("audit_verdict", {"module_id": "m1"}) with self.assertRaises(ValueError): emit("audit_verdict", {"module_id": "m1", "contract_version": "v1", "verdict": "pass", "source": "audit"}) def test_conditional_maps_to_fail_and_keeps_original(self): event = emit("audit_verdict", { "module_id": "m1", "contract_version": "v1", "verdict": "conditional", "source": "audit", "failures": []}) self.assertEqual(event["payload"]["verdict"], "fail") self.assertEqual(event["payload"]["extra"]["original_verdict"], "conditional") # A driver verdict of conditional is a fail, not a pass. action = drive(new_loop_state("m1", "v1"), fail_verdict(verdict="conditional")) self.assertEqual(action.kind, "pushback") def test_test_change_hard_gate_in_emit(self): payload = {"module_id": "m1", "contract_version": "v1", "test_changes": {"added": [], "modified": ["t1"], "deleted": []}, "auditor": "auditor-a", "contract_refrozen": False, "reason": "drop a test"} with self.assertRaises(ValueError): emit("audit_test_change", payload) payload["contract_refrozen"] = True payload["refreeze_ref"] = "freeze-2" emit("audit_test_change", payload) # no raise def test_rebuild_notice_confirmed_needs_confirmer(self): payload = {"module_id": "m1", "contract_version": "v1", "rebuild_no": 1, "notification_content": "notice", "notified_party": "user", "confirmation_status": "confirmed"} with self.assertRaises(ValueError): emit("audit_rebuild_notice", payload) payload["confirmed_by"] = "user" emit("audit_rebuild_notice", payload) # no raise def test_action_kinds_are_bounded(self): for kind in ACTION_KINDS: self.assertIn(kind, ("pass", "pushback", "rebuild", "refreeze", "escalate", "pending_user_confirmation", "quota_blocked")) action = drive(new_loop_state("m1", "v1"), fail_verdict()) self.assertIn(action.kind, ACTION_KINDS) if __name__ == "__main__": unittest.main()