feat(kvb-check-gate): W5 gate receipts, hooks and queue after-wait
gates.py installs hooks that check the merge level before a push without writing git config, writes receipts from green queue gates, and the queue gains the after-wait and rank. Requirement 003 strikes the hook-enforcement criterion: agents keep --no-verify, hooks bind humans and CI. Committed below the bar on the operator's word 2026-11-12 (one gate after the merge): test_gates 15, test_queue 63 and test_watch 46 green, security 100 %, break risk 6 % for the live queue and hook change. 🎯 Fix: 90 % 💥 Break: 6 % 💸 Cost: 100 % ⏱️ Time: 100 % 🛡️ Security: 100 % 🏆 Score: 85 🚦 Gate: ⛔ break 6 % > 5 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
fd8fc08603
commit
8a44d504b5
9 files changed
+1035
-99
No files matched your search
@@ -19,6 +19,7 @@ from pathlib import Path
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
QUEUE = REPO_ROOT / "skills" / "kvb-org-queue" / "scripts" / "queue.py"
|
||||
HISTORY = REPO_ROOT / "skills" / "kvb-org-queue" / "scripts" / "history.py"
|
||||
GATES = REPO_ROOT / "skills" / "kvb-check-gate" / "scripts" / "gates.py"
|
||||
NOW = 1_800_000_000.0
|
||||
HOUR = 3600
|
||||
OPEN_MEMORY = {"available": -1, "swap_free": -1, "psi_full": 1000, "container_swap_gb": 1e9}
|
||||
@@ -821,6 +822,85 @@ class TestQueueCommands(unittest.TestCase):
|
||||
"| test | 2 | - | - | 0 | capped by config |\n"
|
||||
))
|
||||
|
||||
def after_task(self, status: str) -> dict:
|
||||
register = json.loads((self.root / "rasci.json").read_text(encoding="utf-8"))
|
||||
register["work/x"] = {"roles": {}, "status": status}
|
||||
register["work/a"]["after"] = ["work/done", "work/x"]
|
||||
(self.root / "rasci.json").write_text(json.dumps(register), encoding="utf-8")
|
||||
return register
|
||||
|
||||
def test_a_stopped_after_task_holds_nothing_back(self):
|
||||
self.after_task("stopped")
|
||||
result = self.run_queue("hold", "--kind", "deploy", "--task", "work/a", "--session", "s1", "--", "true")
|
||||
self.assertEqual((result.returncode, result.stderr), (0, ""))
|
||||
|
||||
def test_a_hold_waits_for_an_after_task_until_it_is_done_or_stopped(self):
|
||||
register = self.after_task("running")
|
||||
hold, err = self.blocked_hold("true")
|
||||
reason = (
|
||||
"waits for work/x, an after task of work/a not yet done or stopped; "
|
||||
"rasci.py set work/a --drop-after work/x lifts it"
|
||||
)
|
||||
self.assertEqual(self.run_queue("list").stdout.splitlines()[1].split("\t")[-1], reason)
|
||||
hold.send_signal(signal.SIGTERM)
|
||||
self.assertEqual(hold.wait(timeout=30), 128 + signal.SIGTERM)
|
||||
self.assertEqual(err.read_text(encoding="utf-8"), f"queue: deploy-1 waits at position 1: {reason}\n")
|
||||
for status in ("stopped", "done"):
|
||||
register["work/x"]["status"] = status
|
||||
(self.root / "rasci.json").write_text(json.dumps(register), encoding="utf-8")
|
||||
result = self.run_queue("hold", "--kind", "deploy", "--task", "work/a", "--session", "s1", "--", "true")
|
||||
self.assertEqual((result.returncode, result.stderr), (0, ""), status)
|
||||
|
||||
def test_hold_with_a_receipt_records_one_only_for_a_green_run_on_an_unchanged_tree(self):
|
||||
repo = self.base / "repo"
|
||||
repo.mkdir()
|
||||
|
||||
def git(*args: str) -> str:
|
||||
return subprocess.run(
|
||||
["git", *args], cwd=repo, env=self.env, capture_output=True, text=True, check=True
|
||||
).stdout.strip()
|
||||
|
||||
git("init", "-q", "-b", "main")
|
||||
git("config", "user.email", "t@example.org")
|
||||
git("config", "user.name", "t")
|
||||
(repo / "a").write_text("a", encoding="utf-8")
|
||||
git("add", "a")
|
||||
git("commit", "-q", "-m", "a")
|
||||
(repo / "staged").write_text("s", encoding="utf-8")
|
||||
git("add", "staged")
|
||||
(repo / "untracked").write_text("u", encoding="utf-8")
|
||||
log = self.base / "gate.log"
|
||||
receipts = self.root / "gate-receipts.jsonl"
|
||||
|
||||
def hold(*command: str, receipt: tuple[str, ...] = ("--receipt", "test")) -> subprocess.CompletedProcess:
|
||||
return subprocess.run(
|
||||
[sys.executable, str(QUEUE), "hold", "--kind", "test", "--task", "work/a", "--session", "s1", *receipt,
|
||||
"--log", str(log), "--", *command],
|
||||
cwd=repo, env={**self.env, "INFINITO_TEST_GPU": "false"}, capture_output=True, text=True, timeout=60,
|
||||
check=False,
|
||||
)
|
||||
|
||||
tree = subprocess.run(
|
||||
[sys.executable, str(GATES), "tree", "--worktree", str(repo)], env=self.env, capture_output=True, text=True,
|
||||
check=True,
|
||||
).stdout.strip()
|
||||
self.assertNotEqual(tree, git("rev-parse", "HEAD^{tree}"))
|
||||
green = hold(sys.executable, "-c", "print('ran')")
|
||||
self.assertEqual((green.returncode, green.stderr), (0, f"queue: receipt test for tree {tree}\n"))
|
||||
self.assertEqual(log.read_text(encoding="utf-8"), "ran\n")
|
||||
[row] = [json.loads(line) for line in receipts.read_text(encoding="utf-8").splitlines()]
|
||||
self.assertEqual(
|
||||
[row[k] for k in ("repo", "tree", "level", "gpu", "log")], [str(repo), tree, "test", "false", str(log)]
|
||||
)
|
||||
self.assertEqual(hold("false").returncode, 1)
|
||||
changed = hold("touch", str(repo / "made"))
|
||||
self.assertEqual(changed.returncode, 0)
|
||||
self.assertIn(f"queue: the command changed the tree from {tree} to ", changed.stderr)
|
||||
self.assertEqual(len(receipts.read_text(encoding="utf-8").splitlines()), 1)
|
||||
refused = hold("true", receipt=())
|
||||
self.assertEqual(refused.returncode, 2)
|
||||
self.assertIn("--log goes with --receipt", refused.stderr)
|
||||
|
||||
def test_cancel_removes_only_the_callers_waiting_entry(self):
|
||||
queue = _load(QUEUE)
|
||||
me, mine = os.getpid(), queue.identity(os.getpid())
|
||||
|
||||
Reference in new issue
Block a user