feat(org): kvb-org-radio and kvb-org-queue replace chair and peer messages

kvb-org-radio is the default channel to other agents: broadcast and direct reach the receiver on its next prompt, security and pan-pan mid-turn through a PostToolBatch hook, and only mayday or direct --wake wake a session, through the receiver's own asyncRewake Stop hook (a SendMessage fallback is printed for sessions without a listener). It keeps the news.jsonl store and lock, so news.py stays a forwarding shim. A mayday holds radio silence for two hours: watch.py blocks new heavy commands and the operator gets a desktop notice. hotfix.py announces merged hotfixes as security items.

kvb-org-queue grants heavy jobs by rule instead of chair messages: queue.py hold waits under a flock with disk reservation, hotfix-first and register rank order, slot limits, stage and silence checks, then runs the command and releases. Every job is recorded; history.py learns disk need per command and slot limits per kind (p90, five runs), watch.py mirrors the history to ~/.local/state. enable-plugins.js registers the new hooks behind a file check so a missing script never blocks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kevin Veen-BirkenbachandClaude Opus 5.5 committed 2026-11-04 17:07:24 +01:00
1 parent ed5bb074c5
commit 27559dabc4
18 files changed
+2607 -225

No files matched your search

+54 -19
View File
@@ -145,22 +145,59 @@ class TestSettingsRegistration(unittest.TestCase):
)
return json.loads(self.settings.read_text(encoding="utf-8"))
@staticmethod
def _hooks(data: dict, event: str, script: str) -> list[dict]:
return [
hook
for entry in data["hooks"].get(event, [])
for hook in entry.get("hooks", [])
if script in hook.get("command", "")
]
def test_hook_registered_once(self):
self._patch()
data = self._patch()
entries = json.dumps(data["hooks"]["UserPromptSubmit"])
self.assertEqual(entries.count("kvb-dream/hooks/reminder.sh"), 1)
self.assertEqual(entries.count("kvb-coach/hooks/reminder.sh"), 1)
self.assertNotIn("tune/hooks/reminder.sh", entries)
gate = json.dumps(data["hooks"]["PreToolUse"])
self.assertEqual(gate.count("kvb-org-organize/scripts/council.py"), 1)
self.assertEqual(gate.count("kvb-org-organize/scripts/watch.py"), 1)
self.assertEqual(entries.count("kvb-org-organize/scripts/watch.py"), 1)
self.assertEqual(entries.count("kvb-org-organize/scripts/news.py"), 1)
post = json.dumps(data["hooks"]["PostToolUse"])
self.assertEqual(post.count("kvb-org-organize/scripts/watch.py"), 1)
self.assertEqual(gate.count("kvb-org-perform/scripts/log.py"), 1)
self.assertEqual(post.count("kvb-org-perform/scripts/log.py"), 1)
expected = {
("UserPromptSubmit", "kvb-dream/hooks/reminder.sh"): 1,
("UserPromptSubmit", "kvb-coach/hooks/reminder.sh"): 1,
("UserPromptSubmit", "tune/hooks/reminder.sh"): 0,
("UserPromptSubmit", "kvb-org-organize/scripts/watch.py"): 1,
("UserPromptSubmit", "kvb-org-radio/scripts/radio.py"): 1,
("UserPromptSubmit", "kvb-org-organize/scripts/news.py"): 0,
("PostToolBatch", "kvb-org-radio/scripts/radio.py"): 1,
("Stop", "kvb-org-radio/scripts/radio.py"): 1,
("PreToolUse", "kvb-org-organize/scripts/council.py"): 1,
("PreToolUse", "kvb-org-organize/scripts/watch.py"): 1,
("PreToolUse", "kvb-org-queue/scripts/queue.py"): 1,
("PreToolUse", "kvb-org-perform/scripts/log.py"): 1,
("PostToolUse", "kvb-org-organize/scripts/watch.py"): 1,
("PostToolUse", "kvb-org-perform/scripts/log.py"): 1,
}
for (event, script), count in expected.items():
with self.subTest(event=event, script=script):
self.assertEqual(len(self._hooks(data, event, script)), count)
def test_radio_listener_wakes_asynchronously_and_hooks_survive_a_missing_script(self):
data = self._patch()
(listen,) = self._hooks(data, "Stop", "kvb-org-radio/scripts/radio.py")
self.assertIs(listen["asyncRewake"], True)
self.assertGreater(listen["timeout"], 3600)
self.assertTrue(listen["command"].endswith(" listen"))
(gate,) = self._hooks(data, "PreToolUse", "kvb-org-queue/scripts/queue.py")
result = subprocess.run(["sh", "-c", gate["command"]], capture_output=True, check=False)
self.assertEqual(result.returncode, 1)
def test_news_hook_moves_to_radio(self):
base = Path(self.tmp.name) / "skills"
self.settings.write_text(
json.dumps({"hooks": {"UserPromptSubmit": [
{"hooks": [{"type": "command", "command": f'python3 "{base}/kvb-org-organize/scripts/news.py" hook'}]}
]}}),
encoding="utf-8",
)
data = self._patch()
self.assertEqual(self._hooks(data, "UserPromptSubmit", "kvb-org-organize/scripts/news.py"), [])
self.assertEqual(len(self._hooks(data, "UserPromptSubmit", "kvb-org-radio/scripts/radio.py")), 1)
def test_code_graph_plugin_is_retired_for_the_agent_agnostic_installer(self):
self.settings.write_text(
@@ -231,12 +268,10 @@ class TestSettingsRegistration(unittest.TestCase):
for old in ("/autotune/", "/kvb-skill-tune/", "/kvb-skill-dream/", "/democracy/", "/subagent-model/"):
self.assertNotIn(f"skills{old}", text)
self.assertIn("echo keep", text)
entries = json.dumps(data["hooks"]["UserPromptSubmit"])
self.assertEqual(entries.count("kvb-dream/hooks/reminder.sh"), 1)
self.assertEqual(entries.count("kvb-org-organize/scripts/watch.py"), 1)
gate = json.dumps(data["hooks"]["PreToolUse"])
self.assertEqual(gate.count("kvb-org-organize/scripts/council.py"), 1)
self.assertEqual(gate.count("kvb-org-perform/scripts/log.py"), 1)
self.assertEqual(len(self._hooks(data, "UserPromptSubmit", "kvb-dream/hooks/reminder.sh")), 1)
self.assertEqual(len(self._hooks(data, "UserPromptSubmit", "kvb-org-organize/scripts/watch.py")), 1)
self.assertEqual(len(self._hooks(data, "PreToolUse", "kvb-org-organize/scripts/council.py")), 1)
self.assertEqual(len(self._hooks(data, "PreToolUse", "kvb-org-perform/scripts/log.py")), 1)
if __name__ == "__main__":
+38 -15
View File
@@ -1,8 +1,9 @@
"""Exercise news.py: post, the hook's once-only delivery, addressing, own posts, and a broken hook input."""
"""Exercise the news.py shim: it forwards post, hook and show to radio.py, and a missing radio.py never blocks a prompt."""
from __future__ import annotations
import json
import shutil
import subprocess
import sys
import tempfile
@@ -17,44 +18,66 @@ class TestNews(unittest.TestCase):
def setUp(self):
tmp = tempfile.TemporaryDirectory()
self.addCleanup(tmp.cleanup)
self.home = Path(tmp.name) / "home"
self.root = Path(tmp.name) / "council"
self.tmp = Path(tmp.name)
self.home = self.tmp / "home"
self.root = self.tmp / "council"
sessions = self.home / ".claude" / "sessions"
sessions.mkdir(parents=True)
for sid, name in (("s1", "One"), ("s2", "Two"), ("s3", "Three")):
(sessions / f"{sid}.json").write_text(json.dumps({"sessionId": sid, "name": name}), encoding="utf-8")
def _run(self, *args: str, sid: str | None = "s1", stdin: str = "") -> subprocess.CompletedProcess:
env = {"PATH": "/usr/bin:/bin", "HOME": str(self.home), "AGENTS_COUNCIL_ROOT": str(self.root)}
def _exec(self, news: Path, sid: str | None, stdin: str, args: tuple[str, ...]) -> subprocess.CompletedProcess:
env = {
"PATH": "/usr/bin:/bin",
"HOME": str(self.home),
"AGENTS_COUNCIL_ROOT": str(self.root),
"AGENTS_COUNCIL_CONFIG": str(self.tmp / "config.json"),
}
if sid:
env["CLAUDE_CODE_SESSION_ID"] = sid
return subprocess.run(
[sys.executable, str(NEWS), *args], input=stdin, capture_output=True, text=True, env=env, check=False
[sys.executable, str(news), *args], input=stdin, capture_output=True, text=True, env=env, check=False
)
def _cli(self, sid: str | None, *args: str) -> subprocess.CompletedProcess:
return self._exec(NEWS, sid, "", args)
def _hook(self, sid: str) -> list[str]:
result = self._run("hook", sid=None, stdin=json.dumps({"session_id": sid, "prompt": "x"}))
result = self._exec(NEWS, None, json.dumps({"session_id": sid, "prompt": "x"}), ("hook",))
self.assertEqual(result.returncode, 0, result.stderr)
return [line.split(" ", 2)[2] for line in result.stdout.splitlines()]
return [line.split(" ", 3)[3] for line in result.stdout.splitlines()]
def test_every_session_sees_a_post_once_except_its_author(self):
self.assertEqual(self._run("post", "core #1358 merged").stdout, "posted to all\n")
self.assertEqual(self._cli("s1", "post", "core #1358 merged").stdout, "sent broadcast to all\n")
self.assertEqual(self._hook("s2"), ["One: core #1358 merged"])
self.assertEqual(self._hook("s2"), [])
self.assertEqual(self._hook("s3"), ["One: core #1358 merged"])
self.assertEqual(self._hook("s1"), [])
def test_addressed_post_reaches_only_its_target(self):
self._run("post", "your gate is granted", "--to", "Two")
self._cli("s1", "post", "your gate is granted", "--to", "Two")
self.assertEqual(self._hook("s3"), [])
self.assertEqual(self._hook("s2"), ["One to Two: your gate is granted"])
self.assertEqual(len(self._run("show", "--all").stdout.splitlines()), 1)
self.assertEqual(len(self._cli("s1", "show", "--all").stdout.splitlines()), 1)
def test_hook_never_fails_and_post_needs_a_session(self):
self.assertEqual((self._run("hook", sid=None, stdin="not json").returncode), 0)
self.assertEqual(self._run("hook", sid=None, stdin="{}").stdout, "")
self.assertEqual(self._run("post", "x", sid=None).returncode, 2)
self.assertEqual(self._run("post", " ").returncode, 2)
self.assertEqual(self._exec(NEWS, None, "not json", ("hook",)).returncode, 0)
self.assertEqual(self._exec(NEWS, None, "{}", ("hook",)).stdout, "")
self.assertEqual(self._cli(None, "post", "x").returncode, 2)
self.assertEqual(self._cli("s1", "post", " ").returncode, 2)
def test_without_radio_the_hook_passes_and_the_rest_fail(self):
lone = self.tmp / "skills" / "kvb-org-organize" / "scripts" / "news.py"
lone.parent.mkdir(parents=True)
shutil.copy(NEWS, lone)
hook = self._exec(lone, None, json.dumps({"session_id": "s2"}), ("hook",))
self.assertEqual((hook.returncode, hook.stdout), (0, ""))
for args in (("post", "x"), ("show",)):
with self.subTest(args=args):
result = self._exec(lone, "s1", "", args)
self.assertEqual(result.returncode, 2)
self.assertIn("kvb-org-radio", result.stderr)
self.assertFalse(self.root.exists())
if __name__ == "__main__":
+503
View File
@@ -0,0 +1,503 @@
"""Exercise the kvb-org-queue grant order, its history learning and its hold, list, history, cancel and block commands."""
from __future__ import annotations
import importlib.util
import json
import os
import signal
import subprocess
import sys
import tempfile
import time
import unittest
from collections.abc import Callable
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"
NOW = 1_800_000_000.0
HOUR = 3600
BASE = {
"free_gb": 100.0,
"floor": 10.0,
"stage": "ok",
"silenced": False,
"slot_limits": {"test": 5, "deploy": 1},
"ranks": {"work/a": 0, "work/b": 1, "work/c": 2},
"now": NOW,
}
CHILD = (
"import json, os, sys, time\n"
"deadline = time.monotonic() + 20\n"
"while True:\n"
" with open(sys.argv[1], encoding='utf-8') as handle:\n"
" entries = json.load(handle)['entries']\n"
" if (entries and entries[0]['child'] == os.getpid()) or time.monotonic() > deadline:\n"
" break\n"
" time.sleep(0.05)\n"
"with open(sys.argv[2], 'w', encoding='utf-8') as out:\n"
" json.dump(entries, out)\n"
"sys.exit(7)\n"
)
def _load(path: Path):
spec = importlib.util.spec_from_file_location(f"{path.stem}_under_test", path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
def history_row(seq: int, **fields: object) -> dict:
return {
"id": f"r{seq}", "kind": "test", "signature": "make test", "workdir": "/w", "start": NOW - 600, "end": NOW,
"minutes": 10.0, "exit": 0, "free_before_gb": 50.0, "min_free_gb": 49.0, "disk_gb": 1.0, "mem_before": 20.0,
"mem_max": 30.0, "cpu_before": 10.0, "cpu_max": 40.0, "concurrent": 0, **fields,
}
def job(seq: int, **fields: object) -> dict:
return {
"id": f"j{seq}", "seq": seq, "kind": "test", "task": "work/a", "need_gb": 1, "hotfix": None,
"workdir": f"/w{seq}", "status": "waiting", "enqueued": NOW, **fields,
}
def stored(seq: int, status: str, pid: int, pid_start: str | None, child: int | None,
child_start: str | None) -> dict:
return {
"id": f"test-{seq}", "seq": seq, "kind": "test", "task": "work/a", "need_gb": 0, "hotfix": None,
"workdir": f"/w{seq}", "session": "s1", "name": "s1", "pid": pid, "pid_start": pid_start,
"child": child, "child_start": child_start, "status": status, "enqueued": time.time(),
"granted": time.time() if status == "active" else None, "cmd": "true",
}
class TestEvaluate(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.queue = _load(QUEUE)
def decide(self, entries: list[dict], **overrides: object) -> dict:
return self.queue.evaluate(entries, **{**BASE, **overrides})
def test_hotfix_goes_first_then_rank_then_arrival_and_unknown_tasks_last(self):
entries = [
job(1, task="work/c"), job(2, task="work/gone"), job(3), job(4), job(5, task="work/c", hotfix="fix-1"),
]
self.assertEqual(list(self.decide(entries)), ["j5", "j3", "j4", "j1", "j2"])
def test_each_whole_hour_waited_moves_one_rank_up(self):
fresh = job(1)
aged = job(2, task="work/c", enqueued=NOW - 3 * HOUR)
almost = job(3, task="work/b", enqueued=NOW - HOUR + 1)
self.assertEqual(list(self.decide([fresh, aged, almost])), ["j2", "j1", "j3"])
tied = job(4, task="work/c", enqueued=NOW - 2 * HOUR)
self.assertEqual(list(self.decide([tied, fresh])), ["j1", "j4"])
def test_the_slot_limit_counts_active_jobs_per_kind(self):
verdicts = self.decide([job(1, kind="deploy", status="active"), job(2, kind="deploy"), job(3)])
self.assertEqual(verdicts, {"j2": "all 1 deploy slots taken", "j3": None})
def test_a_higher_waiter_is_granted_virtually_before_a_lower_one(self):
verdicts = self.decide([job(1, task="work/b", kind="deploy"), job(2, kind="deploy")])
self.assertEqual(verdicts, {"j2": None, "j1": "all 1 deploy slots taken"})
def test_disk_reservation_keeps_smaller_jobs_from_starving_a_big_one(self):
big, small = job(1, need_gb=50), job(2, task="work/b", need_gb=5)
verdicts = self.decide([big, small], free_gb=40.0)
self.assertEqual(verdicts["j1"], "50 GB would cut into the 10 GB floor; reserved")
self.assertEqual(verdicts["j2"], "5 GB would cut into the 10 GB floor; reserved")
self.assertIsNone(self.decide([small], free_gb=40.0)["j2"])
running = job(3, need_gb=25, status="active")
self.assertIsNone(self.decide([running, job(4, need_gb=5)], free_gb=40.0)["j4"])
self.assertIsNotNone(self.decide([running, job(4, need_gb=6)], free_gb=40.0)["j4"])
def test_warn_admits_only_jobs_without_disk_and_stop_admits_none(self):
entries = [job(1, need_gb=0), job(2, task="work/b")]
verdicts = self.decide(entries, stage="warn")
self.assertEqual(verdicts, {"j1": None, "j2": "disk stage warn admits only jobs that need no disk"})
for stage in ("stop", "critical"):
with self.subTest(stage=stage):
self.assertEqual(set(self.decide(entries, stage=stage).values()), {f"disk stage {stage}"})
def test_radio_silence_blocks_every_job(self):
verdicts = self.decide([job(1, need_gb=0), job(2)], silenced=True)
self.assertEqual(set(verdicts.values()), {"radio silence"})
def test_a_busy_workdir_blocks_a_second_job_there(self):
verdicts = self.decide([
job(1, workdir="/w", status="active"), job(2, workdir="/w"), job(3, workdir="/x"),
job(4, task="work/b", workdir="/x"),
])
self.assertEqual(verdicts, {"j2": "/w runs another job", "j3": None, "j4": "/x runs another job"})
class TestHistory(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.history = _load(HISTORY)
def learn(self, rows: list[dict]) -> tuple[dict, dict]:
return self.history.learn(rows, NOW, {"load_limits": {"mem_percent": 80, "cpu_percent": 90}})
def test_signature_names_a_command_apart_from_paths_options_and_variables(self):
cases = {
("make", "-C", "/repo", "-j4", "VAR=1", "compose-deploy", "apps=x"): "make compose-deploy",
("/usr/bin/make", "-j", "8", "-f", "Other.mk", "test"): "make test",
("make", "-k"): "make",
("python3", "/s/kvb-check-gate/scripts/gate.py", "run", "--repo", ".", "--target", "test"): "gate.py run",
("/usr/bin/python3.14", "-u", "x.py", "--flag", "sub"): "x.py sub",
("python3", "-m", "unittest", "tests.test_queue"): "unittest tests.test_queue",
("python3", "-c", "print(1)"): "python3 -c",
("bash", "scripts/install.sh"): "install.sh",
("docker", "compose", "up", "-d"): "docker compose",
("/bin/true",): "true",
}
for argv, expected in cases.items():
with self.subTest(argv=argv):
self.assertEqual(self.history.signature(list(argv)), expected)
def test_learn_waits_for_min_runs_inside_the_30_day_window(self):
four = [history_row(n) for n in range(4)]
signatures, kinds = self.learn(four)
self.assertEqual((signatures["make test"]["runs"], signatures["make test"]["need_gb"]), (4, None))
self.assertEqual((kinds["test"]["runs"], kinds["test"]["limit"]), (4, None))
stale = history_row(4, end=NOW - self.history.WINDOW_SECONDS - 1)
self.assertIsNone(self.learn([*four, stale])[0]["make test"]["need_gb"])
signatures, kinds = self.learn([*four, history_row(4, end=NOW - self.history.WINDOW_SECONDS)])
self.assertEqual((signatures["make test"]["need_gb"], signatures["make test"]["minutes"]), (2, 10))
self.assertEqual(kinds["test"]["limit"], 2)
def test_learn_needs_the_p90_disk_plus_one_gb_and_takes_the_median_minutes(self):
runs = zip((4, 1, 10, 3, 2), (5, 9, 7, 1, 30))
stats = self.learn([history_row(n, disk_gb=gb, minutes=m) for n, (gb, m) in enumerate(runs)])[0]["make test"]
self.assertAlmostEqual(stats["disk_gb"], 7.6)
self.assertEqual((stats["need_gb"], stats["minutes"]), (9, 7))
def test_learn_fits_the_slot_limit_between_the_median_load_and_the_load_limits(self):
loads = ((10, 5), (20, 5), (20, 10), (30, 15), (40, 15))
rows = [history_row(n, mem_before=before, mem_max=before + share) for n, (before, share) in enumerate(loads)]
kind = self.learn(rows)[1]["test"]
self.assertEqual((kind["ram_share"], kind["cpu_share"], kind["limit"]), (15, 30, 2))
idle_cpu = [{**row, "cpu_max": row["cpu_before"]} for row in rows]
self.assertEqual(self.learn(idle_cpu)[1]["test"]["limit"], 4)
idle = [{**row, "mem_max": row["mem_before"]} for row in idle_cpu]
self.assertEqual(self.learn(idle)[1]["test"]["limit"], 60)
crowded = [history_row(n, mem_before=90, mem_max=95) for n in range(5)]
self.assertEqual(self.learn(crowded)[1]["test"]["limit"], 1)
def test_slot_limits_take_the_learned_limit_else_the_seed_and_a_smaller_cap(self):
kinds = {"deploy": {"limit": 3}, "build": {"limit": None}}
self.assertEqual(self.history.slot_limits(kinds, {"deploy": 2, "swarm": 0, "test": 9}, ["test", "gate"]), {
"build": (1, "seed"), "deploy": (2, "capped by config"), "gate": (1, "seed"),
"swarm": (0, "capped by config"), "test": (1, "seed"),
})
self.assertEqual(self.history.slot_limits(kinds, {"deploy": 5}, [])["deploy"], (3, "learned"))
def test_row_keeps_the_extremes_and_floors_the_disk_taken_at_zero(self):
samples = [
{"free_gb": 50, "mem": 20, "cpu": 5}, {"free_gb": 47.5, "mem": 60, "cpu": 90},
{"free_gb": 49, "mem": 30, "cpu": 1},
]
row = self.history.row({"id": "j"}, NOW, NOW + 90, 3, samples, 2)
self.assertEqual(row, {
"id": "j", "start": NOW, "end": NOW + 90, "minutes": 1.5, "exit": 3, "free_before_gb": 50,
"min_free_gb": 47.5, "disk_gb": 2.5, "mem_before": 20, "mem_max": 60, "cpu_before": 5, "cpu_max": 90,
"concurrent": 2,
})
grown = self.history.row({"id": "j"}, NOW, NOW, 0, [samples[0], {**samples[0], "free_gb": 55}], 0)
self.assertEqual(grown["disk_gb"], 0)
def test_read_merges_the_files_by_id_and_skips_broken_lines(self):
with tempfile.TemporaryDirectory() as tmp:
first, second = Path(tmp) / "a.jsonl", Path(tmp) / "b.jsonl"
first.write_text('{"id": "x", "v": 1}\n{broken\n[1]\n', encoding="utf-8")
second.write_text('{"id": "x", "v": 2}\n{"id": "y"}\n', encoding="utf-8")
rows = self.history.read([first, second, Path(tmp) / "missing.jsonl"])
self.assertEqual(rows, [{"id": "x", "v": 1}, {"id": "y"}])
def test_eta_adds_the_remaining_and_the_waiting_median_minutes(self):
signatures = self.learn([history_row(n) for n in range(5)])[0]
eta = self.history.eta
self.assertEqual(eta(signatures, [("make test", NOW - 240)], ["make test"], NOW), 16)
self.assertEqual(eta(signatures, [("make test", NOW - 900)], [], NOW), 0)
self.assertEqual(eta(signatures, [], [], NOW), 0)
self.assertIsNone(eta(signatures, [], ["make test", "true"], NOW))
class TestQueueCommands(unittest.TestCase):
def setUp(self):
tmp = tempfile.TemporaryDirectory()
self.addCleanup(tmp.cleanup)
self.base = Path(tmp.name).resolve()
self.root = self.base / "council"
self.root.mkdir()
home = self.base / "home"
home.mkdir()
self.history_file = self.root / "queue-history.jsonl"
self.kept = home / ".local" / "state" / "agents-council" / "queue-history.jsonl"
self.config = self.base / "config.json"
self.cfg = {
"disk": str(self.base),
"thresholds_gb": {"warn": 0, "stop": 0, "critical": 0},
"projection_minutes": 30,
"slot_need_gb": {"test": 0.001, "deploy": 0},
"slot_limits": {"test": 1, "deploy": 1},
"load_limits": {"cpu_percent": 101, "mem_percent": 101, "sample_seconds": 0.1, "kinds": ["test"]},
"heavy_commands": ["^make compose-deploy"],
"never_block": ["^make compose-down"],
}
self.write_config()
(self.root / "rasci.json").write_text(json.dumps({
"work/a": {"roles": {}, "status": "running"},
"work/done": {"roles": {}, "status": "done"},
"hotfix/fix-1": {"roles": {}, "status": "running", "stage": "pr"},
}), encoding="utf-8")
self.env = {
"PATH": os.environ["PATH"],
"HOME": str(home),
"GIT_CEILING_DIRECTORIES": str(self.base.parent),
"AGENTS_COUNCIL_ROOT": str(self.root),
"AGENTS_COUNCIL_CONFIG": str(self.config),
}
def write_config(self) -> None:
self.config.write_text(json.dumps(self.cfg), encoding="utf-8")
def write_store(self, entries: list[dict]) -> None:
(self.root / "queue.json").write_text(json.dumps({"seq": len(entries), "entries": entries}), encoding="utf-8")
def entries(self) -> list[dict]:
return json.loads((self.root / "queue.json").read_text(encoding="utf-8"))["entries"]
def write_history(self, path: Path, rows: list[dict]) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text("".join(json.dumps(row) + "\n" for row in rows), encoding="utf-8")
def blocked_hold(self, *command: str) -> tuple[subprocess.Popen, Path]:
err = self.base / "hold.err"
with open(err, "w", encoding="utf-8") as handle:
hold = subprocess.Popen(
[sys.executable, str(QUEUE), "hold", "--kind", "deploy", "--task", "work/a", "--session", "s1",
"--", *command],
cwd=self.base, env=self.env, stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=handle,
)
self.addCleanup(hold.wait)
self.addCleanup(hold.kill)
self.wait_for(lambda: "waits at position 1" in err.read_text(encoding="utf-8"))
return hold, err
def run_queue(self, *args: str) -> subprocess.CompletedProcess:
return subprocess.run(
[sys.executable, str(QUEUE), *args], cwd=self.base, env=self.env, stdin=subprocess.DEVNULL,
capture_output=True, text=True, timeout=60, check=False,
)
def hook(self, payload: str) -> subprocess.CompletedProcess:
return subprocess.run(
[sys.executable, str(QUEUE), "block"], cwd=self.base, env=self.env, input=payload,
capture_output=True, text=True, timeout=60, check=False,
)
def bash(self, command: str) -> int:
return self.hook(json.dumps({"tool_input": {"command": command}})).returncode
def wait_for(self, condition: Callable[[], bool]) -> None:
deadline = time.monotonic() + 30
while not condition():
self.assertLess(time.monotonic(), deadline)
time.sleep(0.05)
def test_hold_runs_the_command_on_its_grant_passes_its_exit_code_and_leaves(self):
seen = self.base / "seen.json"
result = self.run_queue(
"hold", "--kind", "test", "--task", "work/a", "--hotfix", "fix-1", "--session", "s1", "--",
sys.executable, "-c", CHILD, str(self.root / "queue.json"), str(seen),
)
self.assertEqual((result.returncode, result.stderr), (7, ""))
[entry] = json.loads(seen.read_text(encoding="utf-8"))
self.assertEqual(
[entry[k] for k in ("id", "status", "kind", "task", "need_gb", "hotfix", "workdir", "session")],
["test-1", "active", "test", "work/a", 0.001, "fix-1", str(self.base), "s1"],
)
self.assertEqual(self.entries(), [])
[row] = [json.loads(line) for line in self.history_file.read_text(encoding="utf-8").splitlines()]
self.assertEqual(
[row[k] for k in ("kind", "signature", "workdir", "exit", "concurrent")],
["test", f"{Path(sys.executable).name} -c", str(self.base), 7, 0],
)
self.assertTrue(row["id"].startswith("test-1-"))
self.assertLessEqual(row["start"], row["end"])
self.assertEqual(row["disk_gb"], round(max(0.0, row["free_before_gb"] - row["min_free_gb"]), 2))
self.assertLessEqual(row["mem_before"], row["mem_max"])
def test_hold_runs_without_slot_limits_on_the_seed_limit(self):
del self.cfg["slot_limits"]
self.write_config()
result = self.run_queue("hold", "--kind", "test", "--task", "work/a", "--session", "s1", "--", "true")
self.assertEqual((result.returncode, result.stderr), (0, ""))
def test_a_blocked_hold_reports_once_shows_in_list_and_leaves_on_sigterm(self):
self.cfg["slot_limits"]["deploy"] = 0
self.write_config()
hold, err = self.blocked_hold("true")
listing = self.run_queue("list")
self.assertEqual(listing.returncode, 0, listing.stderr)
row = ["1", "deploy-1", "waiting", "deploy", "work/a", "0", "0", "0", str(self.base), "all 0 deploy slots taken"]
self.assertEqual(listing.stdout.splitlines()[1], "\t".join(row))
hold.send_signal(signal.SIGTERM)
self.assertEqual(hold.wait(timeout=30), 128 + signal.SIGTERM)
self.assertEqual(
err.read_text(encoding="utf-8"), "queue: deploy-1 waits at position 1: all 0 deploy slots taken\n"
)
self.assertEqual(self.entries(), [])
def test_hold_refuses_bad_input_with_exit_2_and_enqueues_nothing(self):
def hold(*args: str) -> list[str]:
return ["hold", "--session", "s1", *args]
cases = (
("unknown kind", hold("--kind", "nope", "--task", "work/a", "--", "true"), "kind 'nope'"),
("unknown task", hold("--kind", "test", "--task", "work/zzz", "--", "true"), "no open task"),
("ended task", hold("--kind", "test", "--task", "work/done", "--", "true"), "no open task"),
("missing hotfix row", hold("--kind", "test", "--task", "work/a", "--hotfix", "nope", "--", "true"),
"hotfix/nope"),
("negative need", hold("--kind", "test", "--task", "work/a", "--need-gb", "-1", "--", "true"),
"--need-gb"),
("no command", hold("--kind", "test", "--task", "work/a"), "required"),
)
for name, args, message in cases:
with self.subTest(name):
result = self.run_queue(*args)
self.assertEqual(result.returncode, 2, result.stderr)
self.assertIn(message, result.stderr)
self.config.unlink()
missing_config = self.run_queue(*hold("--kind", "test", "--task", "work/a", "--", "true"))
self.assertEqual(missing_config.returncode, 2)
self.assertIn("no config", missing_config.stderr)
self.assertFalse((self.root / "queue.json").exists())
def test_list_drops_entries_of_dead_holders(self):
queue = _load(QUEUE)
gone = subprocess.Popen(["true"])
gone.wait()
me, mine = os.getpid(), queue.identity(os.getpid())
self.write_store([
stored(1, "waiting", gone.pid, "1", None, None),
stored(2, "waiting", me, "0", None, None),
stored(3, "active", gone.pid, "1", gone.pid, "1"),
stored(4, "active", gone.pid, "1", me, mine),
stored(5, "waiting", me, mine, None, None),
])
listing = self.run_queue("list")
self.assertEqual(listing.returncode, 0, listing.stderr)
self.assertEqual([e["id"] for e in self.entries()], ["test-4", "test-5"])
self.assertEqual(
[line.split("\t")[:3] for line in listing.stdout.splitlines()[1:]],
[["run", "test-4", "active"], ["1", "test-5", "waiting"]],
)
def test_hold_takes_the_need_learned_for_its_command_from_both_history_files(self):
self.cfg["slot_limits"]["deploy"] = 0
self.write_config()
now = time.time()
rows = [
history_row(n, kind="deploy", signature="true", end=now, disk_gb=gb) for n, gb in enumerate((4, 1, 10, 3, 2))
]
self.write_history(self.history_file, rows[:3])
self.write_history(self.kept, [{**rows[0], "disk_gb": 100}, *rows[3:]])
hold, _ = self.blocked_hold("true")
listing = self.run_queue("list")
self.assertEqual(listing.returncode, 0, listing.stderr)
self.assertEqual(listing.stdout.splitlines()[1].split("\t")[5], "9")
hold.send_signal(signal.SIGTERM)
self.assertEqual(hold.wait(timeout=30), 128 + signal.SIGTERM)
def test_list_estimates_from_the_learned_median_minutes(self):
queue = _load(QUEUE)
me, mine = os.getpid(), queue.identity(os.getpid())
now = time.time()
self.write_history(
self.history_file, [history_row(n, signature="make compose-deploy", end=now) for n in range(5)]
)
deploy = {"cmd": "make compose-deploy apps=x"}
self.write_store([
{**stored(1, "active", me, mine, me, mine), **deploy, "granted": now - 240},
{**stored(2, "waiting", me, mine, None, None), **deploy},
{**stored(3, "waiting", me, mine, None, None), **deploy},
stored(4, "waiting", me, mine, None, None),
stored(5, "waiting", me, mine, None, None),
])
listing = self.run_queue("list")
self.assertEqual(listing.returncode, 0, listing.stderr)
self.assertEqual([line.split("\t")[7] for line in listing.stdout.splitlines()], [
"eta_min", "6", "6", "16", "26", "-",
])
def test_history_prints_a_table_per_signature_and_per_kind(self):
self.cfg["slot_need_gb"]["build"] = 5
self.cfg["slot_limits"] = {"deploy": 9, "test": 0}
self.write_config()
now = time.time()
deploys = [
history_row(n, kind="deploy", signature="make compose-deploy", end=now, disk_gb=n + 1, minutes=10 + n,
cpu_before=1, cpu_max=21)
for n in range(5)
]
self.write_history(self.history_file, [*deploys, *(history_row(n, signature="true", end=now) for n in (5, 6))])
result = self.run_queue("history")
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(result.stdout, (
"| Signature | Kind | Runs | Median min | p90 disk GB | p90 RAM % | Need GB |\n"
"| --- | --- | --- | --- | --- | --- | --- |\n"
"| make compose-deploy | deploy | 5 | 12.0 | 4.6 | 10.0 | 6 |\n"
"| true | test | 2 | - | - | - | - |\n"
"\n"
"| Kind | Runs | p90 RAM share % | p90 CPU share % | Slot limit | Source |\n"
"| --- | --- | --- | --- | --- | --- |\n"
"| build | 0 | - | - | 1 | seed |\n"
"| deploy | 5 | 10.0 | 20.0 | 5 | learned |\n"
"| test | 2 | - | - | 0 | capped by config |\n"
))
def test_cancel_removes_only_the_callers_waiting_entry(self):
queue = _load(QUEUE)
me, mine = os.getpid(), queue.identity(os.getpid())
self.write_store([stored(1, "waiting", me, mine, None, None), stored(2, "active", me, mine, me, mine)])
self.assertEqual(self.run_queue("cancel", "test-1", "--session", "s2").returncode, 2)
self.assertEqual(self.run_queue("cancel", "test-2", "--session", "s1").returncode, 2)
result = self.run_queue("cancel", "test-1", "--session", "s1")
self.assertEqual((result.returncode, result.stdout), (0, "cancelled test-1\n"))
self.assertEqual([e["id"] for e in self.entries()], ["test-2"])
def test_block_allows_everything_without_queue_enforce(self):
self.assertEqual(self.bash("make compose-deploy"), 0)
self.cfg["queue_enforce"] = False
self.write_config()
self.assertEqual(self.bash("make compose-deploy"), 0)
def test_block_with_queue_enforce_refuses_heavy_commands_outside_the_queue(self):
self.cfg["queue_enforce"] = True
self.write_config()
refused = self.hook(json.dumps({"tool_input": {"command": "make compose-deploy apps=x"}}))
self.assertEqual(refused.returncode, 2)
self.assertIn("queue.py hold --kind <kind> --task <register task> -- <command>", refused.stderr)
self.assertEqual(self.bash("python3 skills/kvb-check-gate/scripts/gate.py run --repo . --target test"), 2)
for command in (
"python3 /x/queue.py hold --kind deploy --task work/a -- make compose-deploy",
"make compose-down",
"ls",
'git commit -m "make compose-deploy"',
):
with self.subTest(command=command):
self.assertEqual(self.bash(command), 0)
self.assertEqual(self.hook("not json").returncode, 0)
self.config.unlink()
self.assertEqual(self.bash("make compose-deploy"), 0)
if __name__ == "__main__":
unittest.main()
+319
View File
@@ -0,0 +1,319 @@
"""Exercise radio.py: class rules, the store shared with news.py, radio silence, the three hooks and the listener."""
from __future__ import annotations
import json
import os
import subprocess
import sys
import tempfile
import time
import unittest
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
RADIO = REPO_ROOT / "skills" / "kvb-org-radio" / "scripts" / "radio.py"
CLASSES = ("BROADCAST", "DIRECT", "SECURITY", "PAN-PAN", "MAYDAY")
LISTENER = (
"import importlib.util, sys\n"
"spec = importlib.util.spec_from_file_location('radio', sys.argv[1])\n"
"radio = importlib.util.module_from_spec(spec)\n"
"spec.loader.exec_module(radio)\n"
"radio.POLL_SECONDS = 0.05\n"
"sys.argv = [sys.argv[1], 'listen']\n"
"sys.exit(radio.main())\n"
)
def strip(line: str) -> str:
parts = line.split(" ", 3)
if parts[0] != "RADIO" or parts[1] not in CLASSES:
return line
return f"{parts[1]} {parts[3]}"
class TestRadio(unittest.TestCase):
def setUp(self):
tmp = tempfile.TemporaryDirectory()
self.addCleanup(tmp.cleanup)
self.home = Path(tmp.name) / "home"
self.root = Path(tmp.name) / "council"
self.config = Path(tmp.name) / "config.json"
sessions = self.home / ".claude" / "sessions"
sessions.mkdir(parents=True)
for sid, name in (("s1", "One"), ("s2", "Two"), ("s3", "Three")):
(sessions / f"{sid}.json").write_text(json.dumps({"sessionId": sid, "name": name}), encoding="utf-8")
def _env(self, sid: str | None) -> dict[str, str]:
env = {
"PATH": "/usr/bin:/bin",
"HOME": str(self.home),
"AGENTS_COUNCIL_ROOT": str(self.root),
"AGENTS_COUNCIL_CONFIG": str(self.config),
}
if sid:
env["CLAUDE_CODE_SESSION_ID"] = sid
return env
def _exec(self, sid: str | None, stdin: str, args: tuple[str, ...]) -> subprocess.CompletedProcess:
return subprocess.run(
[sys.executable, str(RADIO), *args],
input=stdin, capture_output=True, text=True, env=self._env(sid), check=False, timeout=30,
)
def _cli(self, sid: str | None, *args: str) -> subprocess.CompletedProcess:
return self._exec(sid, "", args)
def _send(self, sid: str, *args: str) -> list[str]:
result = self._cli(sid, *args)
self.assertEqual(result.returncode, 0, result.stderr)
return result.stdout.splitlines()
def _hook(self, command: str, sid: str, **extra: str) -> str:
result = self._exec(None, json.dumps({"session_id": sid, **extra}), (command,))
self.assertEqual(result.returncode, 0, result.stderr)
return result.stdout
def _prompt(self, sid: str) -> list[str]:
return [strip(line) for line in self._hook("hook", sid).splitlines()]
def _tool(self, sid: str) -> list[str]:
out = self._hook("tool", sid)
if not out:
return []
output = json.loads(out)["hookSpecificOutput"]
self.assertEqual(output["hookEventName"], "PostToolBatch")
return [strip(line) for line in output["additionalContext"].splitlines()]
def _rows(self) -> list[dict]:
return [json.loads(line) for line in (self.root / "news.jsonl").read_text(encoding="utf-8").splitlines()]
def _pidfile(self, sid: str) -> Path:
return self.root / "radio-listen" / f"{sid}.pid"
def _listen(self, sid: str) -> subprocess.Popen:
proc = subprocess.Popen(
[sys.executable, "-c", LISTENER, str(RADIO)],
stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, env=self._env(None),
)
self.addCleanup(self._stop, proc)
proc.stdin.write(json.dumps({"session_id": sid}))
proc.stdin.close()
deadline = time.monotonic() + 10
while time.monotonic() < deadline and proc.poll() is None:
pidfile = self._pidfile(sid)
if pidfile.is_file() and pidfile.read_text(encoding="utf-8") == str(proc.pid):
return proc
time.sleep(0.02)
self.fail(f"listener for {sid} did not start")
def _stop(self, proc: subprocess.Popen) -> None:
if proc.poll() is None:
proc.kill()
proc.communicate()
def _finish(self, proc: subprocess.Popen) -> tuple[int, str]:
_, err = proc.communicate(timeout=10)
return proc.returncode, err
def test_classes_are_validated(self):
for args in (
("send", "broadcast", "x", "--to", "Two"),
("send", "direct", "x"),
("send", "mayday", "x", "--to", "Two"),
("send", "security", "x", "--wake"),
("send", "broadcast", "x", "--wake"),
("send", "bogus", "x"),
("send", "pan-pan", " "),
):
with self.subTest(args=args):
self.assertEqual(self._cli("s1", *args).returncode, 2)
self.assertEqual(self._cli(None, "send", "broadcast", "x").returncode, 2)
self.assertEqual(
self._send("s1", "send", "direct", "a b", "--to", "Two", "--to", "Three"), ["sent direct to Two, Three"]
)
self.assertEqual(self._send("s1", "post", "board"), ["sent broadcast to all"])
self.assertEqual(self._send("s1", "post", "one peer", "--to", "Two"), ["sent direct to Two"])
self.assertEqual(
[(r["class"], r["to"], r["text"], "wake" in r) for r in self._rows()],
[("direct", ["Two", "Three"], "a b", False), ("broadcast", [], "board", False), ("direct", ["Two"], "one peer", False)],
)
def test_old_rows_are_read_and_new_rows_keep_the_old_keys(self):
self.root.mkdir()
now = time.time()
old = [
{"ts": now - 10, "from": "Three", "to": [], "text": "old board"},
{"ts": now - 5, "from": "Three", "to": ["Two"], "text": "old direct"},
]
(self.root / "news.jsonl").write_text("".join(json.dumps(r) + "\n" for r in old), encoding="utf-8")
self.assertEqual(self._prompt("s2"), ["BROADCAST Three: old board", "DIRECT Three to Two: old direct"])
self.assertEqual(float((self.root / "news-seen" / "s2").read_text(encoding="utf-8")), now - 5)
self._send("s1", "send", "security", "new")
rows = self._rows()
self.assertEqual(rows[:2], old)
for row in rows:
self.assertIsInstance(row["ts"], float)
self.assertIsInstance(row["from"], str)
self.assertIsInstance(row["to"], list)
self.assertIsInstance(row["text"], str)
self.assertEqual(rows[2]["class"], "security")
def test_ts_strictly_increases_under_the_lock(self):
self.root.mkdir()
row = {"ts": time.time() + 1000, "from": "Three", "to": [], "text": "clock ahead"}
(self.root / "news.jsonl").write_text(json.dumps(row) + "\n", encoding="utf-8")
self._send("s1", "post", "first")
self._send("s1", "post", "second")
stamps = [r["ts"] for r in self._rows()]
self.assertGreater(stamps[1], stamps[0])
self.assertGreater(stamps[2], stamps[1])
def test_mayday_sets_silence_and_only_its_sender_clears_it(self):
self.assertEqual(self._send("s1", "clear"), ["no radio silence active"])
self.assertEqual(self._send("s1", "send", "mayday", "disk full")[0], "sent mayday to all")
silence = self.root / "radio-silence.json"
state = json.loads(silence.read_text(encoding="utf-8"))
self.assertEqual(sorted(state), ["by", "since", "text", "until"])
self.assertEqual((state["by"], state["text"], state["until"] - state["since"]), ("One", "disk full", 7200))
self.assertEqual(state["since"], self._rows()[-1]["ts"])
held = self._cli("s2", "clear")
self.assertEqual(held.returncode, 3)
self.assertIn("held by One", held.stdout)
self.assertTrue(silence.exists())
self.assertEqual(self._send("s1", "clear"), ["cleared radio silence by One"])
self.assertFalse(silence.exists())
self._send("s1", "send", "mayday", "again")
self.assertEqual(self._send("s2", "clear", "--force"), ["cleared radio silence by One, forced by Two"])
self.assertFalse(silence.exists())
def test_a_wake_names_every_addressee_without_a_live_listener(self):
self._pidfile("s2").parent.mkdir(parents=True)
self._pidfile("s2").write_text(str(os.getpid()), encoding="utf-8")
self.assertEqual(self._send("s1", "send", "mayday", "stop"), ["sent mayday to all", "wake: SendMessage to Three"])
self.assertEqual(
self._send("s1", "send", "direct", "answer", "--to", "Two", "--to", "Three", "--wake"),
["sent direct to Two, Three", "wake: SendMessage to Three"],
)
dead = subprocess.Popen([sys.executable, "-c", "pass"])
dead.wait()
self._pidfile("s2").write_text(str(dead.pid), encoding="utf-8")
self.assertEqual(
self._send("s1", "send", "direct", "answer", "--to", "Two", "--wake"),
["sent direct to Two", "wake: SendMessage to Two"],
)
self.assertEqual(self._send("s1", "send", "direct", "no wake", "--to", "Three"), ["sent direct to Three"])
self.assertEqual([r.get("wake") for r in self._rows()], [None, True, True, None])
def test_prompt_hook_puts_urgent_first_and_repeats_the_silence(self):
self._send("s1", "post", "board")
self._send("s1", "post", "for two", "--to", "Two")
self._send("s1", "send", "security", "sec")
self._send("s1", "send", "pan-pan", "pan")
self._send("s1", "send", "mayday", "may")
lines = self._prompt("s2")
self.assertRegex(lines[0], r"^MAYDAY ACTIVE since \d\d:\d\d by One: may; no new heavy jobs until radio\.py clear$")
self.assertEqual(
lines[1:],
["MAYDAY One: may", "PAN-PAN One: pan", "SECURITY One: sec", "BROADCAST One: board", "DIRECT One to Two: for two"],
)
self.assertEqual(len(self._prompt("s2")), 1)
self.assertNotIn("DIRECT One to Two: for two", self._prompt("s3"))
self.assertEqual(len(self._prompt("s1")), 1)
def test_prompt_hook_shows_ten_and_counts_the_rest(self):
self._send("s1", "send", "security", "early warning")
for n in range(12):
self._send("s1", "post", f"item {n}")
lines = self._prompt("s2")
self.assertEqual(lines[0], "SECURITY One: early warning")
self.assertEqual(lines[1:-1], [f"BROADCAST One: item {n}" for n in range(3, 12)])
self.assertEqual(lines[-1], "RADIO 3 more items: radio.py show --all")
self.assertEqual(self._prompt("s2"), [])
def test_tool_hook_delivers_urgent_rows_once_and_the_prompt_skips_them(self):
self._send("s1", "post", "board")
self.assertEqual(self._tool("s2"), [])
self._send("s1", "send", "pan-pan", "pan")
self.assertEqual(self._tool("s2"), ["PAN-PAN One: pan"])
self.assertEqual(self._tool("s2"), [])
self.assertEqual(self._prompt("s2"), ["BROADCAST One: board"])
self.assertEqual(self._tool("s3"), ["PAN-PAN One: pan"])
self.assertEqual(self._tool("s1"), [])
def test_tool_hook_skips_parsing_while_the_store_is_unchanged(self):
self._send("s1", "post", "x")
self.assertEqual(self._tool("s2"), [])
news = self.root / "news.jsonl"
before = news.stat()
row = {"ts": time.time(), "from": "Three", "to": [], "text": "hidden", "class": "security"}
with news.open("a", encoding="utf-8") as handle:
handle.write(json.dumps(row) + "\n")
os.utime(news, ns=(before.st_atime_ns, before.st_mtime_ns))
self.assertEqual(self._tool("s2"), [])
os.utime(news, ns=(before.st_atime_ns, before.st_mtime_ns + 10**9))
self.assertEqual(self._tool("s2"), ["SECURITY Three: hidden"])
def test_hooks_skip_subagents_and_never_fail(self):
self._send("s1", "send", "pan-pan", "pan")
self.assertEqual(self._hook("hook", "s2", agent_id="a1"), "")
self.assertEqual(self._hook("tool", "s2", agent_id="a1"), "")
listen = subprocess.run(
[sys.executable, "-c", LISTENER, str(RADIO)],
input=json.dumps({"session_id": "s2", "agent_id": "a1"}),
capture_output=True, text=True, env=self._env(None), check=False, timeout=30,
)
self.assertEqual((listen.returncode, listen.stderr), (0, ""))
self.assertFalse(self._pidfile("s2").exists())
self.assertFalse((self.root / "news-seen" / "s2").exists())
for command in ("hook", "tool"):
for stdin in ("not json", "{}", "[]"):
with self.subTest(command=command, stdin=stdin):
result = self._exec(None, stdin, (command,))
self.assertEqual((result.returncode, result.stdout), (0, ""))
def test_the_first_hook_notifies_the_operator_once(self):
log = self.root.parent / "notified.log"
script = "import sys; open(sys.argv[1], 'a').write(sys.argv[2] + '\\n')"
self.config.write_text(json.dumps({"notify": [sys.executable, "-c", script, str(log)]}), encoding="utf-8")
self._send("s1", "send", "mayday", "smoke")
self._prompt("s2")
self._prompt("s3")
self.assertEqual(log.read_text(encoding="utf-8"), "Agents radio MAYDAY by One: smoke\n")
def test_listener_wakes_on_mayday(self):
proc = self._listen("s2")
self.assertEqual(
self._send("s1", "send", "mayday", "host down"), ["sent mayday to all", "wake: SendMessage to Three"]
)
code, err = self._finish(proc)
self.assertEqual(code, 2)
self.assertEqual([strip(line) for line in err.splitlines()], ["MAYDAY One: host down"])
self.assertEqual(self._tool("s2"), [])
self.assertEqual(len(self._prompt("s2")), 1)
def test_listener_wakes_only_on_a_direct_wake(self):
proc = self._listen("s2")
self._send("s1", "post", "board")
self._send("s1", "post", "plain", "--to", "Two")
self._send("s1", "send", "pan-pan", "pan")
self._send("s1", "send", "direct", "elsewhere", "--to", "Three", "--wake")
time.sleep(0.5)
self.assertIsNone(proc.poll())
self.assertEqual(self._send("s1", "send", "direct", "answer me", "--to", "Two", "--wake"), ["sent direct to Two"])
code, err = self._finish(proc)
self.assertEqual(code, 2)
self.assertEqual([strip(line) for line in err.splitlines()], ["PAN-PAN One: pan", "DIRECT One to Two: answer me"])
self.assertEqual(self._prompt("s2"), ["BROADCAST One: board", "DIRECT One to Two: plain"])
def test_listener_yields_to_a_newer_one(self):
first = self._listen("s2")
second = self._listen("s2")
self.assertEqual(self._finish(first), (0, ""))
self.assertIsNone(second.poll())
self._pidfile("s2").write_text("0", encoding="utf-8")
self.assertEqual(self._finish(second), (0, ""))
if __name__ == "__main__":
unittest.main()
+34
View File
@@ -105,6 +105,19 @@ class TestWatchCommands(unittest.TestCase):
self._stage("warn")
self.assertEqual(self._run("block", stdin=self._bash("make compose-deploy")).returncode, 0)
def test_block_heavy_jobs_while_a_mayday_holds_radio_silence(self):
self._stage("ok")
self.root.mkdir(exist_ok=True)
silence = self.root / "radio-silence.json"
now = time.time()
silence.write_text(json.dumps({"since": now, "until": now + 60, "by": "One", "text": "fire"}), encoding="utf-8")
blocked = self._run("block", stdin=self._bash("make compose-deploy"))
self.assertEqual(blocked.returncode, 2)
self.assertIn("mayday by One", blocked.stderr)
self.assertEqual(self._run("block", stdin=self._bash("make swarm-down")).returncode, 0)
silence.write_text(json.dumps({"since": now - 120, "until": now - 60, "by": "One", "text": "fire"}), encoding="utf-8")
self.assertEqual(self._run("block", stdin=self._bash("make compose-deploy")).returncode, 0)
def test_block_ignores_heavy_words_inside_quotes_and_heredocs(self):
self.cfg["heavy_commands"] = [r"\bmake\s+(-C\s+\S+\s+)?(compose-deploy|test|swarm-\S+)(\s|$)"]
self._write_config()
@@ -152,6 +165,27 @@ class TestWatchCommands(unittest.TestCase):
self.assertEqual(hook("s1"), "")
self.assertIn("RESOURCE WARN", hook("s2"))
def test_watch_keeps_each_new_queue_history_row_once(self):
self.cfg["thresholds_gb"] = {"warn": 0, "stop": 0, "critical": 0}
self._write_config()
self.root.mkdir()
source = self.root / "queue-history.jsonl"
kept = self.base / ".local" / "state" / "agents-council" / "queue-history.jsonl"
kept.parent.mkdir(parents=True)
kept.write_text('{"id": "old"}\n', encoding="utf-8")
source.write_text('{"id": "old"}\n{"id": "a"}\n{broken\n', encoding="utf-8")
payload = json.dumps({"session_id": "s1", "hook_event_name": "PostToolUse"})
def kept_ids() -> list[str]:
self.assertEqual(self._run("watch", stdin=payload).returncode, 0)
return [json.loads(line)["id"] for line in kept.read_text(encoding="utf-8").splitlines()]
self.assertEqual(kept_ids(), ["old", "a"])
self.assertEqual(kept_ids(), ["old", "a"])
with source.open("a", encoding="utf-8") as handle:
handle.write('{"id": "b"}\n')
self.assertEqual(kept_ids(), ["old", "a", "b"])
def test_cache_trim_evicts_least_recently_used_first(self):
old, new = self.cache / "old", self.cache / "new"
old.write_bytes(b"x" * 4096)