Track 3 decisions (audit_pushback) now really block the server merge
endpoint and really land on the track 4 hash chain, via the new
cloud/audit_wiring.py translation layer:
- POST /tasks/{id}/merge ANDs the chain pushback block
(merge_blocked/release_blocked/blocked_by=audit_pushback/round/
failed_checks, replayed per module+contract version) with the
hard gate's five checks in every gate mode; either side blocks
-> 409, status unchanged, audit_pushback_blocked appended to
merge_gate.blocks. Ledger reads that fail (other than a missing
audit_ledger table) fail closed to 409.
- Every pushback/fix/re-audit/rebuild/escalation step is written
to the ledger with track 3 fields authoritative and track 4
storage authoritative; CONDITIONAL maps to fail at the boundary
with the original preserved, history never rewritten.
- Hard-gate 409 blocks codes feed pushback packages machine-first
(branch protection survives as its own extra check).
- Re-audit QA guard: a claimed pass with un-rerun or failed QA
links never reaches the chain.
- Track 4 compat: one audit_wiring_event action for escalation /
blocked-quota / audit-sample kinds, counted in the timeline.
- Resolved tension (documented in audit_wiring.py): a meta-review
correction to fail re-blocks at once, but a correction to pass
does NOT unblock by itself -- track 3 schedules a re-audit,
it does not release the gate; track 4 failure counts still use
effective verdicts, so the two views may disagree while a
re-audit is pending.
Tests: cloud/tests/test_audit_wiring.py (18 items), incl. a real
HTTP end-to-end chain (fail -> 409 -> fix -> re-audit pass ->
200) and a separate QA-link chain test.
628 lines
28 KiB
Python
628 lines
28 KiB
Python
"""Audit-gap-1 wiring: track 3 (pushback) <-> track 1 (hard gate)
|
|
<-> track 4 (hash-chain ledger), through cloud/audit_wiring.py.
|
|
|
|
What is pinned here (and nowhere else):
|
|
|
|
- CONDITIONAL maps to fail at the wiring boundary, original kept.
|
|
- Hard-gate 409 blocks codes become pushback-package failed
|
|
checks (branch protection travels as its own extra, never
|
|
dropped, never disguised as one of the ten gate checks).
|
|
- Every pushback / fix / re-audit / rebuild / escalation step
|
|
lands on the hash chain, and the chain still verifies.
|
|
- The server ANDs the chain's pushback block with the hard
|
|
gate's five checks: audit fail -> merge 409, status
|
|
unchanged, chain intact -> fix -> re-audit pass -> merge 200.
|
|
This chain runs against the real HTTP endpoint, not just
|
|
pure functions.
|
|
- The QA full chain is covered separately: a package carrying
|
|
QA failed links cannot be re-audited to a pass until exactly
|
|
those links have been re-run passing.
|
|
- The resolved meta-review tension: a correction to fail
|
|
re-blocks at once, but a correction to pass does NOT unblock
|
|
by itself (track 3: schedule a re-audit, not release the
|
|
gate); only a fresh passing verdict / re-audit unblocks.
|
|
Track 4's failure counts still use effective verdicts, so
|
|
the two views may disagree while a re-audit is pending --
|
|
that disagreement is asserted, not papered over.
|
|
"""
|
|
|
|
import http.client
|
|
import json
|
|
import os
|
|
import sqlite3
|
|
import sys
|
|
import tempfile
|
|
import threading
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
try:
|
|
from cloud.audit_pushback import GATE_CHECKS
|
|
from cloud.audit_pushback import decide_pushback
|
|
from cloud.audit_wiring import gate_failed_checks
|
|
from cloud.audit_wiring import normalize_re_review_result
|
|
from cloud.audit_wiring import normalize_verdict
|
|
from cloud.audit_wiring import pushback_block_for_task
|
|
from cloud.audit_wiring import pushback_package_from_gate
|
|
from cloud.audit_wiring import record_audit_verdict
|
|
from cloud.audit_wiring import record_escalation
|
|
from cloud.audit_wiring import record_fix
|
|
from cloud.audit_wiring import record_meta_review
|
|
from cloud.audit_wiring import record_pushback
|
|
from cloud.audit_wiring import record_reaudit
|
|
from cloud.audit_wiring import record_rebuild
|
|
from cloud.audit_wiring import record_third_failure_assessment
|
|
from cloud.auth import AuthStore
|
|
from cloud.ledger import LedgerStore
|
|
from cloud.ledger import audit_timeline
|
|
from cloud.merge_gate_hard import evaluate_hard_merge_gate
|
|
from cloud.server import create_server
|
|
except ImportError:
|
|
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
|
|
from cloud.audit_pushback import GATE_CHECKS
|
|
from cloud.audit_pushback import decide_pushback
|
|
from cloud.audit_wiring import gate_failed_checks
|
|
from cloud.audit_wiring import normalize_re_review_result
|
|
from cloud.audit_wiring import normalize_verdict
|
|
from cloud.audit_wiring import pushback_block_for_task
|
|
from cloud.audit_wiring import pushback_package_from_gate
|
|
from cloud.audit_wiring import record_audit_verdict
|
|
from cloud.audit_wiring import record_escalation
|
|
from cloud.audit_wiring import record_fix
|
|
from cloud.audit_wiring import record_meta_review
|
|
from cloud.audit_wiring import record_pushback
|
|
from cloud.audit_wiring import record_reaudit
|
|
from cloud.audit_wiring import record_rebuild
|
|
from cloud.audit_wiring import record_third_failure_assessment
|
|
from cloud.auth import AuthStore
|
|
from cloud.ledger import LedgerStore
|
|
from cloud.ledger import audit_timeline
|
|
from cloud.merge_gate_hard import evaluate_hard_merge_gate
|
|
from cloud.server import create_server
|
|
|
|
MODULE = "apps/web"
|
|
CONTRACT = "v2"
|
|
|
|
|
|
def _failing_gate():
|
|
return evaluate_hard_merge_gate(
|
|
files_changed=["apps/web/a.py"], owned_dir="apps/web",
|
|
protection="protected", ci_status="pending",
|
|
ci_checks=[{"name": "ci", "status": "pending"}],
|
|
required_checks=["ci"], contract_version="v2",
|
|
contract_frozen=True, coordinator_approved=True,
|
|
unresolved_review_comments=0)
|
|
|
|
|
|
def _full_gate():
|
|
return evaluate_hard_merge_gate(
|
|
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="v2",
|
|
contract_frozen=True, coordinator_approved=True,
|
|
unresolved_review_comments=0)
|
|
|
|
|
|
def _failing_audit():
|
|
checks = {name: True for name in GATE_CHECKS}
|
|
checks["ci_green"] = False
|
|
return {
|
|
"task_id": "task-1",
|
|
"verdict": "fail",
|
|
"verdict_text": "audit fail: ci not green",
|
|
"signer": "auditor-a",
|
|
"executor": "executor-e",
|
|
"coordinator_approver": "coordinator-c",
|
|
"qa_tester": "qa-q",
|
|
"security_verdict": "pass",
|
|
"checks": checks,
|
|
"failed_items": [],
|
|
"deviations": [],
|
|
"regression": {"items": [
|
|
{"path": "apps/web/app.py", "sha256_manifest": "a" * 64,
|
|
"sha256_recomputed": "a" * 64, "source": "exact"},
|
|
]},
|
|
"demo": {"verified_runnable": True, "runs_app_entry": True,
|
|
"command": "python3 apps/web/app.py"},
|
|
}
|
|
|
|
|
|
class VerdictNormalizationTestCase(unittest.TestCase):
|
|
def test_conditional_maps_to_fail_and_keeps_original(self):
|
|
normalized, original = normalize_verdict("conditional")
|
|
self.assertEqual(normalized, "fail")
|
|
self.assertEqual(original, "conditional")
|
|
normalized, original = normalize_verdict("pass")
|
|
self.assertEqual((normalized, original), ("pass", "pass"))
|
|
normalized, original = normalize_verdict(None)
|
|
self.assertEqual(normalized, "fail")
|
|
self.assertIsNone(original)
|
|
|
|
def test_re_review_conditional_maps_to_fail(self):
|
|
result = normalize_re_review_result({"verdict": "conditional"})
|
|
self.assertEqual(result["verdict"], "fail")
|
|
self.assertEqual(result["original_verdict"], "conditional")
|
|
result = normalize_re_review_result({"verdict": "pass"})
|
|
self.assertEqual(result["verdict"], "pass")
|
|
self.assertNotIn("original_verdict", result)
|
|
|
|
|
|
class GatePackageTestCase(unittest.TestCase):
|
|
def test_all_block_families_map_and_branch_protection_survives(self):
|
|
gate = evaluate_hard_merge_gate(
|
|
files_changed=["apps/other/x.py"], owned_dir="apps/web",
|
|
protection="unknown", ci_status="pending",
|
|
ci_checks=[{"name": "ci", "status": "failure"}],
|
|
required_checks=["ci"], contract_version=None,
|
|
contract_frozen=False, coordinator_approved=None,
|
|
unresolved_review_comments=["c1"])
|
|
self.assertFalse(gate["allowed"])
|
|
checks, extras = gate_failed_checks(gate)
|
|
for name in ("ci_green", "directory_isolation",
|
|
"contract_version", "coordinator_approval",
|
|
"unresolved_review_comments"):
|
|
self.assertIn(name, checks)
|
|
self.assertIn("branch_protection", extras)
|
|
package = pushback_package_from_gate(gate, "task-1", 0)
|
|
self.assertIn("branch_protection", package["failed_checks"])
|
|
self.assertIn("ci_green", package["failed_checks"])
|
|
# Full re-audit criteria, not failed-items-only.
|
|
self.assertEqual(package["reaudit_criteria"],
|
|
list(GATE_CHECKS))
|
|
# No block code is lost in translation.
|
|
self.assertTrue(package["failed_items"])
|
|
|
|
def test_qa_links_carried_onto_package(self):
|
|
links = [{"link": "checkout-flow", "case": "pay",
|
|
"failure": "500", "evidence": "log",
|
|
"reproduction": ["run checkout"]}]
|
|
package = pushback_package_from_gate(
|
|
_failing_gate(), "task-1", 0, qa_failed_links=links)
|
|
self.assertEqual(package["qa_failed_links"], links)
|
|
self.assertEqual(package["reaudit_must_rerun_links"], links)
|
|
|
|
|
|
class LedgerRecordingTestCase(unittest.TestCase):
|
|
def setUp(self):
|
|
self.tmp = tempfile.TemporaryDirectory()
|
|
self.db_path = os.path.join(self.tmp.name, "control.db")
|
|
self.store = LedgerStore(self.db_path)
|
|
self.task = "task-1"
|
|
self.project = "proj-1"
|
|
|
|
def tearDown(self):
|
|
try:
|
|
self.store.close()
|
|
except Exception:
|
|
pass
|
|
self.tmp.cleanup()
|
|
|
|
def _block(self):
|
|
conn = sqlite3.connect(self.db_path)
|
|
try:
|
|
return pushback_block_for_task(conn, self.task)
|
|
finally:
|
|
conn.close()
|
|
|
|
def _chain_ok(self):
|
|
result = self.store.verify()
|
|
self.assertTrue(result["ok"], result)
|
|
return result
|
|
|
|
def test_conditional_verdict_stored_as_fail_with_original(self):
|
|
entry = record_audit_verdict(
|
|
self.store, self.task, self.project, MODULE, CONTRACT,
|
|
"conditional", "audit", failures=["ci_green"])
|
|
timeline = audit_timeline(self.store.conn, task_id=self.task)
|
|
verdict = [e for e in timeline["entries"]
|
|
if e["action"] == "audit_verdict"][0]
|
|
self.assertEqual(verdict["parsed"]["verdict"], "fail")
|
|
self.assertEqual(verdict["parsed"]["extra"]["original_verdict"],
|
|
"conditional")
|
|
self.assertEqual(entry["id"], verdict["id"])
|
|
self._chain_ok()
|
|
|
|
def test_pushback_fix_reaudit_cycle_moves_gate(self):
|
|
record_audit_verdict(self.store, self.task, self.project,
|
|
MODULE, CONTRACT, "fail", "audit",
|
|
failures=["ci_green"])
|
|
package = pushback_package_from_gate(_failing_gate(),
|
|
self.task, 0)
|
|
record_pushback(self.store, self.task, self.project, MODULE,
|
|
CONTRACT, 1, package["failed_checks"], package,
|
|
failure_source="audit")
|
|
block = self._block()
|
|
self.assertTrue(block["merge_blocked"])
|
|
self.assertTrue(block["release_blocked"])
|
|
self.assertEqual(block["blocked_by"], "audit_pushback")
|
|
self.assertEqual(block["round"], 1)
|
|
self.assertIn("ci_green", block["failed_checks"])
|
|
# A landed fix alone does not unblock.
|
|
record_fix(self.store, self.task, self.project, MODULE,
|
|
CONTRACT, 1,
|
|
{"summary": "fix ci", "diff_sha256": "b" * 64,
|
|
"files_changed": ["apps/web/a.py"]}, package)
|
|
self.assertTrue(self._block()["merge_blocked"])
|
|
# A passing re-audit unblocks; chain still verifies.
|
|
record_reaudit(self.store, self.task, self.project, MODULE,
|
|
CONTRACT, 1, {"verdict": "pass"},
|
|
package=package)
|
|
self.assertFalse(self._block()["merge_blocked"])
|
|
self._chain_ok()
|
|
|
|
def test_qa_guard_refuses_unrerun_or_failed_link_pass(self):
|
|
links = [{"link": "checkout-flow", "case": "pay",
|
|
"failure": "500", "evidence": "log",
|
|
"reproduction": ["run checkout"]}]
|
|
package = pushback_package_from_gate(
|
|
_failing_gate(), self.task, 0, qa_failed_links=links)
|
|
record_audit_verdict(self.store, self.task, self.project,
|
|
MODULE, CONTRACT, "fail", "audit",
|
|
failures=["ci_green"])
|
|
record_pushback(self.store, self.task, self.project, MODULE,
|
|
CONTRACT, 1, package["failed_checks"], package,
|
|
failure_source="audit")
|
|
before = self.store.verify()["count"]
|
|
with self.assertRaises(ValueError):
|
|
record_reaudit(self.store, self.task, self.project,
|
|
MODULE, CONTRACT, 1, {"verdict": "pass"},
|
|
package=package, rerun_results={})
|
|
with self.assertRaises(ValueError):
|
|
record_reaudit(self.store, self.task, self.project,
|
|
MODULE, CONTRACT, 1, {"verdict": "pass"},
|
|
package=package,
|
|
rerun_results={"checkout-flow": False})
|
|
self.assertEqual(self.store.verify()["count"], before)
|
|
self.assertTrue(self._block()["merge_blocked"])
|
|
record_reaudit(self.store, self.task, self.project, MODULE,
|
|
CONTRACT, 1, {"verdict": "pass"},
|
|
package=package,
|
|
rerun_results={"checkout-flow": True})
|
|
self.assertFalse(self._block()["merge_blocked"])
|
|
|
|
def test_rebuild_alone_does_not_unblock(self):
|
|
record_audit_verdict(self.store, self.task, self.project,
|
|
MODULE, CONTRACT, "fail", "audit",
|
|
failures=["ci_green"])
|
|
record_rebuild(self.store, self.task, self.project, MODULE,
|
|
CONTRACT, 1, {"commit": "old123"},
|
|
root_cause="implementation", status="decided")
|
|
self.assertTrue(self._block()["merge_blocked"])
|
|
record_rebuild(self.store, self.task, self.project, MODULE,
|
|
CONTRACT, 1, {"commit": "old123"},
|
|
new_impl_ref={"commit": "new456"},
|
|
root_cause="implementation", status="rebuilt",
|
|
retained_assets={"old_tests_status": "pass"})
|
|
self.assertTrue(self._block()["merge_blocked"])
|
|
record_audit_verdict(self.store, self.task, self.project,
|
|
MODULE, CONTRACT, "pass", "audit")
|
|
self.assertFalse(self._block()["merge_blocked"])
|
|
self._chain_ok()
|
|
|
|
def test_escalation_blocks_sample_does_not(self):
|
|
record_escalation(self.store, self.task, self.project,
|
|
MODULE, CONTRACT, "blocked_quota",
|
|
target="coordinator",
|
|
reason="fix exceeds quota", round_no=2,
|
|
failed_checks=["ci_green"])
|
|
self.assertTrue(self._block()["merge_blocked"])
|
|
record_escalation(self.store, self.task, self.project,
|
|
MODULE, CONTRACT, "audit_sample",
|
|
reason="every 10th record")
|
|
self.assertTrue(self._block()["merge_blocked"])
|
|
record_audit_verdict(self.store, self.task, self.project,
|
|
MODULE, CONTRACT, "pass", "audit")
|
|
self.assertFalse(self._block()["merge_blocked"])
|
|
|
|
def test_meta_review_tension_is_pinned(self):
|
|
fail_entry = record_audit_verdict(
|
|
self.store, self.task, self.project, MODULE, CONTRACT,
|
|
"fail", "audit", failures=["ci_green"])
|
|
self.assertTrue(self._block()["merge_blocked"])
|
|
# Wrong block corrected to pass: gate STAYS blocked until
|
|
# a fresh passing re-audit (track 3 restored action).
|
|
record_meta_review(self.store, self.task, self.project,
|
|
MODULE, CONTRACT, fail_entry["id"],
|
|
"fail", "pass", True, "reviewer-r",
|
|
findings=["wrong call"],
|
|
triggered_process="re-review")
|
|
self.assertTrue(self._block()["merge_blocked"])
|
|
# ...while track 4 counts use the effective verdict.
|
|
counts = self.store.module_failure_counts(MODULE, CONTRACT,
|
|
task_id=self.task)
|
|
self.assertEqual(counts["total_fails"], 0)
|
|
record_audit_verdict(self.store, self.task, self.project,
|
|
MODULE, CONTRACT, "pass", "audit")
|
|
self.assertFalse(self._block()["merge_blocked"])
|
|
# Wrong pass corrected to fail: re-blocks at once.
|
|
pass_entry = record_audit_verdict(
|
|
self.store, self.task, self.project, MODULE, CONTRACT,
|
|
"pass", "audit")
|
|
self.assertFalse(self._block()["merge_blocked"])
|
|
record_meta_review(self.store, self.task, self.project,
|
|
MODULE, CONTRACT, pass_entry["id"],
|
|
"pass", "fail", True, "reviewer-r",
|
|
findings=["missed ci"],
|
|
triggered_process="re-review")
|
|
self.assertTrue(self._block()["merge_blocked"])
|
|
self._chain_ok()
|
|
|
|
def test_modules_are_scoped_not_global(self):
|
|
record_audit_verdict(self.store, self.task, self.project,
|
|
"apps/a", CONTRACT, "fail", "audit",
|
|
failures=["ci_green"])
|
|
record_audit_verdict(self.store, self.task, self.project,
|
|
"apps/b", CONTRACT, "pass", "audit")
|
|
self.assertTrue(self._block()["merge_blocked"])
|
|
record_audit_verdict(self.store, self.task, self.project,
|
|
"apps/a", CONTRACT, "pass", "audit")
|
|
self.assertFalse(self._block()["merge_blocked"])
|
|
|
|
def test_triage_and_root_cause_mapping(self):
|
|
record_third_failure_assessment(
|
|
self.store, self.task, self.project, MODULE, CONTRACT,
|
|
"targeted_fix",
|
|
{"audit_conclusion": "nearly done",
|
|
"failure_items": ["ci_green"],
|
|
"passed_items": ["directory_isolation"]})
|
|
timeline = audit_timeline(self.store.conn, task_id=self.task)
|
|
assessment = [e for e in timeline["entries"]
|
|
if e["action"] == "audit_third_failure_assessment"][0]
|
|
self.assertEqual(assessment["parsed"]["determination"],
|
|
"continuation_round")
|
|
with self.assertRaises(ValueError):
|
|
record_third_failure_assessment(
|
|
self.store, self.task, self.project, MODULE, CONTRACT,
|
|
"early_refreeze", {"audit_conclusion": "contract"})
|
|
with self.assertRaises(ValueError):
|
|
record_rebuild(self.store, self.task, self.project,
|
|
MODULE, CONTRACT, 1, {"commit": "old"},
|
|
root_cause="unknown")
|
|
# Design root cause stores as contract_design.
|
|
record_rebuild(self.store, self.task, self.project, MODULE,
|
|
CONTRACT, 1, {"commit": "old"},
|
|
root_cause="design", status="decided")
|
|
timeline = audit_timeline(self.store.conn, task_id=self.task)
|
|
rebuild = [e for e in timeline["entries"]
|
|
if e["action"] == "audit_rebuild"][0]
|
|
self.assertEqual(rebuild["parsed"]["root_cause"],
|
|
"contract_design")
|
|
self._chain_ok()
|
|
|
|
def test_missing_ledger_table_is_unblocked_not_error(self):
|
|
fresh = os.path.join(self.tmp.name, "empty.db")
|
|
conn = sqlite3.connect(fresh)
|
|
try:
|
|
block = pushback_block_for_task(conn, "nope")
|
|
finally:
|
|
conn.close()
|
|
self.assertFalse(block["merge_blocked"])
|
|
self.assertIsNone(block["blocked_by"])
|
|
|
|
def test_decide_pushback_round_feeds_ledger_round(self):
|
|
decision = decide_pushback(_failing_audit(), 0)
|
|
self.assertEqual(decision["action"], "pushback")
|
|
self.assertEqual(decision["next_round"], 1)
|
|
record_audit_verdict(self.store, self.task, self.project,
|
|
MODULE, CONTRACT, "fail", "audit",
|
|
failures=decision["failed_checks"])
|
|
package = pushback_package_from_gate(
|
|
_failing_gate(), self.task, decision["round"])
|
|
record_pushback(self.store, self.task, self.project, MODULE,
|
|
CONTRACT, decision["next_round"],
|
|
package["failed_checks"], package,
|
|
failure_source="audit")
|
|
block = self._block()
|
|
self.assertTrue(block["merge_blocked"])
|
|
self.assertEqual(block["round"], 1)
|
|
|
|
|
|
class EndpointChainTestCase(unittest.TestCase):
|
|
def setUp(self):
|
|
self.tmp = tempfile.TemporaryDirectory()
|
|
self.db_path = os.path.join(self.tmp.name, "control.db")
|
|
self.auth = AuthStore(self.db_path)
|
|
self.owner_id, self.owner_key = self.auth.create_user("owner")
|
|
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._saved_env = {k: os.environ.get(k) for k in
|
|
("CLOUD_MERGE_GATE_MODE",
|
|
"CLOUD_BRANCH_PROTECTION")}
|
|
os.environ.pop("CLOUD_MERGE_GATE_MODE", None)
|
|
os.environ.pop("CLOUD_BRANCH_PROTECTION", None)
|
|
self.ledger = LedgerStore(self.db_path)
|
|
|
|
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.ledger.close()
|
|
except Exception:
|
|
pass
|
|
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 _hard_body(self, **overrides):
|
|
body = {
|
|
"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": "v2",
|
|
"contract_frozen": True,
|
|
"coordinator_approved": True,
|
|
"unresolved_review_comments": 0,
|
|
}
|
|
body.update(overrides)
|
|
return body
|
|
|
|
def _status_of(self, task_id):
|
|
_s, got = self._req("GET", "/tasks/%s" % task_id,
|
|
key=self.owner_key)
|
|
return got["status"]
|
|
|
|
def _block_task(self, task, package=None):
|
|
if package is None:
|
|
package = pushback_package_from_gate(_failing_gate(),
|
|
task["id"], 0)
|
|
record_audit_verdict(self.ledger, task["id"],
|
|
self.project["id"], MODULE, CONTRACT,
|
|
"fail", "audit",
|
|
failures=package["failed_checks"])
|
|
record_pushback(self.ledger, task["id"], self.project["id"],
|
|
MODULE, CONTRACT, 1, package["failed_checks"],
|
|
package, failure_source="audit")
|
|
return package
|
|
|
|
def test_audit_fail_blocks_merge_fix_reaudit_releases(self):
|
|
task = self._completed_task()
|
|
package = self._block_task(task)
|
|
status, blocked = self._req(
|
|
"POST", "/tasks/%s/merge" % task["id"],
|
|
self._hard_body(), self.owner_key)
|
|
self.assertEqual(status, 409, blocked)
|
|
self.assertIn("audit_pushback_blocked",
|
|
blocked["merge_gate"]["blocks"])
|
|
self.assertTrue(
|
|
blocked["merge_gate"]["audit_pushback"]["merge_blocked"])
|
|
self.assertTrue(
|
|
blocked["merge_gate"]["audit_pushback"]["release_blocked"])
|
|
self.assertEqual(
|
|
blocked["merge_gate"]["audit_pushback"]["blocked_by"],
|
|
"audit_pushback")
|
|
self.assertEqual(self._status_of(task["id"]), "completed")
|
|
# Fix + passing re-audit (no QA links on this package).
|
|
record_fix(self.ledger, task["id"], self.project["id"],
|
|
MODULE, CONTRACT, 1,
|
|
{"summary": "fix ci", "diff_sha256": "b" * 64,
|
|
"files_changed": ["apps/web/a.py"]}, package)
|
|
status, still = self._req(
|
|
"POST", "/tasks/%s/merge" % task["id"],
|
|
self._hard_body(), self.owner_key)
|
|
self.assertEqual(status, 409)
|
|
record_reaudit(self.ledger, task["id"], self.project["id"],
|
|
MODULE, CONTRACT, 1, {"verdict": "pass"},
|
|
package=package)
|
|
status, merged = self._req(
|
|
"POST", "/tasks/%s/merge" % task["id"],
|
|
self._hard_body(), self.owner_key)
|
|
self.assertEqual(status, 200, merged)
|
|
self.assertEqual(self._status_of(task["id"]), "merged")
|
|
self.assertFalse(
|
|
merged["merge_gate"]["audit_pushback"]["merge_blocked"])
|
|
chain = self.ledger.verify()
|
|
self.assertTrue(chain["ok"], chain)
|
|
|
|
def test_qa_failed_links_gate_the_release(self):
|
|
task = self._completed_task()
|
|
links = [{"link": "checkout-flow", "case": "pay",
|
|
"failure": "500", "evidence": "log",
|
|
"reproduction": ["run checkout"]}]
|
|
package = pushback_package_from_gate(
|
|
_failing_gate(), task["id"], 0, qa_failed_links=links)
|
|
self._block_task(task, package=package)
|
|
status, _blocked = self._req(
|
|
"POST", "/tasks/%s/merge" % task["id"],
|
|
self._hard_body(), self.owner_key)
|
|
self.assertEqual(status, 409)
|
|
# Claimed pass without a passing re-run of the failed
|
|
# link must not reach the chain, and must not unblock.
|
|
with self.assertRaises(ValueError):
|
|
record_reaudit(self.ledger, task["id"],
|
|
self.project["id"], MODULE, CONTRACT, 1,
|
|
{"verdict": "pass"}, package=package,
|
|
rerun_results={"checkout-flow": False})
|
|
status, _still = self._req(
|
|
"POST", "/tasks/%s/merge" % task["id"],
|
|
self._hard_body(), self.owner_key)
|
|
self.assertEqual(status, 409)
|
|
record_reaudit(self.ledger, task["id"], self.project["id"],
|
|
MODULE, CONTRACT, 1, {"verdict": "pass"},
|
|
package=package,
|
|
rerun_results={"checkout-flow": True})
|
|
status, merged = self._req(
|
|
"POST", "/tasks/%s/merge" % task["id"],
|
|
self._hard_body(), self.owner_key)
|
|
self.assertEqual(status, 200, merged)
|
|
|
|
def test_pushback_blocks_even_in_record_mode(self):
|
|
task = self._completed_task()
|
|
self._block_task(task)
|
|
os.environ["CLOUD_MERGE_GATE_MODE"] = "record"
|
|
status, blocked = self._req(
|
|
"POST", "/tasks/%s/merge" % task["id"],
|
|
self._hard_body(), self.owner_key)
|
|
self.assertEqual(status, 409, blocked)
|
|
self.assertIn("audit_pushback_blocked",
|
|
blocked["merge_gate"]["blocks"])
|
|
self.assertEqual(self._status_of(task["id"]), "completed")
|
|
|
|
def test_hard_gate_failure_without_pushback_has_no_pushback_code(self):
|
|
task = self._completed_task()
|
|
status, blocked = self._req(
|
|
"POST", "/tasks/%s/merge" % task["id"],
|
|
self._hard_body(ci_status="pending"), self.owner_key)
|
|
self.assertEqual(status, 409, blocked)
|
|
self.assertIn("ci_not_green", blocked["merge_gate"]["blocks"])
|
|
self.assertNotIn("audit_pushback_blocked",
|
|
blocked["merge_gate"]["blocks"])
|
|
self.assertFalse(
|
|
blocked["merge_gate"]["audit_pushback"]["merge_blocked"])
|
|
self.assertEqual(self._status_of(task["id"]), "completed")
|