Merge feature/orchestrator/s53-agent-presence into master
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> # Conflicts: # docs/requirements/001-agent-orchestrator.md # orchestrator/templates/page-board.js # tests/e2e/test_orchestrator_e2e.py
This commit is contained in:
commit
afebf24703
24 files changed
+798
-13
No files matched your search
@@ -0,0 +1,147 @@
|
||||
"""Pin decision 61: each agent state from the host's session snapshot, the night state, radio receipts and claim beats."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
import hub_support # noqa: F401
|
||||
import sessions as client
|
||||
from kvb_orchestrator.presence import SNAPSHOT_STALE, presence, read_snapshot
|
||||
|
||||
NOW = 1_800_000_000.0
|
||||
FRESH = int((NOW - 60) * 1000)
|
||||
OLD = int((NOW - 1800) * 1000)
|
||||
|
||||
|
||||
def record(sid: str, status: str, updated: int, name: str) -> dict:
|
||||
return {"status": status, "statusUpdatedAt": updated, "name": name, "running": sid != "gone"}
|
||||
|
||||
|
||||
def claim(session: str, name: str, beat: float | None) -> dict:
|
||||
return {"session": session, "agent": name, "beat": beat}
|
||||
|
||||
|
||||
class TestPresence(unittest.TestCase):
|
||||
def states(self, sessions: dict[str, dict], parts: dict[str, str], mode: str, claims: list[dict], news: list[dict]) -> dict[str, str]:
|
||||
return presence(sessions, parts, mode, claims, news, NOW)
|
||||
|
||||
def test_each_state_by_day_and_by_night(self):
|
||||
sessions = {sid: record(sid, status, updated, name) for sid, status, updated, name in (
|
||||
("work", "busy", FRESH, "Worker"),
|
||||
("idle", "idle", OLD, "Idler"),
|
||||
("gone", "idle", FRESH, "Ghost"),
|
||||
("stuck", "busy", OLD, "Stuck"),
|
||||
("called", "idle", OLD, "Callee"),
|
||||
("answered", "idle", OLD, "Answerer"),
|
||||
("sleep", "idle", FRESH, "Sleeper"),
|
||||
("park", "busy", FRESH, "Parker"),
|
||||
)}
|
||||
call = NOW - 700
|
||||
news = [
|
||||
{"ts": call, "from": "Operator", "to": ["Callee"], "class": "call"},
|
||||
{"ts": call, "from": "Operator", "to": ["Answerer"], "class": "call"},
|
||||
{"ts": call + 2, "from": "Answerer", "to": ["Operator"], "class": "receipt", "call": call},
|
||||
{"ts": NOW - 60, "from": "Operator", "to": ["Idler"], "class": "call"},
|
||||
]
|
||||
claims = [claim("codex-on", "Codex", NOW - 60), claim("codex-off", "Codex old", NOW - 3600), claim("-", "Nobody", NOW), claim("op", "Operator", NOW)]
|
||||
day = self.states(sessions, {"sleep": "suspend", "park": "drain"}, "day", claims, news)
|
||||
self.assertEqual(day, {
|
||||
"work": "working", "idle": "alive", "gone": "dead", "stuck": "unreachable", "called": "unreachable",
|
||||
"answered": "alive", "sleep": "sleeping", "park": "suspending", "codex-on": "alive", "codex-off": "dead",
|
||||
})
|
||||
night = self.states(sessions, {"idle": "day", "work": "suspend"}, "drain", [], [])
|
||||
self.assertEqual({sid: night[sid] for sid in ("work", "idle", "called", "sleep", "park")}, {
|
||||
"work": "suspending", "idle": "alive", "called": "sleeping", "sleep": "sleeping", "park": "suspending",
|
||||
})
|
||||
|
||||
def test_a_status_update_after_the_call_counts_as_reached(self):
|
||||
news = [{"ts": NOW - 900, "from": "Operator", "to": ["Alias"], "class": "call"}]
|
||||
claims = [claim("later", "Alias", NOW)]
|
||||
self.assertEqual(self.states({"later": record("later", "idle", FRESH, "Later")}, {}, "day", claims, news), {"later": "alive"})
|
||||
self.assertEqual(self.states({"later": record("later", "idle", OLD, "Later")}, {}, "day", claims, news), {"later": "unreachable"})
|
||||
|
||||
def test_a_record_without_running_true_is_dead(self):
|
||||
for value in (False, None, "yes", 1):
|
||||
with self.subTest(value=value):
|
||||
entry = {**record("s", "busy", FRESH, "S"), "running": value}
|
||||
self.assertEqual(self.states({"s": entry}, {}, "day", [], []), {"s": "dead"})
|
||||
|
||||
|
||||
class TestSnapshot(unittest.TestCase):
|
||||
def test_the_host_client_keeps_name_status_and_whether_the_process_runs(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
base = Path(tmp)
|
||||
proc, folder = base / "proc", base / "sessions"
|
||||
(proc / "4242").mkdir(parents=True)
|
||||
folder.mkdir()
|
||||
(proc / "4242" / "stat").write_text("4242 (claude (main)) S " + " ".join(str(n) for n in range(1, 19)) + " 777 0 0\n", encoding="utf-8")
|
||||
live = {"sessionId": "s1", "pid": 4242, "procStart": "777", "status": "busy", "statusUpdatedAt": FRESH, "name": "One", "cwd": "/secret"}
|
||||
(folder / "1.json").write_text(json.dumps(live), encoding="utf-8")
|
||||
(folder / "2.json").write_text(json.dumps({**live, "sessionId": "s2", "procStart": "778"}), encoding="utf-8")
|
||||
(folder / "3.json").write_text(json.dumps({"status": "idle"}), encoding="utf-8")
|
||||
(folder / "4.json").write_text("{broken", encoding="utf-8")
|
||||
(folder / "5.json").write_text(json.dumps(["s5"]), encoding="utf-8")
|
||||
output = base / "agent-sessions.json"
|
||||
client.write(output, client.snapshot(folder, proc, NOW))
|
||||
kept = {"status": "busy", "statusUpdatedAt": FRESH, "name": "One"}
|
||||
self.assertEqual(json.loads(output.read_text(encoding="utf-8")), {
|
||||
"written": NOW, "sessions": {"s1": {**kept, "running": True}, "s2": {**kept, "running": False}},
|
||||
})
|
||||
self.assertEqual(sorted(path.name for path in base.iterdir()), ["agent-sessions.json", "proc", "sessions"])
|
||||
self.assertEqual(client.snapshot(folder / "missing", proc, NOW), {"written": NOW, "sessions": {}})
|
||||
|
||||
def test_the_host_client_skips_hostile_records_and_leaves_no_temp_file(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
base = Path(tmp)
|
||||
proc, folder = base / "proc", base / "sessions"
|
||||
proc.mkdir()
|
||||
folder.mkdir()
|
||||
good = {"sessionId": "s1", "pid": 1, "procStart": "1", "status": "busy", "statusUpdatedAt": FRESH, "name": "One"}
|
||||
(base / "outside.json").write_text(json.dumps({**good, "sessionId": "s9"}), encoding="utf-8")
|
||||
(folder / "1.json").write_text(json.dumps({**good, "name": {"x": 1}, "status": "y" * 201, "statusUpdatedAt": True}), encoding="utf-8")
|
||||
(folder / "2.json").symlink_to(base / "outside.json")
|
||||
os.mkfifo(folder / "3.json")
|
||||
(folder / "4.json").write_text("[" * 30000 + "]" * 30000, encoding="utf-8")
|
||||
(folder / "5.json").write_text(json.dumps({**good, "sessionId": "s5", "name": "z" * client.RECORD_BYTES}), encoding="utf-8")
|
||||
self.assertEqual(client.snapshot(folder, proc, NOW), {"written": NOW, "sessions": {"s1": {"running": False}}})
|
||||
output = base / "agent-sessions.json"
|
||||
with self.assertRaises(TypeError):
|
||||
client.write(output, {"written": object()})
|
||||
self.assertEqual(sorted(path.name for path in base.iterdir()), ["outside.json", "proc", "sessions"])
|
||||
|
||||
def test_running_needs_an_int_pid_and_the_same_process_start(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
proc = Path(tmp)
|
||||
(proc / "4242").mkdir()
|
||||
(proc / "4242" / "stat").write_text("4242 (claude (main)) S " + " ".join(str(n) for n in range(1, 19)) + " 777 0 0\n", encoding="utf-8")
|
||||
entry = {"pid": 4242, "procStart": "777"}
|
||||
self.assertTrue(client.running(entry, proc))
|
||||
for bad in ({**entry, "procStart": "778"}, {**entry, "pid": 4243}, {**entry, "pid": True}, {**entry, "pid": "4242"}, {"pid": 4242}):
|
||||
with self.subTest(bad=bad):
|
||||
self.assertFalse(client.running(bad, proc))
|
||||
|
||||
def test_the_hub_reads_only_a_fresh_well_formed_snapshot(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
path = Path(tmp) / "agent-sessions.json"
|
||||
self.assertEqual(read_snapshot(path, NOW), {})
|
||||
entry = record("s1", "busy", FRESH, "One")
|
||||
for data, expected in (
|
||||
({"written": NOW - SNAPSHOT_STALE, "sessions": {"s1": entry, "s2": "x"}}, {"s1": entry}),
|
||||
({"written": NOW - SNAPSHOT_STALE - 1, "sessions": {"s1": entry}}, {}),
|
||||
({"written": True, "sessions": {"s1": entry}}, {}),
|
||||
({"written": NOW + 1, "sessions": {"s1": entry}}, {}),
|
||||
({"written": float("nan"), "sessions": {"s1": entry}}, {}),
|
||||
({"written": NOW, "sessions": ["s1"]}, {}),
|
||||
(["s1"], {}),
|
||||
):
|
||||
with self.subTest(data=data):
|
||||
path.write_text(json.dumps(data), encoding="utf-8")
|
||||
self.assertEqual(read_snapshot(path, NOW), expected)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user