audit: wire loop driver to server endpoints and release gate (track 2)
Server-side execution for the track-1 pure driver (#260), no driver semantics changed: audit verdict / fix completion / correction / rebuild confirmation endpoints persist LoopState in audit_loop_state, write every step through the #241/#247 ledger path (ledger first, state second, fail-closed), derive fix/rebuild subtasks, and block merge/release via the existing #247 chain reader. POST /tasks/{id}/release follows the #259 contract (global-admin write, fresh hard-gate evidence, merged status, multi-source 409 with no audit write on block); deploy.py stays outside task gates.
This commit is contained in:
1 parent
91b61e5302
commit
3eac2a702e
4 files changed
+2374
No files matched your search
@@ -0,0 +1,439 @@
|
||||
"""Server wiring for the #260 loop driver (track 2, module level).
|
||||
|
||||
Uses a real #241/#247 chain (LedgerStore on a temp file) and a
|
||||
real sqlite loop-state connection -- no production tasks, no
|
||||
GitHub, no production DB. The one fake is FailingLedgerStore,
|
||||
used only to prove ledger-failure fail-closed behaviour.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sqlite3
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
from cloud import audit_loop_wiring as wiring
|
||||
from cloud.audit_wiring import pushback_block_for_task
|
||||
from cloud.ledger import LedgerStore
|
||||
from cloud.ledger import verify_chain
|
||||
except ImportError:
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
|
||||
from cloud import audit_loop_wiring as wiring
|
||||
from cloud.audit_wiring import pushback_block_for_task
|
||||
from cloud.ledger import LedgerStore
|
||||
from cloud.ledger import verify_chain
|
||||
|
||||
MODULE = "mod-a"
|
||||
VERSION = "v1"
|
||||
OLD_REF = {"archive_ref": "task:parent:pre-rebuild"}
|
||||
EXTRA_PASS = {
|
||||
"security_unconditional": True,
|
||||
"regression_exact_artifact": True,
|
||||
"demo_runnable": True,
|
||||
"known_deviation_due_todo": True,
|
||||
"audit_signer_separation": True,
|
||||
"qa_failed_links_rerun": True,
|
||||
}
|
||||
HARD_PASS = {
|
||||
"files_changed": ["apps/web/a.py"],
|
||||
"owned_dir": "apps/web",
|
||||
"protection": "protected",
|
||||
"ci_status": "success",
|
||||
"ci_checks": [{"name": "ci", "status": "success"}],
|
||||
"required_checks": ["ci"],
|
||||
"contract_version": "v1",
|
||||
"contract_frozen": True,
|
||||
"coordinator_approved": True,
|
||||
"unresolved_review_comments": 0,
|
||||
}
|
||||
|
||||
|
||||
def fail_verdict(text="check x failed", source="audit", **extra):
|
||||
verdict = {
|
||||
"verdict": "fail",
|
||||
"source": source,
|
||||
"verdict_text": text,
|
||||
"failures": [{"check": "ci_green", "detail": text}],
|
||||
"passed_items": ["a", "b", "c"],
|
||||
"contract_version": VERSION,
|
||||
}
|
||||
verdict.update(extra)
|
||||
return verdict
|
||||
|
||||
|
||||
def pass_verdict(**extra):
|
||||
verdict = {
|
||||
"verdict": "pass",
|
||||
"source": "audit",
|
||||
"verdict_text": "all green",
|
||||
"failures": [],
|
||||
"passed_items": ["a", "b", "c"],
|
||||
"contract_version": VERSION,
|
||||
}
|
||||
verdict.update(extra)
|
||||
return verdict
|
||||
|
||||
|
||||
class FailingLedgerStore:
|
||||
"""Every append raises: proves the wiring never saves state
|
||||
after a ledger failure."""
|
||||
|
||||
def __getattr__(self, name):
|
||||
if name.startswith("append_"):
|
||||
def _raise(*args, **kwargs):
|
||||
raise RuntimeError("ledger down")
|
||||
return _raise
|
||||
raise AttributeError(name)
|
||||
|
||||
|
||||
class WiringTestCase(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.tmp = tempfile.TemporaryDirectory()
|
||||
self.ledger_path = os.path.join(self.tmp.name, "ledger.db")
|
||||
self.store = LedgerStore(self.ledger_path)
|
||||
self.conn = sqlite3.connect(":memory:")
|
||||
self.task = {"id": "task-parent", "project_id": "proj-1",
|
||||
"module_id": MODULE, "contract_version": VERSION}
|
||||
|
||||
def tearDown(self):
|
||||
try:
|
||||
self.store.close()
|
||||
except Exception:
|
||||
pass
|
||||
self.conn.close()
|
||||
self.tmp.cleanup()
|
||||
|
||||
def _block(self, task_id="task-parent"):
|
||||
return pushback_block_for_task(self.store.conn, task_id)
|
||||
|
||||
def _verdict(self, verdict, **kwargs):
|
||||
kwargs.setdefault("user_id", "auditor-1")
|
||||
return wiring.process_audit_verdict(
|
||||
self.conn, self.store, self.task, verdict, **kwargs)
|
||||
|
||||
def test_module_mapping_explicit_wins_and_fallback_is_task_id(self):
|
||||
self.assertEqual(
|
||||
wiring.resolve_module_id(
|
||||
{"id": "t1", "module_id": "m-explicit"}), "m-explicit")
|
||||
self.assertEqual(
|
||||
wiring.resolve_module_id({"id": "t1"}), "t1")
|
||||
self.assertEqual(
|
||||
wiring.resolve_module_id({"id": "t1", "module": "m2"}), "m2")
|
||||
with self.assertRaises(ValueError):
|
||||
wiring.resolve_contract_version({"id": "t1"}, None)
|
||||
self.assertEqual(
|
||||
wiring.resolve_contract_version(
|
||||
{"id": "t1"}, {"contract_version": "v9"}), "v9")
|
||||
|
||||
def test_state_persists_and_reloads_from_table_not_memory(self):
|
||||
result = self._verdict(fail_verdict())
|
||||
self.assertEqual(result["action_kind"], "pushback")
|
||||
self.assertEqual(result["subtask"]["kind"], "fix")
|
||||
self.assertTrue(result["merge_blocked"])
|
||||
reloaded = wiring.load_loop_state(self.conn, MODULE, VERSION)
|
||||
self.assertEqual(reloaded.pushback_round, 1)
|
||||
self.assertEqual(reloaded.failure_count, 1)
|
||||
self.assertEqual(reloaded.status, "pushback")
|
||||
self.assertTrue(self._block()["merge_blocked"])
|
||||
# The chain itself verifies: the wiring wrote real rows.
|
||||
self.assertTrue(verify_chain(self.store.conn)["ok"])
|
||||
|
||||
def test_full_chain_fail_fix_reaudit_pass_unblocks(self):
|
||||
first = self._verdict(fail_verdict())
|
||||
fix_task = {"id": "task-fix-1", "project_id": "proj-1",
|
||||
"module_id": MODULE, "contract_version": VERSION}
|
||||
result = wiring.process_fix_completion(
|
||||
self.conn, self.store, fix_task,
|
||||
{"reaudit_verdict": pass_verdict()},
|
||||
automated={"hard_gate": HARD_PASS, "extra_checks": EXTRA_PASS},
|
||||
user_id="auditor-1", ledger_task_id="task-parent")
|
||||
self.assertEqual(result["action_kind"], "pass")
|
||||
self.assertIsNone(result["subtask"])
|
||||
self.assertFalse(result["merge_blocked"])
|
||||
self.assertTrue(
|
||||
result["automated_checks"]["automated_checks_pass"])
|
||||
# Parent task dict untouched by the wiring layer: the
|
||||
# original task is never rolled back by this module.
|
||||
self.assertNotIn("status", self.task)
|
||||
self.assertFalse(self._block()["merge_blocked"])
|
||||
state = wiring.load_loop_state(self.conn, MODULE, VERSION)
|
||||
self.assertEqual(state.status, "passed")
|
||||
self.assertEqual(state.failure_count, 0)
|
||||
|
||||
def test_automated_failure_forces_claimed_pass_to_fail(self):
|
||||
self._verdict(fail_verdict())
|
||||
fix_task = {"id": "task-fix-1", "project_id": "proj-1",
|
||||
"module_id": MODULE, "contract_version": VERSION}
|
||||
broken_extra = dict(EXTRA_PASS, demo_runnable=False)
|
||||
result = wiring.process_fix_completion(
|
||||
self.conn, self.store, fix_task,
|
||||
{"reaudit_verdict": pass_verdict()},
|
||||
automated={"hard_gate": HARD_PASS,
|
||||
"extra_checks": broken_extra},
|
||||
user_id="auditor-1", ledger_task_id="task-parent")
|
||||
# Automated checks passing is never audit passing, and
|
||||
# here they did not pass: the claimed pass is forced fail.
|
||||
self.assertEqual(result["action_kind"], "pushback")
|
||||
self.assertTrue(result["merge_blocked"])
|
||||
self.assertIn("demo_runnable",
|
||||
result["automated_checks"]["failed_checks"])
|
||||
self.assertTrue(self._block()["merge_blocked"])
|
||||
|
||||
def test_fix_without_reaudit_verdict_and_diff_only_refused(self):
|
||||
self._verdict(fail_verdict())
|
||||
fix_task = {"id": "task-fix-1", "project_id": "proj-1",
|
||||
"module_id": MODULE, "contract_version": VERSION}
|
||||
with self.assertRaises(ValueError):
|
||||
wiring.process_fix_completion(
|
||||
self.conn, self.store, fix_task, {"fix_diff": {}},
|
||||
user_id="auditor-1", ledger_task_id="task-parent")
|
||||
with self.assertRaises(ValueError):
|
||||
wiring.process_fix_completion(
|
||||
self.conn, self.store, fix_task,
|
||||
{"diff_only": True,
|
||||
"reaudit_verdict": pass_verdict()},
|
||||
user_id="auditor-1", ledger_task_id="task-parent")
|
||||
# Fix with no prior loop at all is refused, not invented.
|
||||
fresh_conn = sqlite3.connect(":memory:")
|
||||
try:
|
||||
with self.assertRaises(ValueError):
|
||||
wiring.process_fix_completion(
|
||||
fresh_conn, self.store, fix_task,
|
||||
{"reaudit_verdict": pass_verdict()},
|
||||
user_id="auditor-1", ledger_task_id="task-parent")
|
||||
finally:
|
||||
fresh_conn.close()
|
||||
|
||||
def test_exactly_three_pushes_back_fourth_rebuilds(self):
|
||||
for _ in range(3):
|
||||
result = self._verdict(fail_verdict())
|
||||
self.assertEqual(result["action_kind"], "pushback")
|
||||
# Fourth failure without a caller-supplied old impl ref
|
||||
# is refused BEFORE any ledger row: state stays at 3.
|
||||
with self.assertRaises(ValueError):
|
||||
self._verdict(fail_verdict())
|
||||
state = wiring.load_loop_state(self.conn, MODULE, VERSION)
|
||||
self.assertEqual(state.pushback_round, 3)
|
||||
self.assertEqual(state.failure_count, 3)
|
||||
result = self._verdict(fail_verdict(), old_impl_ref=OLD_REF)
|
||||
self.assertEqual(result["action_kind"], "rebuild")
|
||||
self.assertEqual(result["subtask"]["kind"], "rebuild")
|
||||
state = wiring.load_loop_state(self.conn, MODULE, VERSION)
|
||||
self.assertEqual(state.rebuild_no, 1)
|
||||
self.assertEqual(state.status, "rebuilding")
|
||||
|
||||
def test_targeted_third_round_scope(self):
|
||||
self._verdict(fail_verdict())
|
||||
self._verdict(fail_verdict())
|
||||
result = self._verdict(fail_verdict(
|
||||
failures=[{"check": "ci_green", "detail": "one left"}],
|
||||
passed_items=["a", "b", "c"],
|
||||
broken_passed_items=[]))
|
||||
self.assertEqual(result["action_kind"], "pushback")
|
||||
self.assertEqual(result["subtask"]["scope"], "targeted")
|
||||
self.assertEqual(result["subtask"]["targeted_items"],
|
||||
["ci_green"])
|
||||
|
||||
def test_correction_to_pass_does_not_unblock(self):
|
||||
self._verdict(fail_verdict())
|
||||
verdict_rows = self.store.conn.execute(
|
||||
"SELECT id FROM audit_ledger WHERE action = ?"
|
||||
" ORDER BY id ASC", ("audit_verdict",)).fetchall()
|
||||
entry_id = verdict_rows[0][0]
|
||||
result = wiring.process_correction(
|
||||
self.conn, self.store, self.task,
|
||||
{"original_verdict": "fail", "corrected_verdict": "pass",
|
||||
"reviewer": "reviewer-1", "original_entry_id": entry_id,
|
||||
"findings": ["misjudged"], "contract_version": VERSION},
|
||||
user_id="reviewer-1", ledger_task_id="task-parent")
|
||||
self.assertEqual(result["correction_action"], "schedule_reaudit")
|
||||
self.assertTrue(result["merge_blocked"])
|
||||
self.assertEqual(result["next_state"].failure_count, 0)
|
||||
self.assertEqual(result["next_state"].status, "blocked")
|
||||
self.assertEqual(result["subtask"]["kind"], "reaudit")
|
||||
# Counting view (0) vs gate view (still blocked) is the
|
||||
# #247 intended divergence, pinned here, not papered over.
|
||||
self.assertTrue(self._block()["merge_blocked"])
|
||||
# Only a fresh passing verdict unblocks.
|
||||
self._verdict(pass_verdict())
|
||||
self.assertFalse(self._block()["merge_blocked"])
|
||||
|
||||
def test_correction_to_fail_reblocks_at_once(self):
|
||||
self._verdict(pass_verdict())
|
||||
self.assertFalse(self._block()["merge_blocked"])
|
||||
verdict_rows = self.store.conn.execute(
|
||||
"SELECT id FROM audit_ledger WHERE action = ?"
|
||||
" ORDER BY id ASC", ("audit_verdict",)).fetchall()
|
||||
entry_id = verdict_rows[-1][0]
|
||||
result = wiring.process_correction(
|
||||
self.conn, self.store, self.task,
|
||||
{"original_verdict": "pass", "corrected_verdict": "fail",
|
||||
"reviewer": "reviewer-1", "original_entry_id": entry_id,
|
||||
"findings": ["missed failure"],
|
||||
"contract_version": VERSION},
|
||||
user_id="reviewer-1", ledger_task_id="task-parent")
|
||||
self.assertEqual(
|
||||
result["correction_action"], "retrigger_pushback_and_reblock")
|
||||
self.assertTrue(result["merge_blocked"])
|
||||
self.assertEqual(result["next_state"].failure_count, 1)
|
||||
self.assertTrue(self._block()["merge_blocked"])
|
||||
|
||||
def test_acceptance_rebuild_pending_confirm_reject_no_budget(self):
|
||||
for _ in range(4):
|
||||
result = self._verdict(
|
||||
fail_verdict(source="acceptance"))
|
||||
self.assertEqual(result["action_kind"],
|
||||
"pending_user_confirmation")
|
||||
self.assertIsNone(result["subtask"])
|
||||
self.assertIsNotNone(result["notification"])
|
||||
state = wiring.load_loop_state(self.conn, MODULE, VERSION)
|
||||
self.assertTrue(state.pending_confirmation)
|
||||
self.assertEqual(state.rebuild_no, 0)
|
||||
# System/coordinator markers can never confirm.
|
||||
with self.assertRaises(ValueError):
|
||||
wiring.process_rebuild_confirmation(
|
||||
self.conn, self.store, self.task,
|
||||
user_confirmed=True, confirmed_by="system",
|
||||
user_id="user-1", ledger_task_id="task-parent")
|
||||
with self.assertRaises(ValueError):
|
||||
wiring.process_rebuild_confirmation(
|
||||
self.conn, self.store, self.task,
|
||||
user_confirmed=True, confirmed_by="coordinator",
|
||||
user_id="user-1", ledger_task_id="task-parent")
|
||||
# Rejection escalates and consumes no rebuild budget.
|
||||
rejected = wiring.process_rebuild_confirmation(
|
||||
self.conn, self.store, self.task,
|
||||
user_confirmed=False, confirmed_by="user-1",
|
||||
user_id="user-1", ledger_task_id="task-parent")
|
||||
self.assertEqual(rejected["action_kind"], "escalate")
|
||||
state = wiring.load_loop_state(self.conn, MODULE, VERSION)
|
||||
self.assertEqual(state.rebuild_no, 0)
|
||||
|
||||
def test_acceptance_rebuild_confirm_then_rebuilds(self):
|
||||
for _ in range(4):
|
||||
self._verdict(fail_verdict(source="acceptance"))
|
||||
confirmed = wiring.process_rebuild_confirmation(
|
||||
self.conn, self.store, self.task,
|
||||
user_confirmed=True, confirmed_by="user-1",
|
||||
old_impl_ref=OLD_REF,
|
||||
user_id="user-1", ledger_task_id="task-parent")
|
||||
self.assertEqual(confirmed["action_kind"], "rebuild")
|
||||
self.assertEqual(confirmed["subtask"]["kind"], "rebuild")
|
||||
state = wiring.load_loop_state(self.conn, MODULE, VERSION)
|
||||
self.assertEqual(state.rebuild_no, 1)
|
||||
self.assertEqual(state.status, "rebuilding")
|
||||
# Confirming again with no pending confirmation fails.
|
||||
with self.assertRaises(ValueError):
|
||||
wiring.process_rebuild_confirmation(
|
||||
self.conn, self.store, self.task,
|
||||
user_confirmed=True, confirmed_by="user-1",
|
||||
user_id="user-1", ledger_task_id="task-parent")
|
||||
|
||||
def test_quota_blocked_consumes_nothing(self):
|
||||
self._verdict(fail_verdict())
|
||||
blocked = self._verdict(
|
||||
fail_verdict(), quota_check=lambda estimate: False)
|
||||
self.assertEqual(blocked["action_kind"], "quota_blocked")
|
||||
self.assertFalse(blocked["state_saved"])
|
||||
state = wiring.load_loop_state(self.conn, MODULE, VERSION)
|
||||
self.assertEqual(state.pushback_round, 1)
|
||||
self.assertEqual(state.failure_count, 1)
|
||||
|
||||
def _raising(estimate):
|
||||
raise RuntimeError("quota service down")
|
||||
blocked2 = self._verdict(
|
||||
fail_verdict(), quota_check=_raising)
|
||||
self.assertEqual(blocked2["action_kind"], "quota_blocked")
|
||||
state = wiring.load_loop_state(self.conn, MODULE, VERSION)
|
||||
self.assertEqual(state.pushback_round, 1)
|
||||
|
||||
def test_quota_adapter_matches_server_rule(self):
|
||||
check = wiring.build_quota_check(1000000, 999999)
|
||||
self.assertTrue(check(None))
|
||||
self.assertFalse(check(0.01))
|
||||
self.assertTrue(wiring.build_quota_check(None, 10 ** 9)(100.0))
|
||||
|
||||
def test_cost_threshold_escalates_rebuild_with_full_history(self):
|
||||
for _ in range(3):
|
||||
self._verdict(fail_verdict())
|
||||
result = self._verdict(
|
||||
fail_verdict(cost_estimate=100.0), cost_threshold=10.0,
|
||||
old_impl_ref=OLD_REF)
|
||||
self.assertEqual(result["action_kind"], "escalate")
|
||||
self.assertEqual(result["notification"]["party"], "user")
|
||||
self.assertEqual(
|
||||
len(result["notification"]["escalation_history"]), 4)
|
||||
self.assertFalse(
|
||||
result["escalation_grants_user_acceptance"])
|
||||
|
||||
def test_ledger_failure_fail_closed_state_not_saved(self):
|
||||
failing = FailingLedgerStore()
|
||||
with self.assertRaises(RuntimeError):
|
||||
wiring.process_audit_verdict(
|
||||
self.conn, failing, self.task, fail_verdict(),
|
||||
user_id="auditor-1")
|
||||
self.assertIsNone(
|
||||
wiring.load_loop_state(self.conn, MODULE, VERSION))
|
||||
|
||||
def test_automated_reaudit_evidence_missing_fails(self):
|
||||
result = wiring.evaluate_automated_reaudit(None, {})
|
||||
self.assertFalse(result["automated_checks_pass"])
|
||||
self.assertIn("hard_gate_evidence_missing",
|
||||
result["failed_checks"])
|
||||
result = wiring.evaluate_automated_reaudit(
|
||||
HARD_PASS, EXTRA_PASS)
|
||||
self.assertTrue(result["automated_checks_pass"])
|
||||
# Merge-body vocabulary is translated, not rejected:
|
||||
# branch_protection -> the hard gate's protection.
|
||||
body_style = dict(HARD_PASS)
|
||||
body_style["branch_protection"] = body_style.pop("protection")
|
||||
result = wiring.evaluate_automated_reaudit(
|
||||
body_style, EXTRA_PASS)
|
||||
self.assertTrue(result["automated_checks_pass"])
|
||||
|
||||
def test_release_gate_multi_source_and_read_only(self):
|
||||
self._verdict(fail_verdict())
|
||||
before = self.store.conn.execute(
|
||||
"SELECT COUNT(*) FROM audit_ledger").fetchone()[0]
|
||||
decision = wiring.evaluate_release_gate(
|
||||
"task-parent", self.store.conn,
|
||||
task_status="completed", hard_gate={"allowed": False,
|
||||
"blocks": ["ci_not_green"]})
|
||||
self.assertFalse(decision["allowed"])
|
||||
self.assertEqual(decision["http_status"], 409)
|
||||
self.assertEqual(decision["blocked_sources"],
|
||||
["task_status", "hard_gate", "audit_pushback"])
|
||||
self.assertEqual(decision["code"], "release_requires_merged")
|
||||
after = self.store.conn.execute(
|
||||
"SELECT COUNT(*) FROM audit_ledger").fetchone()[0]
|
||||
self.assertEqual(before, after)
|
||||
self.assertTrue(verify_chain(self.store.conn)["ok"])
|
||||
|
||||
def test_release_gate_evidence_missing_and_allowed(self):
|
||||
decision = wiring.evaluate_release_gate(
|
||||
"task-parent", self.store.conn,
|
||||
task_status="merged", hard_gate=None)
|
||||
self.assertEqual(decision["code"],
|
||||
"release_hard_gate_evidence_missing")
|
||||
decision = wiring.evaluate_release_gate(
|
||||
"task-parent", self.store.conn,
|
||||
task_status="merged",
|
||||
hard_gate={"allowed": True, "blocks": []})
|
||||
self.assertTrue(decision["allowed"])
|
||||
self.assertEqual(decision["code"], "release_allowed")
|
||||
|
||||
def test_release_gate_chain_read_failure_fail_closed(self):
|
||||
closed = sqlite3.connect(":memory:")
|
||||
closed.close()
|
||||
decision = wiring.evaluate_release_gate(
|
||||
"task-parent", closed, task_status="merged",
|
||||
hard_gate={"allowed": True, "blocks": []})
|
||||
self.assertFalse(decision["allowed"])
|
||||
self.assertEqual(
|
||||
decision["code"], "release_blocked_state_unavailable")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,457 @@
|
||||
"""HTTP end-to-end tests for the audit-loop wiring (track 2).
|
||||
|
||||
Same harness shape as test_server_merge.py: a real server on a
|
||||
temp DB, no production tasks, no GitHub. Covers the automatic
|
||||
chain through HTTP (fail -> fix subtask -> merge 409 -> fix
|
||||
completion -> full re-audit pass -> merge 200 -> release),
|
||||
plus the release contract, acceptance-confirmation flow, and
|
||||
the quota / correction boundaries.
|
||||
"""
|
||||
|
||||
import http.client
|
||||
import json
|
||||
import os
|
||||
import sqlite3
|
||||
import sys
|
||||
import tempfile
|
||||
import threading
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
from cloud.auth import AuthStore
|
||||
from cloud.server import create_server
|
||||
except ImportError:
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
|
||||
from cloud.auth import AuthStore
|
||||
from cloud.server import create_server
|
||||
|
||||
MODULE = "mod-http"
|
||||
VERSION = "v1"
|
||||
EXTRA_PASS = {
|
||||
"security_unconditional": True,
|
||||
"regression_exact_artifact": True,
|
||||
"demo_runnable": True,
|
||||
"known_deviation_due_todo": True,
|
||||
"audit_signer_separation": True,
|
||||
"qa_failed_links_rerun": True,
|
||||
}
|
||||
|
||||
|
||||
def hard_body():
|
||||
return {
|
||||
"files_changed": ["apps/web/a.py"],
|
||||
"owned_dir": "apps/web",
|
||||
"branch_protection": "protected",
|
||||
"ci_status": "success",
|
||||
"ci_checks": [{"name": "ci", "status": "success"}],
|
||||
"required_checks": ["ci"],
|
||||
"contract_version": VERSION,
|
||||
"contract_frozen": True,
|
||||
"coordinator_approved": True,
|
||||
"unresolved_review_comments": 0,
|
||||
}
|
||||
|
||||
|
||||
def fail_body(source="audit"):
|
||||
return {
|
||||
"module_id": MODULE,
|
||||
"contract_version": VERSION,
|
||||
"verdict": {
|
||||
"verdict": "fail",
|
||||
"source": source,
|
||||
"verdict_text": "check x failed",
|
||||
"failures": [{"check": "ci_green", "detail": "x"}],
|
||||
"passed_items": ["a", "b", "c"],
|
||||
"contract_version": VERSION,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def pass_body():
|
||||
return {
|
||||
"module_id": MODULE,
|
||||
"contract_version": VERSION,
|
||||
"verdict": {
|
||||
"verdict": "pass",
|
||||
"source": "audit",
|
||||
"verdict_text": "all green",
|
||||
"failures": [],
|
||||
"passed_items": ["a", "b", "c"],
|
||||
"contract_version": VERSION,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class AuditLoopServerTestCase(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.tmp = tempfile.TemporaryDirectory()
|
||||
self.db_path = os.path.join(self.tmp.name, "control.db")
|
||||
self._saved_env = {
|
||||
k: os.environ.get(k) for k in (
|
||||
"CLOUD_MERGE_GATE_MODE", "CLOUD_BRANCH_PROTECTION",
|
||||
"CODEX_GLOBAL_ADMINS",
|
||||
"CLOUD_AUDIT_LOOP_COST_THRESHOLD_USD")}
|
||||
os.environ.pop("CLOUD_MERGE_GATE_MODE", None)
|
||||
os.environ.pop("CLOUD_BRANCH_PROTECTION", None)
|
||||
os.environ.pop("CLOUD_AUDIT_LOOP_COST_THRESHOLD_USD", None)
|
||||
self.auth = AuthStore(self.db_path)
|
||||
self.owner_id, self.owner_key = self.auth.create_user("owner")
|
||||
self.maint_id, self.maint_key = self.auth.create_user("maint")
|
||||
self.contrib_id, self.contrib_key = self.auth.create_user(
|
||||
"contrib")
|
||||
self.outsider_id, self.outsider_key = self.auth.create_user(
|
||||
"outsider")
|
||||
os.environ["CODEX_GLOBAL_ADMINS"] = self.owner_id
|
||||
self.server = create_server(self.db_path, "127.0.0.1", 0)
|
||||
self.port = self.server.server_address[1]
|
||||
self.thread = threading.Thread(
|
||||
target=self.server.serve_forever, daemon=True)
|
||||
self.thread.start()
|
||||
status, self.project = self._req(
|
||||
"POST", "/projects", {"name": "demo"}, self.owner_key)
|
||||
self.assertEqual(status, 201)
|
||||
self.auth.set_member(
|
||||
self.project["id"], self.owner_id, self.maint_id,
|
||||
"maintainer")
|
||||
self.auth.set_member(
|
||||
self.project["id"], self.owner_id, self.contrib_id,
|
||||
"contributor")
|
||||
|
||||
def tearDown(self):
|
||||
self.server.shutdown()
|
||||
self.server.server_close()
|
||||
self.thread.join(timeout=5)
|
||||
for key, value in self._saved_env.items():
|
||||
if value is None:
|
||||
os.environ.pop(key, None)
|
||||
else:
|
||||
os.environ[key] = value
|
||||
try:
|
||||
self.auth.conn.close()
|
||||
except Exception:
|
||||
pass
|
||||
self.tmp.cleanup()
|
||||
|
||||
def _req(self, method, path, body=None, key=None):
|
||||
conn = http.client.HTTPConnection(
|
||||
"127.0.0.1", self.port, timeout=10)
|
||||
hdrs = {}
|
||||
payload = None
|
||||
if body is not None:
|
||||
payload = json.dumps(body).encode("utf-8")
|
||||
hdrs["Content-Type"] = "application/json"
|
||||
if key is not None:
|
||||
hdrs["X-API-Key"] = key
|
||||
try:
|
||||
conn.request(method, path, body=payload, headers=hdrs)
|
||||
resp = conn.getresponse()
|
||||
data = resp.read()
|
||||
finally:
|
||||
conn.close()
|
||||
try:
|
||||
parsed = json.loads(data.decode("utf-8")) if data else None
|
||||
except ValueError:
|
||||
parsed = None
|
||||
return resp.status, parsed
|
||||
|
||||
def _completed_task(self):
|
||||
status, task = self._req(
|
||||
"POST", "/tasks",
|
||||
{"project_id": self.project["id"], "prompt": "x"},
|
||||
self.owner_key)
|
||||
self.assertEqual(status, 201)
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
try:
|
||||
conn.execute(
|
||||
"UPDATE tasks SET status = 'completed' WHERE id = ?",
|
||||
(task["id"],))
|
||||
conn.commit()
|
||||
finally:
|
||||
conn.close()
|
||||
return task
|
||||
|
||||
def _task_status(self, task_id):
|
||||
status, got = self._req(
|
||||
"GET", "/tasks/%s" % task_id, key=self.owner_key)
|
||||
self.assertEqual(status, 200)
|
||||
return got["status"]
|
||||
|
||||
def _chain_count(self):
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
try:
|
||||
return conn.execute(
|
||||
"SELECT COUNT(*) FROM audit_ledger").fetchone()[0]
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def test_full_chain_fail_fix_reaudit_pass_merge_release(self):
|
||||
task = self._completed_task()
|
||||
status, first = self._req(
|
||||
"POST", "/tasks/%s/audit-verdict" % task["id"],
|
||||
fail_body(), self.owner_key)
|
||||
self.assertEqual(status, 200)
|
||||
self.assertEqual(first["action_kind"], "pushback")
|
||||
self.assertEqual(first["subtask"]["kind"], "fix")
|
||||
fix_task = first["subtask_task"]
|
||||
self.assertIsNotNone(fix_task)
|
||||
self.assertEqual(fix_task["status"], "queued")
|
||||
# Original task stays completed: fail blocks, it does
|
||||
# not roll the task back into a re-run.
|
||||
self.assertEqual(self._task_status(task["id"]), "completed")
|
||||
# Merge is blocked by the chain pushback even with a
|
||||
# fully green hard gate.
|
||||
status, blocked = self._req(
|
||||
"POST", "/tasks/%s/merge" % task["id"],
|
||||
hard_body(), self.owner_key)
|
||||
self.assertEqual(status, 409)
|
||||
self.assertIn("audit_pushback_blocked",
|
||||
blocked["merge_gate"]["blocks"])
|
||||
# Fix completion on the SUBTASK id, with automated
|
||||
# evidence + a real auditor re-audit verdict, unblocks
|
||||
# the PARENT task: chain rows are attributed to the root
|
||||
# task (forked_from walk), not to the transient subtask.
|
||||
fix_body = {
|
||||
"module_id": MODULE,
|
||||
"contract_version": VERSION,
|
||||
"fix_result": {"reaudit_verdict": pass_body()["verdict"]},
|
||||
"automated": {"hard_gate": hard_body(),
|
||||
"extra_checks": EXTRA_PASS},
|
||||
}
|
||||
status, fixed = self._req(
|
||||
"POST", "/tasks/%s/audit-fix" % fix_task["id"],
|
||||
fix_body, self.owner_key)
|
||||
self.assertEqual(status, 200)
|
||||
self.assertEqual(fixed["action_kind"], "pass")
|
||||
self.assertTrue(
|
||||
fixed["automated_checks"]["automated_checks_pass"])
|
||||
# Loop state is durable and readable back.
|
||||
status, loop = self._req(
|
||||
"GET", "/tasks/%s/audit-loop?module_id=%s"
|
||||
"&contract_version=%s"
|
||||
% (task["id"], MODULE, VERSION),
|
||||
key=self.owner_key)
|
||||
self.assertEqual(status, 200)
|
||||
self.assertEqual(loop["loop_state"]["status"], "passed")
|
||||
self.assertFalse(loop["audit_pushback"]["merge_blocked"])
|
||||
# Now merge passes, and a merge 200 is never a release
|
||||
# input: release with no fresh evidence is 409.
|
||||
status, merged = self._req(
|
||||
"POST", "/tasks/%s/merge" % task["id"],
|
||||
hard_body(), self.owner_key)
|
||||
self.assertEqual(status, 200)
|
||||
self.assertEqual(merged["status"], "merged")
|
||||
status, rel = self._req(
|
||||
"POST", "/tasks/%s/release" % task["id"],
|
||||
{}, self.owner_key)
|
||||
self.assertEqual(status, 409)
|
||||
self.assertEqual(rel["release_gate"]["code"],
|
||||
"release_hard_gate_evidence_missing")
|
||||
# Release with fresh evidence passes for a global admin.
|
||||
status, rel = self._req(
|
||||
"POST", "/tasks/%s/release" % task["id"],
|
||||
hard_body(), self.owner_key)
|
||||
self.assertEqual(status, 200)
|
||||
self.assertEqual(rel["release_gate"]["code"], "release_allowed")
|
||||
|
||||
def test_release_409_writes_no_audit_and_multi_source(self):
|
||||
task = self._completed_task()
|
||||
self._req("POST", "/tasks/%s/audit-verdict" % task["id"],
|
||||
fail_body(), self.owner_key)
|
||||
before = self._chain_count()
|
||||
# Not merged + hard gate failing + pushback blocked:
|
||||
# all three sources listed, first code is task_status.
|
||||
bad_hard = hard_body()
|
||||
bad_hard["ci_status"] = "failure"
|
||||
status, rel = self._req(
|
||||
"POST", "/tasks/%s/release" % task["id"],
|
||||
bad_hard, self.owner_key)
|
||||
self.assertEqual(status, 409)
|
||||
self.assertEqual(rel["release_gate"]["blocked_sources"],
|
||||
["task_status", "hard_gate", "audit_pushback"])
|
||||
self.assertEqual(self._chain_count(), before)
|
||||
|
||||
def test_release_requires_global_admin_write(self):
|
||||
task = self._completed_task()
|
||||
# Maintainer with merge rights cannot release: release
|
||||
# is a global-admin WRITE, not a project permission.
|
||||
status, _ = self._req(
|
||||
"POST", "/tasks/%s/release" % task["id"],
|
||||
hard_body(), self.maint_key)
|
||||
self.assertEqual(status, 403)
|
||||
# A global admin who is not a project member CAN release
|
||||
# (identity is CODEX_GLOBAL_ADMINS only, per #259).
|
||||
os.environ["CODEX_GLOBAL_ADMINS"] = self.outsider_id
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
try:
|
||||
conn.execute(
|
||||
"UPDATE tasks SET status = 'merged' WHERE id = ?",
|
||||
(task["id"],))
|
||||
conn.commit()
|
||||
finally:
|
||||
conn.close()
|
||||
status, rel = self._req(
|
||||
"POST", "/tasks/%s/release" % task["id"],
|
||||
hard_body(), self.outsider_key)
|
||||
self.assertEqual(status, 200)
|
||||
self.assertEqual(rel["release_gate"]["code"], "release_allowed")
|
||||
|
||||
def test_acceptance_pending_no_subtask_confirm_flow(self):
|
||||
task = self._completed_task()
|
||||
for _ in range(4):
|
||||
status, result = self._req(
|
||||
"POST", "/tasks/%s/audit-verdict" % task["id"],
|
||||
fail_body(source="acceptance"), self.owner_key)
|
||||
self.assertEqual(status, 200)
|
||||
self.assertEqual(result["action_kind"],
|
||||
"pending_user_confirmation")
|
||||
self.assertIsNone(result["subtask"])
|
||||
self.assertIsNone(result["subtask_task"])
|
||||
self.assertEqual(result["notification"]["party"], "user")
|
||||
# A coordinator-style call (confirmed_by != caller) is
|
||||
# refused: nobody confirms on a user's behalf.
|
||||
status, _ = self._req(
|
||||
"POST", "/tasks/%s/audit-rebuild-confirm" % task["id"],
|
||||
{"module_id": MODULE, "contract_version": VERSION,
|
||||
"user_confirmed": True, "confirmed_by": "someone-else"},
|
||||
self.owner_key)
|
||||
self.assertEqual(status, 403)
|
||||
# Missing explicit flag is refused, not defaulted.
|
||||
status, _ = self._req(
|
||||
"POST", "/tasks/%s/audit-rebuild-confirm" % task["id"],
|
||||
{"module_id": MODULE, "contract_version": VERSION,
|
||||
"confirmed_by": self.owner_id},
|
||||
self.owner_key)
|
||||
self.assertEqual(status, 400)
|
||||
# Real user confirmation creates the rebuild subtask.
|
||||
status, confirmed = self._req(
|
||||
"POST", "/tasks/%s/audit-rebuild-confirm" % task["id"],
|
||||
{"module_id": MODULE, "contract_version": VERSION,
|
||||
"user_confirmed": True,
|
||||
"confirmed_by": self.owner_id,
|
||||
"old_impl_ref": {"archive_ref": "task:%s:pre-rebuild"
|
||||
% task["id"]}},
|
||||
self.owner_key)
|
||||
self.assertEqual(status, 200)
|
||||
self.assertEqual(confirmed["action_kind"], "rebuild")
|
||||
self.assertEqual(confirmed["subtask"]["kind"], "rebuild")
|
||||
self.assertIsNotNone(confirmed["subtask_task"])
|
||||
|
||||
def test_exactly_three_pushback_fourth_rebuild_http(self):
|
||||
task = self._completed_task()
|
||||
for index in range(3):
|
||||
status, result = self._req(
|
||||
"POST", "/tasks/%s/audit-verdict" % task["id"],
|
||||
fail_body(), self.owner_key)
|
||||
self.assertEqual(status, 200)
|
||||
self.assertEqual(result["action_kind"], "pushback")
|
||||
body = fail_body()
|
||||
body["old_impl_ref"] = {"archive_ref": "task:%s:pre-rebuild"
|
||||
% task["id"]}
|
||||
status, result = self._req(
|
||||
"POST", "/tasks/%s/audit-verdict" % task["id"],
|
||||
body, self.owner_key)
|
||||
self.assertEqual(status, 200)
|
||||
self.assertEqual(result["action_kind"], "rebuild")
|
||||
self.assertEqual(result["loop_state"]["rebuild_no"], 1)
|
||||
|
||||
def test_correction_to_pass_still_blocks_until_fresh_pass(self):
|
||||
task = self._completed_task()
|
||||
self._req("POST", "/tasks/%s/audit-verdict" % task["id"],
|
||||
fail_body(), self.owner_key)
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
try:
|
||||
entry_id = conn.execute(
|
||||
"SELECT id FROM audit_ledger WHERE action = ?"
|
||||
" ORDER BY id ASC", ("audit_verdict",)).fetchone()[0]
|
||||
finally:
|
||||
conn.close()
|
||||
status, corrected = self._req(
|
||||
"POST", "/tasks/%s/audit-correction" % task["id"],
|
||||
{"module_id": MODULE, "contract_version": VERSION,
|
||||
"correction": {
|
||||
"original_verdict": "fail",
|
||||
"corrected_verdict": "pass",
|
||||
"reviewer": "reviewer-1",
|
||||
"original_entry_id": entry_id,
|
||||
"findings": ["misjudged"]}},
|
||||
self.owner_key)
|
||||
self.assertEqual(status, 200)
|
||||
self.assertEqual(corrected["correction_action"],
|
||||
"schedule_reaudit")
|
||||
self.assertTrue(corrected["merge_blocked"])
|
||||
status, _ = self._req(
|
||||
"POST", "/tasks/%s/merge" % task["id"],
|
||||
hard_body(), self.owner_key)
|
||||
self.assertEqual(status, 409)
|
||||
# A fresh passing verdict (not the correction) unblocks.
|
||||
status, _ = self._req(
|
||||
"POST", "/tasks/%s/audit-verdict" % task["id"],
|
||||
pass_body(), self.owner_key)
|
||||
self.assertEqual(status, 200)
|
||||
status, merged = self._req(
|
||||
"POST", "/tasks/%s/merge" % task["id"],
|
||||
hard_body(), self.owner_key)
|
||||
self.assertEqual(status, 200)
|
||||
|
||||
def test_fix_without_reaudit_is_refused_and_automation_not_pass(self):
|
||||
task = self._completed_task()
|
||||
status, first = self._req(
|
||||
"POST", "/tasks/%s/audit-verdict" % task["id"],
|
||||
fail_body(), self.owner_key)
|
||||
self.assertEqual(status, 200)
|
||||
fix_task = first["subtask_task"]
|
||||
# No reaudit verdict at all: 400, loop still blocked.
|
||||
status, _ = self._req(
|
||||
"POST", "/tasks/%s/audit-fix" % fix_task["id"],
|
||||
{"module_id": MODULE, "contract_version": VERSION,
|
||||
"fix_result": {"fix_diff": {"summary": "changed"}}},
|
||||
self.owner_key)
|
||||
self.assertEqual(status, 400)
|
||||
# Automated checks all pass but the auditor verdict is a
|
||||
# fail: the loop stays blocked (automation != audit).
|
||||
status, fixed = self._req(
|
||||
"POST", "/tasks/%s/audit-fix" % fix_task["id"],
|
||||
{"module_id": MODULE, "contract_version": VERSION,
|
||||
"fix_result": {"reaudit_verdict":
|
||||
fail_body()["verdict"]},
|
||||
"automated": {"hard_gate": hard_body(),
|
||||
"extra_checks": EXTRA_PASS}},
|
||||
self.owner_key)
|
||||
self.assertEqual(status, 200)
|
||||
self.assertEqual(fixed["action_kind"], "pushback")
|
||||
self.assertTrue(fixed["merge_blocked"])
|
||||
|
||||
def test_quota_zero_blocks_without_consuming_round(self):
|
||||
task = self._completed_task()
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
try:
|
||||
conn.execute(
|
||||
"UPDATE user_quotas SET quota_usd_micros = 0"
|
||||
" WHERE user_id = ?", (self.owner_id,))
|
||||
conn.commit()
|
||||
finally:
|
||||
conn.close()
|
||||
status, result = self._req(
|
||||
"POST", "/tasks/%s/audit-verdict" % task["id"],
|
||||
fail_body(), self.owner_key)
|
||||
self.assertEqual(status, 200)
|
||||
self.assertEqual(result["action_kind"], "quota_blocked")
|
||||
# No loop state was saved: nothing was consumed.
|
||||
status, _ = self._req(
|
||||
"GET", "/tasks/%s/audit-loop?module_id=%s"
|
||||
"&contract_version=%s"
|
||||
% (task["id"], MODULE, VERSION),
|
||||
key=self.owner_key)
|
||||
self.assertEqual(status, 404)
|
||||
|
||||
def test_verdict_forbidden_for_contributor(self):
|
||||
task = self._completed_task()
|
||||
status, _ = self._req(
|
||||
"POST", "/tasks/%s/audit-verdict" % task["id"],
|
||||
fail_body(), self.contrib_key)
|
||||
self.assertEqual(status, 403)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user