"""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, # A3: signer is the HTTP caller; the other three # roles must be declared and distinct from it. "executor": "executor-1", "coordinator_approver": "coordinator-1", "qa_tester": "qa-1", }, } 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, # A3: see fail_body. "executor": "executor-1", "coordinator_approver": "coordinator-1", "qa_tester": "qa-1", }, } 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) def test_verdict_self_audit_rejected_http(self): # A3 HTTP 层:调用方(owner)自审(声明的 executor 即 # 调用方本人)回 400,且循环状态未落盘(audit-loop 404)。 task = self._completed_task() body = fail_body() body["verdict"]["executor"] = self.owner_id status, _ = self._req( "POST", "/tasks/%s/audit-verdict" % task["id"], body, self.owner_key) self.assertEqual(status, 400) 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) if __name__ == "__main__": unittest.main()