feat(orchestrator): hierarchy view from council offices with a separate agent order
The Agents views gain a Hierarchie view: an org chart of the chair and the line offices read from the council claims. The operator appoints, hands over and recalls an office there; the page signs each change with the passkey and a host unit (client/offices.py, offices path unit) verifies the signature, removes a live holder itself and runs a plain council.py claim for the successor. council.py loses the --operator flag, so no agent can override a live holder. The agent order is now separate from task priority: rasci.py agents order stores it in agent-order.json beside the register through the nofollow helpers and never writes a --prio pin. It only breaks ties of equal task rank, also among the queue grants. The Hierarchie view edits it as a drag list. Host units run the installed skill scripts, so orchestrator.sh keeps the offices unit off while the installed council.py, nofollow.py or rasci.py differ from the tree. Job files, council files and hub records are read with O_NOFOLLOW and O_NONBLOCK and size caps; nofollow.rewrite replaces a claim without creating directories, so a beat never revives a released office. 🎯 Fix: 92 % 💥 Break: 4 % 💸 Cost: 100 % ⏱️ Time: 79 % 🛡️ Security: 100 % 🏆 Score: 100 🚦 Gate: ✅ 90/5, cost and time <= 100, security 100 % Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
8e180be256
commit
4540494c30
53 files changed
+1821
-211
No files matched your search
+9
-1
@@ -3,7 +3,8 @@ import {mkdtempSync, readFileSync, rmSync, writeFileSync} from "node:fs";
|
||||
import {tmpdir} from "node:os";
|
||||
import {join} from "node:path";
|
||||
|
||||
// Args: <chromium binary> <plan.json>; the plan lists steps {width, height, url, wait, probe, shot, media} and an optional
|
||||
// Args: <chromium binary> <plan.json>; the plan lists steps {width, height, url, wait, keys, probe, shot, media}, keys
|
||||
// being real key presses (Enter, Tab) on the focused element before the probe, and an optional
|
||||
// authenticator {credentialId, privateKey, rpId}: base64 id and PKCS#8 key of a passkey the page may sign with.
|
||||
// Prints {results, errors}: one probe value per step and every uncaught page exception.
|
||||
const [binary, planFile] = process.argv.slice(2);
|
||||
@@ -14,6 +15,7 @@ const profile = mkdtempSync(join(tmpdir(), "cdp-"));
|
||||
const flags = ["--headless", "--no-sandbox", "--disable-gpu", "--use-angle=swiftshader", "--enable-unsafe-swiftshader", "--no-first-run", "--no-default-browser-check", "--hide-scrollbars", `--user-data-dir=${profile}`, "--remote-debugging-port=0", "about:blank"];
|
||||
const browser = spawn(binary, flags, {stdio: ["ignore", "ignore", "pipe"]});
|
||||
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
const KEYS = {Enter: [13, "\r"], Tab: [9, ""]};
|
||||
|
||||
const address = await new Promise((resolve, reject) => {
|
||||
let text = "";
|
||||
@@ -79,6 +81,12 @@ try {
|
||||
await page("Page.navigate", {url: step.url});
|
||||
}
|
||||
await sleep(step.wait);
|
||||
for (const key of step.keys || []) {
|
||||
const [code, text] = KEYS[key];
|
||||
await page("Input.dispatchKeyEvent", {type: "keyDown", key, code: key, windowsVirtualKeyCode: code, text});
|
||||
await page("Input.dispatchKeyEvent", {type: "keyUp", key, code: key, windowsVirtualKeyCode: code});
|
||||
await sleep(250);
|
||||
}
|
||||
let value = null;
|
||||
if (step.probe) {
|
||||
const {result, exceptionDetails} = await page("Runtime.evaluate", {expression: step.probe, awaitPromise: true, returnByValue: true});
|
||||
|
||||
@@ -19,11 +19,13 @@ sys.path.insert(0, str(HERE.parent))
|
||||
from hub_case import HubCase
|
||||
from hub_support import ASK, iso, verifier
|
||||
from test_hub_cleanup import DONE_TIP, FAKE_PKGMGR, host
|
||||
from test_hub_offices import COUNCIL_SCRIPT
|
||||
from test_hub_offices import host as office_host
|
||||
|
||||
OUT = Path(os.environ.get("E2E_OUT", "/tmp/orchestrator-phase4-shots"))
|
||||
WIDTHS = ((1920, 1080), (1366, 768), (390, 844))
|
||||
VIEWS = ("orbit", "chord", "swimlane", "tree", "3d")
|
||||
SCREENS = ["stream", "stream/inbox", "stream/rules", *(f"{tab}/{view}" for tab in ("agents", "artifacts") for view in VIEWS)]
|
||||
SCREENS = ["stream", "stream/inbox", "stream/rules", *(f"{tab}/{view}" for tab in ("agents", "artifacts") for view in VIEWS), "agents/hierarchy"]
|
||||
HELPERS = """
|
||||
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
const until = async (check, ms) => {
|
||||
@@ -78,9 +80,57 @@ PROBE = """(async () => {@HELPERS@
|
||||
marks: all("#main svg.chart .node").length,
|
||||
note: text(document.querySelector("#main .space-note")),
|
||||
canvas: Boolean(document.querySelector("#main .graph3d canvas")),
|
||||
org: all("#main .org-chart .org-row[data-office]").map((row) => [row.dataset.office, text(row.querySelector(".org-holder"))]),
|
||||
toggles: all("#main .org-toggle").map((button) => [name(button), button.getAttribute("aria-expanded")]),
|
||||
order: all("#main .agent-order li").map((item) => item.dataset.name),
|
||||
wide: document.documentElement.scrollWidth > window.innerWidth + 1,
|
||||
};
|
||||
})()""".replace("@HELPERS@", HELPERS)
|
||||
ORG_OPEN = """(async () => {@HELPERS@
|
||||
const row = await until(() => document.querySelector('#main .org-row[data-office="identity/oidc"]'), 8000);
|
||||
if (!row) {
|
||||
return {error: text(document.getElementById("main"))};
|
||||
}
|
||||
const toggle = all("#main .org-toggle").find((button) => name(button) === "Zuklappen: identity");
|
||||
toggle.focus();
|
||||
return {focused: document.activeElement.dataset.focus, offices: all("#main .org-row[data-office]").map((entry) => entry.dataset.office)};
|
||||
})()""".replace("@HELPERS@", HELPERS)
|
||||
ORG_FOLDED = """(async () => {@HELPERS@
|
||||
const toggle = document.activeElement;
|
||||
const folded = {focused: toggle.dataset.focus, expanded: toggle.getAttribute("aria-expanded"), label: name(toggle), hidden: document.getElementById(toggle.getAttribute("aria-controls")).hidden, seen: all("#main .org-row[data-office]").filter((entry) => entry.offsetParent !== null).map((entry) => entry.dataset.office)};
|
||||
toggle.click();
|
||||
await sleep(200);
|
||||
const before = all("#main .agent-order li").map((item) => item.dataset.name);
|
||||
all("#main .agent-order li")[1].querySelector('[data-focus^="up:"]').focus();
|
||||
return {...folded, again: toggle.getAttribute("aria-expanded"), before};
|
||||
})()""".replace("@HELPERS@", HELPERS)
|
||||
ORDER_MOVED = """(async () => {@HELPERS@
|
||||
const after = all("#main .agent-order li").map((item) => item.dataset.name);
|
||||
const focused = document.activeElement.dataset.focus || null;
|
||||
const save = all("#main .agent-order button").find((button) => text(button) === "Reihenfolge speichern");
|
||||
const disabled = save.disabled;
|
||||
save.click();
|
||||
const toast = await until(() => all("#toasts .toast").find((box) => text(box).startsWith("Priorität")), 8000);
|
||||
return {after, focused, disabled, toast: text(toast)};
|
||||
})()""".replace("@HELPERS@", HELPERS)
|
||||
OPEN_TAKEOVER = """(async () => {@HELPERS@
|
||||
document.querySelector('#main [data-focus="takeover:identity/oidc"]').click();
|
||||
await sleep(400);
|
||||
return {title: text(document.querySelector(".sheet-head .panel-title")), options: all(".sheet-box select option").map((option) => option.value), work: document.querySelector('.sheet-box input[aria-label="Arbeit"]').value, focused: document.activeElement.tagName};
|
||||
})()""".replace("@HELPERS@", HELPERS)
|
||||
CONFIRM_TAKEOVER = """(async () => {@HELPERS@
|
||||
all(".sheet-box button").find((button) => name(button) === "Übergeben").click();
|
||||
const done = await until(() => document.querySelector(".sheet-box [data-result]"), 30000);
|
||||
return {result: done ? done.dataset.result : null, text: text(done), toasts: all("#toasts .toast").map(text)};
|
||||
})()""".replace("@HELPERS@", HELPERS)
|
||||
AFTER_TAKEOVER = """(async () => {@HELPERS@
|
||||
document.querySelector(".sheet-head button").click();
|
||||
const row = await until(() => {
|
||||
const found = document.querySelector('#main .org-row[data-office="identity/oidc"] .org-holder');
|
||||
return found && !text(found).endsWith("Stage") ? found : null;
|
||||
}, 15000);
|
||||
return {holder: text(row)};
|
||||
})()""".replace("@HELPERS@", HELPERS)
|
||||
ACTIONS = """(async () => {@HELPERS@
|
||||
const press = async (key) => {
|
||||
document.dispatchEvent(new KeyboardEvent("keydown", {key, bubbles: true}));
|
||||
@@ -174,6 +224,8 @@ class TestOrchestratorE2E(HubCase):
|
||||
self.claim("s1", "Skills session")
|
||||
self.write_council({
|
||||
"work/s2/claim.json": {"office": "work/s2", "session": "s2", "runtime": "claude", "name": "Stage", "since": iso(now - 7200), "beat": now - 2000, "work": "x"},
|
||||
"chair/claim.json": {"office": "chair", "session": "s1", "runtime": "claude", "name": "Skills session", "since": iso(now - 7200), "beat": now, "work": "allocate"},
|
||||
"identity/oidc/claim.json": {"office": "identity/oidc", "session": "s2", "runtime": "claude", "name": "Stage", "since": iso(now - 7200), "beat": now - 2000, "work": "oidc line"},
|
||||
"rasci.json": {"work/stage": {"title": "Stage line", "status": "running", "worker": "Stage (this session)", "roles": {"Stage (this session)": "RA"}}},
|
||||
"radio-silence.json": {"since": now - 60, "until": now + 3600, "by": "Docs", "text": "disk full"},
|
||||
"news.jsonl": self.jsonl([
|
||||
@@ -253,15 +305,20 @@ class TestOrchestratorE2E(HubCase):
|
||||
self.assertEqual([probe["chips"] for probe in probes if probe["hash"] == "#stream"], [["Für dich", "Funk", "Arbeit", "Transkripte"]] * len(WIDTHS))
|
||||
self.assertEqual((stream["menus"], stream["custom"]), (["Filter"], True))
|
||||
self.assertEqual(by_hash["#stream/inbox"]["inbox"], 2)
|
||||
for tab in ("agents", "artifacts"):
|
||||
for tab, own in (("agents", ["Hierarchie"]), ("artifacts", [])):
|
||||
with self.subTest(tab=tab):
|
||||
self.assertEqual(by_hash[f"#{tab}/orbit"]["views"], ["Orbit", "Chord", "Swimlane", "Tree", "3D"])
|
||||
self.assertEqual(by_hash[f"#{tab}/orbit"]["views"], ["Orbit", "Chord", "Swimlane", "Tree", "3D", *own])
|
||||
self.assertEqual(by_hash[f"#{tab}/3d"]["layouts"], ["Kraft", "Zeit", "Spawn-Tiefe"])
|
||||
self.assertTrue(by_hash[f"#{tab}/3d"]["canvas"], by_hash[f"#{tab}/3d"]["note"])
|
||||
self.assertRegex(by_hash[f"#{tab}/3d"]["note"], r"^\d+ Knoten · \d+ Kanten · Kraft$")
|
||||
agents = by_hash["#agents/orbit"]
|
||||
self.assertEqual(agents["toolbar"], ["Funk an alle", "Mayday aufheben", "Priorität", "Tag/Nacht"])
|
||||
self.assertEqual(agents["toAll"], [["newsletter", True], ["broadcast", False], ["pan-pan", False], ["security", False], ["mayday", False]])
|
||||
hierarchy = by_hash["#agents/hierarchy"]
|
||||
self.assertEqual([office for office, _ in hierarchy["org"]], ["chair", "identity/oidc", "work/s1", "work/s2"])
|
||||
self.assertEqual(hierarchy["toggles"], [["Zuklappen: Operator", "true"], ["Zuklappen: Chair", "true"], ["Zuklappen: identity", "true"], ["Zuklappen: Präsidenten", "true"]])
|
||||
self.assertEqual(hierarchy["toolbar"], agents["toolbar"])
|
||||
self.assertGreaterEqual(len(hierarchy["order"]), 2)
|
||||
self.assertEqual(by_hash["#agents/tree"]["spawn"][0], ["operator", None])
|
||||
self.assertIn(["twig", "true"], by_hash["#agents/tree"]["spawn"])
|
||||
self.assertIn(["twig", "false"], by_hash["#agents/tree"]["spawn"])
|
||||
@@ -363,6 +420,45 @@ class TestOrchestratorE2E(HubCase):
|
||||
["branch", "tree", "--format", "json", "--output", str(self.tree_file)],
|
||||
])
|
||||
|
||||
def test_chromium_folds_the_hierarchy_by_keyboard_reorders_agents_and_hands_an_office_over_with_a_signature(self):
|
||||
signer = self.pin()
|
||||
trust = verifier.Trust(approvers=self.approvers, replay=self.replay / "offices.json", origin=self.origin)
|
||||
runner = office_host.Offices(trust, self.council, COUNCIL_SCRIPT, self.outside / "home")
|
||||
jobs, results = self.offices / "jobs", self.offices / "results"
|
||||
self.beside(lambda: any(jobs.iterdir()) and office_host.cycle(jobs, results, runner))
|
||||
private = "".join(line for line in signer.pem.read_text(encoding="utf-8").splitlines() if not line.startswith("-----"))
|
||||
authenticator = {"credentialId": base64.b64encode(signer.raw_id).decode(), "privateKey": private, "rpId": "localhost"}
|
||||
local = f"http://localhost:{self.port}/"
|
||||
steps = [
|
||||
{"width": 1366, "height": 768, "url": f"{local}#agents/hierarchy", "wait": 1200, "probe": ORG_OPEN, "shot": self.shot("1366-hierarchy-open")},
|
||||
{"width": 1366, "height": 768, "url": None, "wait": 0, "keys": ["Enter"], "probe": ORG_FOLDED, "shot": None},
|
||||
{"width": 1366, "height": 768, "url": None, "wait": 0, "keys": ["Enter"], "probe": ORDER_MOVED, "shot": self.shot("1366-hierarchy-order")},
|
||||
{"width": 1366, "height": 768, "url": None, "wait": 0, "probe": OPEN_TAKEOVER, "shot": self.shot("1366-hierarchy-takeover")},
|
||||
{"width": 1366, "height": 768, "url": None, "wait": 0, "probe": CONFIRM_TAKEOVER, "shot": self.shot("1366-hierarchy-takeover-result")},
|
||||
{"width": 1366, "height": 768, "url": None, "wait": 0, "probe": AFTER_TAKEOVER, "shot": self.shot("1366-hierarchy-after")},
|
||||
]
|
||||
data = self.drive(steps, authenticator)
|
||||
(OUT / "chromium-hierarchy.json").write_text(json.dumps(data, indent=1), encoding="utf-8")
|
||||
self.assertEqual(data["errors"], [])
|
||||
opened, folded, moved, sheet, confirmed, after = data["results"]
|
||||
self.assertNotIn("error", opened)
|
||||
self.assertEqual(opened["focused"], "toggle:dimension:identity")
|
||||
self.assertEqual((folded["focused"], folded["expanded"], folded["label"], folded["hidden"], folded["again"]), ("toggle:dimension:identity", "false", "Aufklappen: identity", True, "true"))
|
||||
self.assertNotIn("identity/oidc", folded["seen"])
|
||||
before = folded["before"]
|
||||
self.assertEqual(moved["after"], [before[1], before[0], *before[2:]])
|
||||
self.assertEqual((moved["focused"], moved["disabled"]), (f"down:{before[1]}", False))
|
||||
self.assertTrue(moved["toast"].startswith("Priorität"), moved)
|
||||
self.assertIn(("rasci.py", ["agents", "order", ",".join(moved["after"])]), [(Path(script).name, args) for script, args in self.scripts.calls])
|
||||
self.assertEqual((sheet["title"], sheet["options"], sheet["work"], sheet["focused"]), ("identity/oidc: Übergeben", ["s1"], "oidc line", "SELECT"))
|
||||
self.assertEqual((confirmed["result"], confirmed["text"]), ("done", "identity/oidcerledigt"), confirmed)
|
||||
self.assertIn("Übergeben: erledigtidentity/oidc: erledigt", confirmed["toasts"])
|
||||
self.assertNotIn("error", after)
|
||||
claim = json.loads((self.council / "identity" / "oidc" / "claim.json").read_text(encoding="utf-8"))
|
||||
self.assertEqual((claim["session"], claim["work"]), ("s1", "oidc line"))
|
||||
written = [json.loads(path.read_text(encoding="utf-8")) for path in results.glob("*.json")]
|
||||
self.assertEqual([[(item["office"], item["action"], item["status"]) for item in result["items"]] for result in written], [[("identity/oidc", "takeover", "done")]])
|
||||
|
||||
@unittest.skipUnless(shutil.which(os.environ.get("FIREFOX", "firefox")), "no Firefox")
|
||||
def test_firefox_walks_every_screen_at_three_widths(self):
|
||||
shots = [(width, height, screen, OUT / f"firefox-{width}-{screen.replace('/', '-')}.png") for width, height in WIDTHS for screen in SCREENS]
|
||||
|
||||
+2
-1
@@ -43,6 +43,7 @@ class HubCase(unittest.TestCase):
|
||||
self.tree_file = base / "tree" / "branch-tree.json"
|
||||
self.timers = base / "timers"
|
||||
self.cleanup = base / "state" / "cleanup"
|
||||
self.offices = base / "state" / "offices"
|
||||
self.approvers = base / "config" / "approvers.json"
|
||||
self.replay = base / "replay"
|
||||
for path in (self.pages, self.state, self.outside, self.projects, self.tree_file.parent, self.timers):
|
||||
@@ -64,7 +65,7 @@ class HubCase(unittest.TestCase):
|
||||
transcripts=self.projects,
|
||||
scripts=self.scripts,
|
||||
templates=HUB / "templates",
|
||||
places=server.Places(tree_file=self.tree_file, timers=self.timers, vendor=HUB / "vendor", cleanup=self.cleanup),
|
||||
places=server.Places(tree_file=self.tree_file, timers=self.timers, vendor=HUB / "vendor", cleanup=self.cleanup, offices=self.offices),
|
||||
model=server.Model(url="", model="", key=""),
|
||||
engines=self.engines,
|
||||
approvers=self.approvers,
|
||||
|
||||
+57
-1
@@ -2,6 +2,8 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
@@ -9,6 +11,7 @@ import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
COUNCIL = REPO_ROOT / "skills" / "kvb-org-organize" / "scripts" / "council.py"
|
||||
@@ -30,7 +33,7 @@ class TestCouncil(unittest.TestCase):
|
||||
json.dumps({"sessionId": sid, "name": name}), encoding="utf-8"
|
||||
)
|
||||
|
||||
def _run(self, sid: str, *args: str, stdin: str = "") -> subprocess.CompletedProcess:
|
||||
def _run(self, sid: str, *args: str, stdin: str = "", cwd: str | None = None) -> subprocess.CompletedProcess:
|
||||
env = {
|
||||
"PATH": "/usr/bin:/bin",
|
||||
"HOME": str(self.home),
|
||||
@@ -43,6 +46,7 @@ class TestCouncil(unittest.TestCase):
|
||||
text=True,
|
||||
env=env,
|
||||
input=stdin,
|
||||
cwd=cwd,
|
||||
check=False,
|
||||
)
|
||||
|
||||
@@ -74,6 +78,58 @@ class TestCouncil(unittest.TestCase):
|
||||
self.assertEqual(self._run("s1", "release", "work/x").returncode, 0)
|
||||
self.assertFalse((self.root / "work/x").exists())
|
||||
|
||||
def test_no_flag_lets_an_agent_take_over_or_release_a_live_holder(self):
|
||||
self._run("s1", "claim", "security/gui", "--work", "w")
|
||||
before = (self.root / "security/gui/claim.json").read_text(encoding="utf-8")
|
||||
for args in (("takeover", "security/gui", "--work", "w2"), ("takeover", "security/gui", "--work", "w2", "--operator"), ("release", "security/gui"), ("release", "security/gui", "--operator")):
|
||||
with self.subTest(args=args):
|
||||
self.assertIn(self._run("s2", *args).returncode, (2, 3))
|
||||
self.assertEqual((self.root / "security/gui/claim.json").read_text(encoding="utf-8"), before)
|
||||
|
||||
def test_no_office_operation_follows_a_planted_link(self):
|
||||
outside = Path(self.tmp.name) / "outside"
|
||||
(outside / "gui").mkdir(parents=True)
|
||||
(outside / "gui" / "keep.txt").write_text("x", encoding="utf-8")
|
||||
(outside / "gui" / "claim.json").write_text(json.dumps({"session": "s1", "runtime": "claude-code", "beat": 1.0}), encoding="utf-8")
|
||||
self.root.mkdir()
|
||||
(self.root / "security").symlink_to(outside)
|
||||
for args in (
|
||||
("takeover", "security/gui", "--work", "w"),
|
||||
("release", "security/gui"),
|
||||
("claim", "security/new", "--work", "w"),
|
||||
("claim", "security/gui", "--work", "w"),
|
||||
):
|
||||
with self.subTest(args=args):
|
||||
self.assertNotEqual(self._run("s2", *args).stdout.split(" ")[0], "elected")
|
||||
self.assertEqual(sorted(path.name for path in outside.rglob("*")), ["claim.json", "gui", "keep.txt"])
|
||||
(self.root / "work").mkdir()
|
||||
(self.root / "work" / "x").symlink_to(outside / "gui")
|
||||
self.assertEqual(self._run("s2", "takeover", "work/x", "--work", "w").returncode, 3)
|
||||
self.assertTrue((outside / "gui" / "keep.txt").exists())
|
||||
|
||||
def test_a_claim_from_the_root_directory_records_no_workdir_or_branch(self):
|
||||
self.assertEqual(self._run("s2", "claim", "work/y", "--work", "appointed", cwd="/").returncode, 0)
|
||||
row = json.loads((self.root / "rasci.json").read_text(encoding="utf-8"))["work/y"]
|
||||
self.assertEqual((row["worker"], row["description"], row["status"]), ("Two", "appointed", "running"))
|
||||
self.assertNotIn("workdir", row)
|
||||
self.assertNotIn("branch", row)
|
||||
|
||||
def test_a_vacant_office_is_taken_over_without_a_council_root_or_group(self):
|
||||
self.assertEqual(self._run("s1", "takeover", "work/x", "--work", "w").stdout, "elected work/x\n")
|
||||
self.assertEqual(self._run("s1", "takeover", "security/gui", "--work", "w").stdout, "elected security/gui\n")
|
||||
|
||||
def test_a_beat_racing_a_release_never_brings_the_office_back(self):
|
||||
spec = importlib.util.spec_from_file_location("council_beat_race", COUNCIL)
|
||||
council = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(council)
|
||||
self.root.mkdir()
|
||||
council.ROOT = self.root
|
||||
released = {"session": "s1", "runtime": "claude-code", "beat": 1.0}
|
||||
council.claims = lambda: [("work/gone", dict(released))]
|
||||
with mock.patch.dict(os.environ, {"CLAUDE_CODE_SESSION_ID": "s1"}):
|
||||
self.assertEqual(council.cmd_beat(argparse.Namespace(session=None)), 0)
|
||||
self.assertEqual(sorted(self.root.rglob("*")), [])
|
||||
|
||||
def test_gate_blocks_unclaimed_edits_in_a_git_worktree_only(self):
|
||||
repo = Path(self.tmp.name) / "repo"
|
||||
(repo / ".git").mkdir(parents=True)
|
||||
|
||||
+11
-4
@@ -69,7 +69,11 @@ class TestHubBoard(HubCase):
|
||||
"rasci.json": {"work/a": {"worker": "A", "status": "running", "prio": 2}, "work/b": {"worker": "B session", "status": "queued", "prio": 1}, "work/c": {"worker": "C", "status": "done"}},
|
||||
})
|
||||
state = self.snapshot()
|
||||
self.assertEqual((state["silence"]["by"], state["night"], state["priority"]), ("Docs", "evening", ["B session", "A"]))
|
||||
self.assertEqual((state["silence"]["by"], state["night"]), ("Docs", "evening"))
|
||||
self.assertEqual(state["agent_order"], ["B session", "A", "Gatekeeper", "Skills session"])
|
||||
self.write_council({"agent-order.json": {"agents": ["Gatekeeper", "Nobody", "A", "A"], "set": "2026-11-07T10:00:00+00:00"}})
|
||||
self.assertEqual(self.snapshot()["agent_order"], ["Gatekeeper", "A", "B session", "Skills session"])
|
||||
register = (self.council / "rasci.json").read_text(encoding="utf-8")
|
||||
|
||||
def board(action: str, payload: object) -> tuple[int, dict | None]:
|
||||
status, _, body = self.post_json(f"/api/board/{action}", payload, self.guard(self.host))
|
||||
@@ -81,11 +85,14 @@ class TestHubBoard(HubCase):
|
||||
status, result = board("broadcast", {"text": "main is green", "class": "newsletter"})
|
||||
message_id = self.tool("operator_message", {"id": self.snapshot()["messages"][-1]["id"]})["structuredContent"]["id"]
|
||||
self.assertEqual(self.scripts.calls[-1], (skill_scripts.RADIO, ["post", "--session", "Operator", f"[kvb-orchestrator msg {message_id}] main is green"]))
|
||||
self.assertEqual(board("order", {"agents": ["A", "B session"]})[0], 200)
|
||||
self.assertEqual(self.scripts.calls[-1], (skill_scripts.RASCI, ["order", "A,B session"]))
|
||||
for payload in ({"agents": ["A,B"]}, {"agents": ["A", "A"]}, {"agents": []}, {"agents": ["-x"]}, {"agents": "A"}, {"text": "x"}):
|
||||
self.assertEqual(board("order", {"agents": ["Gatekeeper", "A", "B session"]})[0], 200)
|
||||
self.assertEqual(self.scripts.calls[-1], (skill_scripts.RASCI, ["agents", "order", "Gatekeeper,A,B session"]))
|
||||
calls = len(self.scripts.calls)
|
||||
for payload in ({"agents": ["A,B"]}, {"agents": ["A", "A"]}, {"agents": []}, {"agents": ["-x"]}, {"agents": "A"}, {"text": "x"}, {"agents": ["A", "Nobody"]}, {"agents": ["C"]}):
|
||||
with self.subTest(payload=payload):
|
||||
self.assertEqual(board("order", payload)[0], 400)
|
||||
self.assertEqual(len(self.scripts.calls), calls)
|
||||
self.assertEqual((self.council / "rasci.json").read_text(encoding="utf-8"), register)
|
||||
calls = len(self.scripts.calls)
|
||||
self.assertEqual(board("evening", {})[0], 200)
|
||||
self.assertEqual(self.scripts.calls[calls:], [
|
||||
|
||||
+15
-2
@@ -9,10 +9,14 @@ from hub_support import NAV
|
||||
NAV_PROBE = """
|
||||
const N = require(process.argv[1]);
|
||||
const hashes = ["", "#stream", "#stream/inbox", "#stream/rules", "#stream/nope", "#agents/chord", "#agents/3d", "#agents", "#agents/nope",
|
||||
"#artifacts/tree", "#artifacts/orbit", "#graph", "#graph/swimlane", "#graph/orbit", "#tree", "#bogus", "#agents%2Fswimlane"];
|
||||
"#artifacts/tree", "#artifacts/orbit", "#graph", "#graph/swimlane", "#graph/orbit", "#tree", "#bogus", "#agents%2Fswimlane",
|
||||
"#agents/hierarchy", "#artifacts/hierarchy", "#graph/hierarchy", "#tree/hierarchy"];
|
||||
console.log(JSON.stringify({
|
||||
tabs: N.TABS.map(([id, label]) => [id, label, (N.SUBS[id] || []).map(([sub, name]) => [sub, name])]),
|
||||
views: N.VIEWS.map(([id, label]) => [id, label]),
|
||||
agents: N.viewsOf("agents").map(([id, label]) => [id, label]),
|
||||
artifacts: N.viewsOf("artifacts").map(([id, label]) => [id, label]),
|
||||
icon: N.ICONS.hierarchy,
|
||||
routes: hashes.map((hash) => N.route(hash)),
|
||||
hashes: [["stream", null], ["inbox", null], ["rules", null], ["agents", "chord"], ["artifacts", "3d"]].map(([screen, view]) => N.hashOf(screen, view)),
|
||||
tabOf: ["stream", "inbox", "rules", "agents", "artifacts"].map((screen) => N.tabOf(screen)),
|
||||
@@ -30,7 +34,12 @@ class TestNavModule(unittest.TestCase):
|
||||
self.assertEqual(self.data["tabs"], [["stream", "Stream", [["inbox", "Inbox"], ["rules", "Regeln"]]], ["agents", "Agents", []], ["artifacts", "Artifacts", []]])
|
||||
|
||||
def test_agents_and_artifacts_offer_five_views_in_order(self):
|
||||
self.assertEqual(self.data["views"], [["orbit", "Orbit"], ["chord", "Chord"], ["swimlane", "Swimlane"], ["tree", "Tree"], ["3d", "3D"]])
|
||||
shared = [["orbit", "Orbit"], ["chord", "Chord"], ["swimlane", "Swimlane"], ["tree", "Tree"], ["3d", "3D"]]
|
||||
self.assertEqual((self.data["views"], self.data["artifacts"]), (shared, shared))
|
||||
|
||||
def test_only_agents_add_the_hierarchy_after_3d(self):
|
||||
self.assertEqual(self.data["agents"][-2:], [["3d", "3D"], ["hierarchy", "Hierarchie"]])
|
||||
self.assertEqual(self.data["icon"], "bi-diagram-2")
|
||||
|
||||
def test_hashes_route_and_the_old_ones_redirect(self):
|
||||
stream = {"screen": "stream", "view": None, "redirect": None}
|
||||
@@ -52,6 +61,10 @@ class TestNavModule(unittest.TestCase):
|
||||
{"screen": "artifacts", "view": "tree", "redirect": "#artifacts/tree"},
|
||||
stream,
|
||||
{"screen": "agents", "view": "swimlane", "redirect": None},
|
||||
{"screen": "agents", "view": "hierarchy", "redirect": None},
|
||||
{"screen": "artifacts", "view": None, "redirect": None},
|
||||
{"screen": "agents", "view": "hierarchy", "redirect": "#agents/hierarchy"},
|
||||
{"screen": "artifacts", "view": "tree", "redirect": "#artifacts/tree"},
|
||||
])
|
||||
|
||||
def test_every_screen_has_its_own_hash_and_tab(self):
|
||||
|
||||
@@ -0,0 +1,324 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import secrets
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
from hub_case import HubCase
|
||||
from hub_support import HUB, ORIGIN, REPO_ROOT, key, signing, verifier
|
||||
|
||||
OFFICES = HUB / "client" / "offices.py"
|
||||
OFFICES_SPEC = importlib.util.spec_from_file_location("kvb_orchestrator_offices_runner", OFFICES)
|
||||
host = importlib.util.module_from_spec(OFFICES_SPEC)
|
||||
OFFICES_SPEC.loader.exec_module(host)
|
||||
COUNCIL_SCRIPT = REPO_ROOT / "skills" / "kvb-org-organize" / "scripts" / "council.py"
|
||||
SINCE = "2026-11-07T08:00:00+00:00"
|
||||
|
||||
|
||||
def office_claim(office: str, session: str, name: str, runtime: str) -> dict:
|
||||
return {"office": office, "session": session, "runtime": runtime, "name": name, "since": SINCE, "beat": time.time(), "work": f"{office} work"}
|
||||
|
||||
|
||||
def unblock(fifo: Path) -> None:
|
||||
try:
|
||||
os.close(os.open(fifo, os.O_WRONLY | os.O_NONBLOCK))
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
class TestHubOffices(HubCase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.write_council({
|
||||
"chair/claim.json": office_claim("chair", "s1", "Maintainer", "claude-code"),
|
||||
"identity/oidc/claim.json": office_claim("identity/oidc", "s2", "Skills", "claude-code"),
|
||||
"work/hub/claim.json": office_claim("work/hub", "s3", "Stage", "claude-code"),
|
||||
"cluster/gate/claim.json": office_claim("cluster/gate", "s2", "Skills", "claude-code"),
|
||||
})
|
||||
self.snapshot()
|
||||
|
||||
def sign(self, items: list[dict]) -> tuple[int, bytes]:
|
||||
status, _, body = self.post_json("/api/sign", {"items": items}, self.guard(self.host))
|
||||
return status, body
|
||||
|
||||
def test_the_chair_is_the_holder_of_the_chair_office(self):
|
||||
state = self.snapshot()
|
||||
self.assertEqual([(node["id"], node["kind"]) for node in state["graph"]["nodes"][:2]], [("Operator", "operator"), ("s1", "chair")])
|
||||
self.assertEqual({claim["office"]: claim["kind"] for claim in state["claims"]}, {"chair": "chair", "cluster/gate": "cluster", "identity/oidc": "line", "work/hub": "work"})
|
||||
code = re.sub(r'<script nonce="[^"]+" id="state" type="application/json">.*?</script>', "", self.index(), flags=re.DOTALL)
|
||||
self.assertNotIn("Maintainer", code)
|
||||
|
||||
def test_a_fifo_or_link_at_a_council_file_never_blocks_or_feeds_the_state(self):
|
||||
fifos = [self.council / "work" / "fifo" / "claim.json", self.council / "agent-order.json"]
|
||||
fifos[0].parent.mkdir(parents=True)
|
||||
for fifo in fifos:
|
||||
os.mkfifo(fifo, 0o600)
|
||||
self.addCleanup(unblock, fifo)
|
||||
planted = self.outside / "claim.json"
|
||||
planted.write_text(json.dumps(office_claim("security/gui", "s9", "Linked", "claude-code")), encoding="utf-8")
|
||||
(self.council / "security" / "gui").mkdir(parents=True)
|
||||
(self.council / "security" / "gui" / "claim.json").symlink_to(planted)
|
||||
offices = [claim["office"] for claim in self.snapshot()["claims"]]
|
||||
self.assertEqual(offices, ["chair", "cluster/gate", "identity/oidc", "work/hub"])
|
||||
|
||||
def test_a_vacant_chair_draws_no_chair_node_and_the_operator_grants(self):
|
||||
shutil.rmtree(self.council / "chair")
|
||||
now = time.time()
|
||||
self.write_council({
|
||||
"rasci.json": {"work/hub": {"title": "Hub", "status": "running", "worker": "Stage", "workdir": "/repo/hub", "roles": {"Stage": "RA"}}},
|
||||
"queue-history.jsonl": self.jsonl([{"id": "q1", "kind": "gate", "workdir": "/repo/hub", "start": now - 10, "end": now - 5, "exit": 0}]),
|
||||
})
|
||||
state = self.snapshot()
|
||||
self.assertEqual([node["kind"] for node in state["graph"]["nodes"]].count("chair"), 0)
|
||||
self.assertIn({"source": "Operator", "target": "s3", "class": "grant", "count": 1}, state["graph"]["edges"])
|
||||
|
||||
def test_signing_an_office_needs_a_known_session_the_current_holder_and_one_line_per_dimension(self):
|
||||
for name, item in (
|
||||
("appoint a vacant line", signing.office_item("appoint", "security/gui", "", "s3", "GUI line")),
|
||||
("take over the chair", signing.office_item("takeover", "chair", "s1", "s2", "allocate")),
|
||||
("recall a president", signing.office_item("recall", "work/hub", "s3", "", "")),
|
||||
("take over a line in the own dimension", signing.office_item("takeover", "identity/oidc", "s2", "s3", "oidc")),
|
||||
):
|
||||
with self.subTest(name=name):
|
||||
status, body = self.sign([item])
|
||||
self.assertEqual(status, 200, body)
|
||||
self.assertEqual(json.loads(body)["payload"]["items"], [signing.item_hash(item)])
|
||||
bad = (
|
||||
("capital office", signing.office_item("appoint", "Chair", "", "s3", "w"), 400),
|
||||
("three levels", signing.office_item("appoint", "a/b/c", "", "s3", "w"), 400),
|
||||
("climbing office", signing.office_item("appoint", "../x", "", "s3", "w"), 400),
|
||||
("bare group", signing.office_item("appoint", "work", "", "s3", "w"), 400),
|
||||
("unknown session", signing.office_item("appoint", "security/gui", "", "s9", "w"), 400),
|
||||
("pseudo session", signing.office_item("appoint", "security/gui", "", "name:Docs", "w"), 400),
|
||||
("the operator", signing.office_item("appoint", "security/gui", "", "Operator", "w"), 400),
|
||||
("unknown action", {**signing.office_item("appoint", "security/gui", "", "s3", "w"), "action": "fire"}, 400),
|
||||
("empty work", signing.office_item("appoint", "security/gui", "", "s3", " "), 400),
|
||||
("two lines of work", signing.office_item("appoint", "security/gui", "", "s3", "a\nb"), 400),
|
||||
("long work", signing.office_item("appoint", "security/gui", "", "s3", "x" * 301), 400),
|
||||
("recall with a successor", signing.office_item("recall", "work/hub", "s3", "s2", ""), 400),
|
||||
("extra key", {**signing.office_item("recall", "work/hub", "s3", "", ""), "command": "rm -rf ~"}, 400),
|
||||
("missing key", {key_name: value for key_name, value in signing.office_item("recall", "work/hub", "s3", "", "").items() if key_name != "holder"}, 400),
|
||||
("number", {**signing.office_item("recall", "work/hub", "s3", "", ""), "holder": 3}, 400),
|
||||
("held office", signing.office_item("appoint", "identity/oidc", "", "s3", "w"), 409),
|
||||
("holder changed", signing.office_item("takeover", "chair", "s2", "s3", "w"), 409),
|
||||
("vacant recall", signing.office_item("recall", "security/gui", "", "", ""), 400),
|
||||
("option as session", signing.office_item("appoint", "security/gui", "", "--all", "w"), 400),
|
||||
("successor is the holder", signing.office_item("takeover", "chair", "s1", "s1", "w"), 400),
|
||||
("second line of a dimension", signing.office_item("appoint", "identity/saml", "", "s2", "w"), 400),
|
||||
)
|
||||
for name, item, expected in bad:
|
||||
with self.subTest(name=name):
|
||||
self.assertEqual(self.sign([item])[0], expected)
|
||||
item = signing.office_item("appoint", "security/gui", "", "s3", "GUI line")
|
||||
self.assertEqual(self.sign([item, item])[0], 400)
|
||||
|
||||
def test_a_signed_office_becomes_a_job_without_any_command(self):
|
||||
item = signing.office_item("takeover", "chair", "s1", "s2", "allocate")
|
||||
signer = self.pin()
|
||||
proof = signer.proof(self.prepare([item])["payload"], self.origin)
|
||||
self.assertEqual(self.post_json("/api/offices", {"items": [item], "proof": None}, self.guard(self.host))[0], 400)
|
||||
self.assertEqual(self.post_json("/api/offices", {"items": [item], "proof": proof}, {"Host": self.host})[0], 403)
|
||||
self.assertEqual(self.post_json("/api/offices", {"items": [{**item, "holder": "s3"}], "proof": proof}, self.guard(self.host))[0], 409)
|
||||
status, _, body = self.post_json("/api/offices", {"items": [item], "proof": proof}, self.guard(self.host))
|
||||
self.assertEqual(status, 202, body)
|
||||
job_id = json.loads(body)["id"]
|
||||
written = self.offices / "jobs" / f"{job_id}.json"
|
||||
job = json.loads(written.read_text(encoding="utf-8"))
|
||||
self.assertEqual(sorted(job), ["created", "id", "items", "proof", "v"])
|
||||
self.assertEqual(job["items"], [{key_name: item[key_name] for key_name in ("action", "office", "holder", "session", "work")}])
|
||||
self.assertEqual(job["proof"], proof)
|
||||
self.assertNotIn("command", written.read_text(encoding="utf-8"))
|
||||
self.assertEqual((written.stat().st_mode & 0o777, (self.offices / "jobs").stat().st_mode & 0o777), (0o600, 0o700))
|
||||
self.assertEqual(json.loads(self.get(f"/api/offices/{job_id}")[2])["status"], "queued")
|
||||
self.assertEqual(self.get("/api/offices/0000000000000000")[0], 404)
|
||||
self.assertEqual(self.get("/api/offices")[0], 405)
|
||||
result = {"id": job_id, "finished": time.time(), "items": [{**job["items"][0], "status": "done", "reason": ""}]}
|
||||
(self.offices / "results" / f"{job_id}.json").write_text(json.dumps(result), encoding="utf-8")
|
||||
self.assertEqual(json.loads(self.get(f"/api/offices/{job_id}")[2])["status"], "done")
|
||||
|
||||
|
||||
class TestHostOffices(unittest.TestCase):
|
||||
def setUp(self):
|
||||
tmp = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(tmp.cleanup)
|
||||
base = Path(tmp.name)
|
||||
self.council = base / "council"
|
||||
self.council.mkdir(mode=0o700)
|
||||
self.home = base / "home"
|
||||
sessions = self.home / ".claude" / "sessions"
|
||||
sessions.mkdir(parents=True)
|
||||
for sid, name in (("s1", "One"), ("s2", "Two")):
|
||||
(sessions / f"{sid}.json").write_text(json.dumps({"sessionId": sid, "name": name}), encoding="utf-8")
|
||||
self.jobs, self.results = base / "jobs", base / "results"
|
||||
self.jobs.mkdir()
|
||||
self.results.mkdir()
|
||||
self.approvers = base / "config" / "approvers.json"
|
||||
self.trust = verifier.Trust(approvers=self.approvers, replay=base / "replay" / "offices.json", origin=ORIGIN)
|
||||
self.signer = key(-7, "main")
|
||||
self.signer.pin(self.approvers, ORIGIN)
|
||||
self.runner = host.Offices(self.trust, self.council, COUNCIL_SCRIPT, self.home)
|
||||
|
||||
def proof(self, items: list[dict], signer) -> dict:
|
||||
hashes = [signing.item_hash(signing.office_item(**item)) for item in items]
|
||||
return signer.proof(signing.approval_payload(hashes, secrets.token_hex(16), int(time.time()) + 600), ORIGIN)
|
||||
|
||||
def run_job(self, items: list[dict], proof: object) -> dict:
|
||||
job_id = secrets.token_hex(8)
|
||||
(self.jobs / f"{job_id}.json").write_text(json.dumps({"v": 1, "id": job_id, "created": "x", "items": items, "proof": proof}), encoding="utf-8")
|
||||
self.assertEqual(host.cycle(self.jobs, self.results, self.runner), 1)
|
||||
self.assertEqual(list(self.jobs.iterdir()), [])
|
||||
return json.loads((self.results / f"{job_id}.json").read_text(encoding="utf-8"))
|
||||
|
||||
def signed(self, item: dict) -> dict:
|
||||
return self.run_job([item], self.proof([item], self.signer))
|
||||
|
||||
def holder(self, office: str) -> dict | None:
|
||||
path = self.council / office / "claim.json"
|
||||
return json.loads(path.read_text(encoding="utf-8")) if path.exists() else None
|
||||
|
||||
def files(self) -> list[str]:
|
||||
return sorted(str(path.relative_to(self.council)) for path in self.council.rglob("*"))
|
||||
|
||||
def test_appoint_take_over_and_recall_run_the_council_script_for_the_operator(self):
|
||||
appoint = {"action": "appoint", "office": "chair", "holder": "", "session": "s1", "work": "allocate"}
|
||||
self.assertEqual([(entry["status"], entry["code"]) for entry in self.signed(appoint)["items"]], [("done", 0)])
|
||||
self.assertEqual({key_name: self.holder("chair")[key_name] for key_name in ("session", "runtime", "name", "work")}, {"session": "s1", "runtime": "claude-code", "name": "One", "work": "allocate"})
|
||||
takeover = {"action": "takeover", "office": "chair", "holder": "s1", "session": "s2", "work": "allocate more"}
|
||||
self.assertEqual(self.signed(takeover)["items"][0]["status"], "done")
|
||||
self.assertEqual((self.holder("chair")["session"], self.holder("chair")["name"]), ("s2", "Two"))
|
||||
asleep = {"action": "appoint", "office": "security/gui", "holder": "", "session": "s9", "work": "gui line"}
|
||||
self.assertEqual(self.signed(asleep)["items"][0]["status"], "done")
|
||||
self.assertEqual((self.holder("security/gui")["session"], self.holder("security/gui")["runtime"]), ("s9", "other"))
|
||||
recall = {"action": "recall", "office": "chair", "holder": "s2", "session": "", "work": ""}
|
||||
self.assertEqual(self.signed(recall)["items"][0]["status"], "done")
|
||||
self.assertIsNone(self.holder("chair"))
|
||||
president = {"action": "appoint", "office": "work/new", "holder": "", "session": "s1", "work": "build it"}
|
||||
self.assertEqual(self.signed(president)["items"][0]["status"], "done")
|
||||
row = json.loads((self.council / "rasci.json").read_text(encoding="utf-8"))["work/new"]
|
||||
self.assertEqual((row["worker"], row["description"], "workdir" in row, "branch" in row), ("One", "build it", False, False))
|
||||
|
||||
def test_every_doubtful_item_is_refused_and_changes_nothing(self):
|
||||
appoint = {"action": "appoint", "office": "chair", "holder": "", "session": "s1", "work": "allocate"}
|
||||
self.signed({"action": "appoint", "office": "identity/oidc", "holder": "", "session": "s2", "work": "oidc"})
|
||||
rule = self.signer.proof(signing.rule_payload(secrets.token_hex(8), ["office"], "", "*", "*", int(time.time()) + 600), ORIGIN)
|
||||
outside = self.council.parent / "outside"
|
||||
(outside / "gui").mkdir(parents=True)
|
||||
cases = (
|
||||
("bad signature", appoint, self.proof([appoint], key(-7, "intruder")), "no valid operator signature"),
|
||||
("missing signature", appoint, None, "no signature"),
|
||||
("rule instead of approval", appoint, rule, "no valid operator signature"),
|
||||
("other item signed", appoint, self.proof([{**appoint, "session": "s2"}], self.signer), "does not list this item"),
|
||||
("holder changed", {**appoint, "office": "identity/oidc"}, self.proof([{**appoint, "office": "identity/oidc"}], self.signer), "holder changed"),
|
||||
)
|
||||
for name, item, proof, reason in cases:
|
||||
with self.subTest(name=name):
|
||||
before = self.files()
|
||||
result = self.run_job([item], proof)
|
||||
self.assertEqual([entry["status"] for entry in result["items"]], ["refused"])
|
||||
self.assertIn(reason, result["items"][0]["reason"])
|
||||
self.assertEqual(self.files(), before)
|
||||
(self.council / "security").symlink_to(outside)
|
||||
linked = {**appoint, "office": "security/gui"}
|
||||
result = self.signed(linked)
|
||||
self.assertEqual(result["items"][0]["status"], "refused")
|
||||
self.assertIn("link", result["items"][0]["reason"])
|
||||
self.assertEqual(list(outside.rglob("*")), [outside / "gui"])
|
||||
(self.council / "security").unlink()
|
||||
self.council.chmod(0o777)
|
||||
result = self.signed(appoint)
|
||||
self.assertEqual(result["items"][0]["status"], "refused")
|
||||
self.assertIn("others can write", result["items"][0]["reason"])
|
||||
self.council.chmod(0o700)
|
||||
self.assertIsNone(self.holder("chair"))
|
||||
|
||||
def test_the_runner_passes_no_flag_and_checks_the_holder_in_the_removal_itself(self):
|
||||
self.signed({"action": "appoint", "office": "chair", "holder": "", "session": "s1", "work": "allocate"})
|
||||
takeover = {"action": "takeover", "office": "chair", "holder": "s1", "session": "s2", "work": "allocate"}
|
||||
recall = {"action": "recall", "office": "chair", "holder": "s1", "session": "", "work": ""}
|
||||
self.assertNotIn("--operator", self.runner.command(takeover)[0])
|
||||
swapped = {**self.holder("chair"), "session": "s9"}
|
||||
(self.council / "chair" / "claim.json").write_text(json.dumps(swapped), encoding="utf-8")
|
||||
with mock.patch.object(host, "holder_of", return_value="s1"):
|
||||
for item in (takeover, recall):
|
||||
with self.subTest(action=item["action"]):
|
||||
result = self.signed(item)["items"][0]
|
||||
self.assertEqual(result["status"], "refused", result)
|
||||
self.assertIn("holder changed", result["reason"])
|
||||
self.assertEqual(self.holder("chair"), swapped)
|
||||
self.assertEqual(self.files(), ["chair", "chair/claim.json"])
|
||||
|
||||
def test_a_claim_council_would_not_read_is_never_reported_recalled(self):
|
||||
(self.council / "chair").mkdir()
|
||||
(self.council / "chair" / "claim.json").write_text(json.dumps({"session": "s1"}), encoding="utf-8")
|
||||
result = self.signed({"action": "recall", "office": "chair", "holder": "s1", "session": "", "work": ""})["items"][0]
|
||||
self.assertNotEqual(result["status"], "done", result)
|
||||
self.assertEqual(self.files(), ["chair", "chair/claim.json"])
|
||||
|
||||
def test_a_job_that_is_a_link_a_fifo_or_too_large_runs_nothing(self):
|
||||
appoint = {"action": "appoint", "office": "chair", "holder": "", "session": "s1", "work": "allocate"}
|
||||
linked, fifo, large = (secrets.token_hex(8) for _ in range(3))
|
||||
planted = self.council.parent / "planted.json"
|
||||
planted.write_text(json.dumps({"v": 1, "id": linked, "created": "x", "items": [appoint], "proof": self.proof([appoint], self.signer)}), encoding="utf-8")
|
||||
(self.jobs / f"{linked}.json").symlink_to(planted)
|
||||
os.mkfifo(self.jobs / f"{fifo}.json", 0o600)
|
||||
(self.jobs / f"{large}.json").write_text(" " * (host.READ_LIMIT + 1), encoding="utf-8")
|
||||
self.assertEqual(host.cycle(self.jobs, self.results, self.runner), 3)
|
||||
for job_id in (linked, fifo, large):
|
||||
with self.subTest(job=job_id):
|
||||
result = json.loads((self.results / f"{job_id}.json").read_text(encoding="utf-8"))
|
||||
self.assertEqual(result["items"], [])
|
||||
self.assertIn("unreadable job", result["error"])
|
||||
self.assertEqual((list(self.jobs.iterdir()), self.files()), ([], []))
|
||||
self.assertTrue(planted.is_file())
|
||||
|
||||
def test_a_signature_runs_once_and_a_malformed_job_nothing(self):
|
||||
appoint = {"action": "appoint", "office": "chair", "holder": "", "session": "s1", "work": "allocate"}
|
||||
proof = self.proof([appoint], self.signer)
|
||||
self.assertEqual(self.run_job([appoint], proof)["items"][0]["status"], "done")
|
||||
self.signed({"action": "recall", "office": "chair", "holder": "s1", "session": "", "work": ""})
|
||||
again = self.run_job([appoint], proof)
|
||||
self.assertEqual(again["items"][0]["status"], "refused")
|
||||
self.assertIn("already used", again["items"][0]["reason"])
|
||||
for name, items in (
|
||||
("command smuggled in", [{**appoint, "command": "rm -rf ~"}]),
|
||||
("climbing office", [{**appoint, "office": "../x"}]),
|
||||
("unknown action", [{**appoint, "action": "fire"}]),
|
||||
("option as session", [{**appoint, "session": "--all"}]),
|
||||
("two lines of work", [{**appoint, "work": "a\nb"}]),
|
||||
("recall with work", [{"action": "recall", "office": "chair", "holder": "s1", "session": "", "work": "x"}]),
|
||||
("no items", []),
|
||||
):
|
||||
with self.subTest(name=name):
|
||||
result = self.run_job(items, None)
|
||||
self.assertEqual(result["items"], [])
|
||||
self.assertIn("malformed job", result["error"])
|
||||
self.assertIsNone(self.holder("chair"))
|
||||
|
||||
def test_the_unit_runs_the_script_once_over_the_job_directory(self):
|
||||
appoint = {"action": "appoint", "office": "chair", "holder": "", "session": "s1", "work": "allocate"}
|
||||
job_id = secrets.token_hex(8)
|
||||
(self.jobs / f"{job_id}.json").write_text(json.dumps({"v": 1, "id": job_id, "created": "x", "items": [appoint], "proof": self.proof([appoint], self.signer)}), encoding="utf-8")
|
||||
(self.jobs / "stray.tmp").write_text("x", encoding="utf-8")
|
||||
trust = ["--approvers", str(self.approvers), "--replay", str(self.trust.replay)]
|
||||
command = [sys.executable, str(OFFICES), "--port", ORIGIN.rsplit(":", 1)[1], "--jobs", str(self.jobs), "--results", str(self.results), *trust, "--council", str(self.council), "--script", str(COUNCIL_SCRIPT)]
|
||||
done = subprocess.run(command, capture_output=True, text=True, timeout=60, check=False, env={"PATH": "/usr/bin:/bin", "HOME": str(self.home)})
|
||||
self.assertEqual(done.returncode, 0, done.stderr)
|
||||
self.assertEqual(list(self.jobs.iterdir()), [])
|
||||
result = json.loads((self.results / f"{job_id}.json").read_text(encoding="utf-8"))
|
||||
self.assertEqual(result["items"][0]["status"], "done", result)
|
||||
self.assertEqual((self.results / f"{job_id}.json").stat().st_mode & 0o777, 0o600)
|
||||
self.assertEqual(self.holder("chair")["session"], "s1")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -175,7 +175,7 @@ class TestHubSpeech(HubCase):
|
||||
args = {
|
||||
"--bind": "127.0.0.1", "--listen-port": "0", "--public-port": "1", "--council": str(self.council), "--pages": str(self.pages), "--state": str(self.state),
|
||||
"--host-state": str(HOST_STATE), "--skills": str(self.outside), "--transcripts": str(self.projects), "--templates": str(HUB / "templates"),
|
||||
"--tree-file": str(self.tree_file), "--timers": str(self.timers), "--vendor": str(HUB / "vendor"), "--cleanup": str(self.cleanup),
|
||||
"--tree-file": str(self.tree_file), "--timers": str(self.timers), "--vendor": str(HUB / "vendor"), "--cleanup": str(self.cleanup), "--offices": str(self.offices),
|
||||
"--llm-key-file": os.devnull, "--llm-url": "", "--llm-model": "",
|
||||
"--piper": str(self.engines.piper), "--voices": str(self.engines.voices), "--operator-voice": "en_US-test-high:9", "--whisper": str(self.engines.whisper),
|
||||
"--whisper-model": str(self.engines.whisper_model), "--scratch": str(self.engines.scratch), "--approvers": str(self.approvers),
|
||||
@@ -185,7 +185,7 @@ class TestHubSpeech(HubCase):
|
||||
argv = [part for pair in given.items() for part in pair]
|
||||
return subprocess.run([sys.executable, str(HUB / "server.py"), *argv], capture_output=True, text=True, timeout=60, check=False)
|
||||
|
||||
for name in ("--piper", "--voices", "--operator-voice", "--whisper", "--whisper-model", "--scratch", "--approvers", "--vendor", "--cleanup"):
|
||||
for name in ("--piper", "--voices", "--operator-voice", "--whisper", "--whisper-model", "--scratch", "--approvers", "--vendor", "--cleanup", "--offices"):
|
||||
with self.subTest(name=name):
|
||||
done = run({key: value for key, value in args.items() if key != name})
|
||||
self.assertEqual(done.returncode, 2, done.stderr)
|
||||
|
||||
+23
-12
@@ -68,10 +68,10 @@ class TestHubState(HubCase):
|
||||
self.assertEqual(self.status(review_id), "pending")
|
||||
|
||||
def test_state_is_empty_without_council_files(self):
|
||||
empty = {"reviews": [], "operator": [], "register": [], "radio": [], "claims": [], "grants": []}
|
||||
empty = {"reviews": [], "operator": [], "register": [], "radio": [], "claims": [], "grants": [], "agent_order": []}
|
||||
state = self.snapshot()
|
||||
self.assertEqual({key: state[key] for key in empty}, empty)
|
||||
self.assertEqual([(node["id"], node["kind"]) for node in state["graph"]["nodes"]], [("Operator", "operator"), ("name:Maintainer", "chair")])
|
||||
self.assertEqual([(node["id"], node["kind"]) for node in state["graph"]["nodes"]], [("Operator", "operator")])
|
||||
self.assertEqual(state["graph"]["edges"], [])
|
||||
self.index()
|
||||
self.write_council({"rasci.json": "{not json", "queue-history.jsonl": "[" * 100000 + "\n", "work/x/claim.json": "[1]"})
|
||||
@@ -129,6 +129,8 @@ class TestHubState(HubCase):
|
||||
]
|
||||
claim = {"office": "work/hub", "session": "s1", "runtime": "claude", "name": "Skills", "since": "2026-11-05T10:00:00+00:00", "beat": now, "work": "build the hub"}
|
||||
gate = {"office": "cluster/gate", "session": "s2", "runtime": "codex", "name": "Gatekeeper", "since": "2026-11-05T09:00:00+00:00", "beat": now, "work": "gate"}
|
||||
chair = {"office": "chair", "session": "s3", "runtime": "claude-code", "name": "Maintainer", "since": "2026-11-05T08:00:00+00:00", "beat": now, "work": "allocate"}
|
||||
line = {"office": "identity/oidc", "session": "s1", "runtime": "claude", "name": "Skills", "since": "2026-11-05T10:00:00+00:00", "beat": now, "work": "oidc line"}
|
||||
history = [
|
||||
{"id": "q1", "kind": "gate", "signature": "make test", "workdir": "/repo/.worktrees/hub/sub", "start": now - 15, "end": now - 5, "exit": 0, "minutes": 0.2},
|
||||
{"id": "q2", "kind": "deploy", "signature": "make deploy", "workdir": "/elsewhere", "start": now - 15, "end": now - 5, "exit": 0, "minutes": 0.2},
|
||||
@@ -139,6 +141,8 @@ class TestHubState(HubCase):
|
||||
"news.jsonl": self.jsonl(news),
|
||||
"work/hub/claim.json": claim,
|
||||
"cluster/gate/claim.json": gate,
|
||||
"chair/claim.json": chair,
|
||||
"identity/oidc/claim.json": line,
|
||||
"work/broken/claim.json": "not json",
|
||||
"queue-history.jsonl": self.jsonl(history),
|
||||
})
|
||||
@@ -147,7 +151,7 @@ class TestHubState(HubCase):
|
||||
set(state),
|
||||
{
|
||||
"now", "since", "until", "first", "alarm_since", "reviews", "asks", "decisions", "messages", "operator",
|
||||
"register", "priority", "radio", "claims", "grants", "night", "silence", "graph",
|
||||
"register", "agent_order", "radio", "claims", "grants", "night", "silence", "graph",
|
||||
"inbox", "pending", "rules", "approvers", "jobs", "views", "llm", "people", "settings", "media", "speech",
|
||||
"badges", "kinds",
|
||||
},
|
||||
@@ -168,7 +172,8 @@ class TestHubState(HubCase):
|
||||
"limit": phrasing.SPEECH_LIMIT, "rate": engines.STT_RATE, "seconds": engines.STT_SECONDS,
|
||||
})
|
||||
self.assertAlmostEqual(state["now"] - state["since"], view.RADIO_WINDOW)
|
||||
self.assertEqual((state["night"], state["silence"], state["priority"]), ("day", None, ["Skills session", "subagent general-purpose (default)"]))
|
||||
self.assertEqual((state["night"], state["silence"]), ("day", None))
|
||||
self.assertEqual(state["agent_order"], ["Skills session", "subagent general-purpose (default)", "Gatekeeper", "Maintainer", "Skills"])
|
||||
radio = [(row["from"], row["to"], row["class"], row["text"]) for row in state["radio"]]
|
||||
self.assertEqual(radio, [
|
||||
("Skills", ["Maintainer"], "post", "ready"),
|
||||
@@ -178,12 +183,17 @@ class TestHubState(HubCase):
|
||||
("Docs", ["Skills"], "pan-pan", "PAN-PAN slow"),
|
||||
("Maintainer", ["Skills", "Docs"], "security", "rotate"),
|
||||
])
|
||||
self.assertEqual([(row["office"], row["agent"], row["runtime"]) for row in state["claims"]], [("cluster/gate", "Gatekeeper", "codex"), ("work/hub", "Skills", "claude")])
|
||||
self.assertEqual([(row["office"], row["agent"], row["runtime"], row["kind"], row["dimension"], row["line"]) for row in state["claims"]], [
|
||||
("chair", "Maintainer", "claude-code", "chair", "", ""),
|
||||
("cluster/gate", "Gatekeeper", "codex", "cluster", "", ""),
|
||||
("identity/oidc", "Skills", "claude", "line", "identity", "oidc"),
|
||||
("work/hub", "Skills", "claude", "work", "", ""),
|
||||
])
|
||||
self.assertEqual([(row["to"], row["kind"], row["id"]) for row in state["grants"]], [("Skills", "gate", "q1")])
|
||||
self.assertEqual([row["task"] for row in state["operator"]], ["operator/push"])
|
||||
nodes = {node["id"]: node for node in state["graph"]["nodes"]}
|
||||
moon = "subagent general-purpose (default) @ s1"
|
||||
self.assertEqual(list(nodes), ["Operator", "name:Maintainer", "name:Docs", "s2", "s1", moon])
|
||||
self.assertEqual(list(nodes), ["Operator", "s3", "name:Docs", "s2", "s1", moon])
|
||||
self.assertEqual([nodes[key]["kind"] for key in nodes], ["operator", "chair", "session", "session", "session", "subagent"])
|
||||
self.assertEqual([nodes[key]["label"] for key in nodes][:5], ["Operator", "Maintainer", "Docs", "Gatekeeper", "Skills"])
|
||||
self.assertEqual((nodes[moon]["parent"], nodes[moon]["label"]), ("s1", "subagent general-purpose (default)"))
|
||||
@@ -191,17 +201,18 @@ class TestHubState(HubCase):
|
||||
self.assertEqual(skills, [("operator/push", False, "A"), ("work/hub", True, "R"), ("work/sub", False, "A")])
|
||||
self.assertEqual([(task["task"], task["letters"]) for task in nodes["Operator"]["tasks"]], [("operator/push", "R")])
|
||||
self.assertEqual([task["task"] for task in nodes[moon]["tasks"]], ["work/sub"])
|
||||
self.assertEqual([claim["office"] for claim in nodes["s1"]["claims"]], ["work/hub"])
|
||||
self.assertEqual([claim["office"] for claim in nodes["s1"]["claims"]], ["identity/oidc", "work/hub"])
|
||||
self.assertEqual([claim["office"] for claim in nodes["s3"]["claims"]], ["chair"])
|
||||
self.assertEqual((state["people"]["s1"]["now"], state["radio"][0]["source"], state["grants"][0]["target"]), ("Skills", "s1", "s1"))
|
||||
edges = {(edge["source"], edge["target"], edge["class"]): edge["count"] for edge in state["graph"]["edges"]}
|
||||
self.assertEqual(edges, {
|
||||
("name:Docs", "*", "mayday"): 1,
|
||||
("name:Docs", "s1", "pan-pan"): 1,
|
||||
("name:Maintainer", "*", "newsletter"): 1,
|
||||
("name:Maintainer", "name:Docs", "security"): 1,
|
||||
("name:Maintainer", "s1", "grant"): 1,
|
||||
("name:Maintainer", "s1", "security"): 1,
|
||||
("s1", "name:Maintainer", "post"): 2,
|
||||
("s3", "*", "newsletter"): 1,
|
||||
("s3", "name:Docs", "security"): 1,
|
||||
("s3", "s1", "grant"): 1,
|
||||
("s3", "s1", "security"): 1,
|
||||
("s1", "s3", "post"): 2,
|
||||
})
|
||||
|
||||
def test_state_needs_the_host(self):
|
||||
|
||||
@@ -13,6 +13,7 @@ from pathlib import Path
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
SCRIPT = REPO_ROOT / "scripts" / "orchestrator.sh"
|
||||
HOST_SCRIPTS = ("kvb-org-organize/scripts/council.py", "kvb-org-organize/scripts/nofollow.py", "kvb-org-rasci/scripts/rasci.py")
|
||||
OLD = "kvb-hub"
|
||||
NEW = "kvb-orchestrator"
|
||||
STUB = r'''#!/usr/bin/env python3
|
||||
@@ -97,6 +98,8 @@ class TestOrchestratorMigration(unittest.TestCase):
|
||||
self.other = {"matcher": "Bash", "hooks": [{"type": "command", "command": "other-hook"}]}
|
||||
legacy = {"matcher": "Bash", "hooks": [{"type": "command", "command": f"python3 {share}/{OLD}/client/permission_hook.py --port 8737"}]}
|
||||
self.write(self.home / ".claude" / "settings.json", json.dumps({"model": "x", "hooks": {"PermissionRequest": [self.other, legacy], "Stop": [self.other]}}), 0o640)
|
||||
for script in HOST_SCRIPTS:
|
||||
self.write(self.home / ".claude" / "skills" / script, (REPO_ROOT / "skills" / script).read_text(encoding="utf-8"))
|
||||
|
||||
def write(self, path: Path, text: str, mode: int = 0o644) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
@@ -127,7 +130,7 @@ class TestOrchestratorMigration(unittest.TestCase):
|
||||
self.assertIn(f"{share / NEW}/client/permission_hook.py", hooks[1]["hooks"][0]["command"])
|
||||
self.assertEqual(settings.stat().st_mode & 0o777, 0o640)
|
||||
self.assertIn(f"{OLD}/client", (self.home / ".claude" / f"settings.json.before-{NEW}-migration").read_text(encoding="utf-8"))
|
||||
self.assertEqual((first["images"], first["mcp"], first["enabled"]), ([f"{NEW}:local"], {NEW: "http://127.0.0.1:8737/mcp"}, [f"{NEW}-runner.service"]))
|
||||
self.assertEqual((first["images"], first["mcp"], first["enabled"]), ([f"{NEW}:local"], {NEW: "http://127.0.0.1:8737/mcp"}, [f"{NEW}-offices.path", f"{NEW}-runner.service"]))
|
||||
self.assertIn(["docker", "compose", "--file", str(share / OLD / "compose.yml"), "down", "--remove-orphans"], first["calls"])
|
||||
|
||||
before = snapshot(self.home)
|
||||
@@ -137,6 +140,15 @@ class TestOrchestratorMigration(unittest.TestCase):
|
||||
legacy_calls = [call for call in second["calls"][len(first["calls"]) :] if any(OLD in arg for arg in call)]
|
||||
self.assertEqual(legacy_calls, [["docker", "image", "inspect", f"{OLD}:local"], ["claude", "mcp", "get", OLD]])
|
||||
|
||||
def test_the_offices_unit_waits_for_the_installed_skill_scripts_of_this_tree(self):
|
||||
self.install()
|
||||
stale = self.home / ".claude" / "skills" / HOST_SCRIPTS[2]
|
||||
stale.write_text(stale.read_text(encoding="utf-8") + "\n", encoding="utf-8")
|
||||
done = subprocess.run(["bash", str(SCRIPT), "install"], env=self.env, capture_output=True, text=True, timeout=120, check=False)
|
||||
self.assertEqual(done.returncode, 0, done.stderr)
|
||||
self.assertIn("run 'make install' to enable", done.stderr)
|
||||
self.assertNotIn(f"{NEW}-offices.path", json.loads(self.state.read_text(encoding="utf-8"))["enabled"])
|
||||
|
||||
def test_install_ships_no_bytecode_from_the_tree(self):
|
||||
planted = REPO_ROOT / "orchestrator" / "client" / "__pycache__" / "planted.cpython-0.pyc"
|
||||
planted.parent.mkdir(exist_ok=True)
|
||||
|
||||
+10
-1
@@ -27,6 +27,7 @@ BASE = {
|
||||
"silenced": False,
|
||||
"slot_limits": {"test": 5, "deploy": 1},
|
||||
"ranks": {"work/a": 0, "work/b": 1, "work/c": 2},
|
||||
"agents": {},
|
||||
"now": NOW,
|
||||
}
|
||||
CHILD = (
|
||||
@@ -62,7 +63,7 @@ def history_row(seq: int, **fields: object) -> dict:
|
||||
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,
|
||||
"workdir": f"/w{seq}", "name": "s1", "status": "waiting", "enqueued": NOW, **fields,
|
||||
}
|
||||
|
||||
|
||||
@@ -98,6 +99,14 @@ class TestEvaluate(unittest.TestCase):
|
||||
tied = job(4, task="work/c", enqueued=NOW - 2 * HOUR)
|
||||
self.assertEqual(list(self.decide([tied, fresh])), ["j1", "j4"])
|
||||
|
||||
def test_the_agent_order_breaks_ties_of_equal_rank_only(self):
|
||||
entries = [job(1, name="Beta"), job(2, name="Alpha"), job(3, task="work/b", name="Alpha"), job(4, name="Stranger")]
|
||||
self.assertEqual(list(self.decide(entries)), ["j1", "j2", "j4", "j3"])
|
||||
self.assertEqual(list(self.decide(entries, agents={"Alpha": 0, "Beta": 1})), ["j2", "j1", "j4", "j3"])
|
||||
tied = [job(5, task="work/b", name="Beta", enqueued=NOW - HOUR), job(6, name="Alpha")]
|
||||
self.assertEqual(list(self.decide(tied)), ["j5", "j6"])
|
||||
self.assertEqual(list(self.decide(tied, agents={"Alpha": 0, "Beta": 1})), ["j6", "j5"])
|
||||
|
||||
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})
|
||||
|
||||
+65
-11
@@ -36,7 +36,7 @@ class TestRasci(unittest.TestCase):
|
||||
json.dumps({"sessionId": sid, "name": name}), encoding="utf-8"
|
||||
)
|
||||
|
||||
def _run(self, *args: str, script: Path = RASCI, sid: str = "s1", timeout: float | None = None) -> subprocess.CompletedProcess:
|
||||
def _run(self, *args: str, script: Path = RASCI, sid: str = "s1", timeout: float = 120) -> subprocess.CompletedProcess:
|
||||
env = {
|
||||
"PATH": "/usr/bin:/bin",
|
||||
"HOME": str(self.home),
|
||||
@@ -312,28 +312,82 @@ class TestRasci(unittest.TestCase):
|
||||
self.assertEqual([line.split("\t")[1] for line in self._out(*args)],
|
||||
["work/a"] if args == ("artifacts",) else [])
|
||||
|
||||
def test_order_pins_the_agents_in_the_operators_order(self):
|
||||
def _ranked(self) -> list[str]:
|
||||
return [line.split(" | ")[2].split(":")[0] for line in self._out("show")[2:] if line.startswith("| ")]
|
||||
|
||||
def test_agents_order_breaks_ties_only_and_never_touches_the_task_pins(self):
|
||||
self._run("set", "work/a1", "--worker", "Alpha", "--prio", "1")
|
||||
self._run("set", "work/a2", "--worker", "Alpha", "--prio", "2")
|
||||
self._run("set", "work/b1", "--worker", "Beta", "--prio", "3")
|
||||
self._run("set", "work/b1", "--worker", "Beta", "--prio", "1")
|
||||
self._run("set", "work/b2", "--worker", "Beta", "--prio", "2")
|
||||
self._run("set", "work/c1", "--worker", "Gamma")
|
||||
self._run("estimate", "work/c1", "One", "--value", "8", "--complexity", "2")
|
||||
self._run("set", "work/d1", "--worker", "Delta")
|
||||
self._claim("security/gui", "s9", "Epsilon", "line")
|
||||
self.assertEqual(
|
||||
self._out("agents"),
|
||||
["1\tAlpha\t2\twork/a1, work/a2", "2\tBeta\t1\twork/b1", "3\tGamma\t1\twork/c1"],
|
||||
["1\tAlpha\t1\twork/a1", "2\tBeta\t2\twork/b1, work/b2", "3\tGamma\t1\twork/c1", "4\tDelta\t1\twork/d1", "5\tEpsilon\t0\t-"],
|
||||
)
|
||||
self.assertEqual(self._ranked(), ["work/a1", "work/b1", "work/b2", "work/c1", "work/d1"])
|
||||
register = (self.root / "rasci.json").read_text(encoding="utf-8")
|
||||
self.assertEqual(
|
||||
self._out("order", "Gamma, Beta"), ["pinned 2 tasks of 2 agents; the others rank by WSJF after them"]
|
||||
self._out("agents", "order", "Gamma, Beta,Epsilon"),
|
||||
["agent order: Gamma, Beta, Epsilon; the others follow by register rank; task --prio pins unchanged"],
|
||||
)
|
||||
self.assertEqual((self.root / "rasci.json").read_text(encoding="utf-8"), register)
|
||||
stored = json.loads((self.root / "agent-order.json").read_text(encoding="utf-8"))
|
||||
self.assertEqual(stored["agents"], ["Gamma", "Beta", "Epsilon"])
|
||||
self.assertEqual(
|
||||
self._out("agents"),
|
||||
["1\tGamma\t1\twork/c1", "2\tBeta\t1\twork/b1", "3\tAlpha\t2\twork/a1, work/a2"],
|
||||
["1\tGamma\t1\twork/c1", "2\tBeta\t2\twork/b1, work/b2", "3\tEpsilon\t0\t-", "4\tAlpha\t1\twork/a1", "5\tDelta\t1\twork/d1"],
|
||||
)
|
||||
self.assertTrue(self._out("show")[2].startswith("| 1 | 4 | work/c1 |"))
|
||||
self.assertNotIn("prio: 1", self._out("get", "work/a1"))
|
||||
for args in (("order", "Gamma,Nobody"), ("order", "Beta,Beta"), ("order", " , ")):
|
||||
self.assertEqual(self._ranked(), ["work/b1", "work/a1", "work/b2", "work/c1", "work/d1"])
|
||||
self.assertIn("prio: 1", self._out("get", "work/a1"))
|
||||
for args in (("agents", "order", "Gamma,Nobody"), ("agents", "order", "Beta,Beta"), ("agents", "order", " , "), ("agents", "sort")):
|
||||
with self.subTest(args=args):
|
||||
self.assertEqual(self._run(*args).returncode, 2)
|
||||
self.assertEqual(json.loads((self.root / "agent-order.json").read_text(encoding="utf-8")), stored)
|
||||
|
||||
def test_the_old_order_command_stores_the_agent_order_and_says_so(self):
|
||||
self._run("set", "work/a1", "--worker", "Alpha", "--prio", "3")
|
||||
self._run("set", "work/b1", "--worker", "Beta")
|
||||
done = self._run("order", "Beta,Alpha")
|
||||
self.assertEqual(done.returncode, 0, done.stderr)
|
||||
self.assertIn("rasci: `order` is now `agents order`", done.stderr)
|
||||
self.assertIn("no longer writes --prio pins", done.stderr)
|
||||
self.assertEqual(json.loads((self.root / "agent-order.json").read_text(encoding="utf-8"))["agents"], ["Beta", "Alpha"])
|
||||
self.assertIn("prio: 3", self._out("get", "work/a1"))
|
||||
self.assertNotIn("prio: 1", self._out("get", "work/b1"))
|
||||
|
||||
def test_a_broken_agent_order_file_ranks_like_none(self):
|
||||
self._run("set", "work/a1", "--worker", "Alpha")
|
||||
self._run("set", "work/b1", "--worker", "Beta")
|
||||
for text in ("{not json", json.dumps({"agents": "Beta"}), json.dumps({"agents": [1, "Beta"]}), json.dumps([1])):
|
||||
with self.subTest(text=text):
|
||||
(self.root / "agent-order.json").write_text(text, encoding="utf-8")
|
||||
self.assertEqual(self._ranked(), ["work/a1", "work/b1"])
|
||||
|
||||
def test_the_agent_order_never_reads_or_writes_through_a_link_fifo_or_deep_nesting(self):
|
||||
self._run("set", "work/a1", "--worker", "Alpha")
|
||||
self._run("set", "work/b1", "--worker", "Beta")
|
||||
outside = Path(self.tmp.name) / "outside.json"
|
||||
outside.write_text(json.dumps({"agents": ["Beta"]}), encoding="utf-8")
|
||||
order = self.root / "agent-order.json"
|
||||
order.symlink_to(outside)
|
||||
self.assertEqual(self._ranked(), ["work/a1", "work/b1"])
|
||||
order.unlink()
|
||||
order.write_text("[" * 100000, encoding="utf-8")
|
||||
self.assertEqual(self._ranked(), ["work/a1", "work/b1"])
|
||||
order.unlink()
|
||||
os.mkfifo(order, 0o600)
|
||||
self.assertEqual(self._run("agents", timeout=20).stdout.splitlines(), ["1\tAlpha\t1\twork/a1", "2\tBeta\t1\twork/b1"])
|
||||
order.unlink()
|
||||
lock = self.root / "rasci.lock"
|
||||
lock.unlink(missing_ok=True)
|
||||
planted = Path(self.tmp.name) / "planted.lock"
|
||||
lock.symlink_to(planted)
|
||||
refused = self._run("agents", "order", "Beta,Alpha")
|
||||
self.assertEqual(refused.returncode, 2, refused.stderr)
|
||||
self.assertIn("agents order", refused.stderr)
|
||||
self.assertFalse(planted.exists() or order.exists())
|
||||
|
||||
def test_a_symlinked_register_is_refused_and_never_copied_into_the_council(self):
|
||||
outside = Path(self.tmp.name) / "outside.json"
|
||||
|
||||
Reference in new issue
Block a user