forked from Keelrleek/Harbor
Messages, threads, acknowledgements, claims, bands, tasks, review items and stop flags for a team of AI agents; the chat page and board; host hooks and MCP server; autopilot; tools/watch.py. Put under version control after the weekend's changes so they have history and can be rolled back. Local only.
225 lines
13 KiB
Python
225 lines
13 KiB
Python
"""Tasks, handoffs, pins, the human's availability, corrections, due times, search filters, connections, doctor."""
|
|
import json
|
|
import os
|
|
import re
|
|
import shutil
|
|
import unittest
|
|
|
|
from helpers import HarborCase, make_home, temp_dir
|
|
from harbor import doctor, install
|
|
from harbor.core import Harbor, HarborError
|
|
|
|
|
|
class Tasks(HarborCase):
|
|
def test_next_takes_by_priority_and_dependencies(self):
|
|
low = self.h.add_task("Claude", "tidy names", priority="low")
|
|
first = self.h.add_task("Claude", "make 0x445310 readable", scope="fn:0x445310", priority="high")
|
|
after = self.h.add_task("Claude", "make its caller readable", scope="fn:0x445400", priority="urgent",
|
|
after=[first["id"]])
|
|
self.assertEqual(after["waiting_on"], [first["id"]])
|
|
took = self.h.next_task("DeepSeek")
|
|
self.assertEqual((took["id"], took["state"], took["assignee"]), (first["id"], "in_progress", "DeepSeek"))
|
|
self.assertEqual(self.h.active_claims(agent="DeepSeek")[0]["scope"], "fn:0x445310")
|
|
self.assertIn(f"task #{first['id']}", next(a for a in self.h.agents() if a["name"] == "DeepSeek")["doing"])
|
|
self.assertEqual(self.h.next_task("Codex")["id"], low["id"]) # the urgent one still waits
|
|
with self.assertRaises(HarborError):
|
|
self.h.take_task("OpenCode", after["id"])
|
|
self.h.update_task("DeepSeek", first["id"], "done", note="landed in abc")
|
|
self.assertEqual(self.h.active_claims(agent="DeepSeek"), []) # claim released
|
|
self.assertIsNone(next(a for a in self.h.agents() if a["name"] == "DeepSeek")["doing"])
|
|
self.assertEqual(self.h.next_task("OpenCode")["id"], after["id"])
|
|
self.assertIn("is done", self.h.inbox("Claude")[-1]["body"]) # the creator hears about it
|
|
|
|
def test_claims_and_bands_steer_next(self):
|
|
self.h.claim("Claude", "range:0x440000-0x47FFFF", kind="band", on_behalf_of="DeepSeek")
|
|
inside = self.h.add_task("Claude", "inside DeepSeek's band", scope="fn:0x450000")
|
|
held = self.h.add_task("Claude", "already drafted", scope="fn:0x490000")
|
|
self.h.claim("OpenCode", "fn:0x490000")
|
|
self.assertIsNone(self.h.next_task("Codex"))
|
|
self.assertEqual(self.h.next_task("DeepSeek")["id"], inside["id"])
|
|
self.assertEqual(self.h.next_task("OpenCode", take=False)["id"], held["id"])
|
|
assigned = self.h.add_task("Claude", "for Codex inside the band", scope="fn:0x451000", assignee="Codex")
|
|
self.assertEqual(self.h.next_task("Codex")["id"], assigned["id"]) # explicit assignment wins
|
|
self.assertEqual(self.h.inbox("Codex")[-1]["refs"]["task"], assigned["id"])
|
|
self.h.update_task("Codex", assigned["id"], "open", note="later") # handed back for now
|
|
self.assertEqual(self.h.task(assigned["id"])[0]["assignee"], "Codex") # still the coordinator's choice
|
|
self.assertEqual(self.h.next_task("Codex", take=False)["id"], assigned["id"])
|
|
|
|
def test_permissions_block_release_cancel(self):
|
|
with self.assertRaises(HarborError):
|
|
self.h.add_task("Codex", "for someone else", assignee="Kimi")
|
|
mine = self.h.add_task("Codex", "my own", assignee="Codex")
|
|
self.h.take_task("Codex", mine["id"])
|
|
with self.assertRaises(HarborError):
|
|
self.h.update_task("Codex", mine["id"], "blocked") # say why
|
|
with self.assertRaises(HarborError):
|
|
self.h.update_task("Kimi", mine["id"], "done")
|
|
self.h.update_task("Codex", mine["id"], "blocked", note="needs 0x4E2680 named first")
|
|
self.assertTrue(any(a["kind"] == "task_blocked" for a in self.h.attention()))
|
|
self.assertIn("blocked", self.h.inbox("Claude")[-1]["body"])
|
|
self.h.update_task("Codex", mine["id"], "open")
|
|
self.assertIsNone(self.h.task(mine["id"])[0]["assignee"]) # handed back
|
|
self.h.update_task("Claude", mine["id"], "cancelled", note="no longer needed")
|
|
self.h.update_task("Codex", mine["id"], "open") # the creator may reopen
|
|
with self.assertRaises(HarborError):
|
|
self.h.update_task("Codex", mine["id"], "review")
|
|
web = self.h.add_task("User", "from the board", origin="web", priority="urgent")
|
|
self.assertEqual(web["created_by"], "User")
|
|
with self.assertRaises(HarborError):
|
|
self.h.add_task("User", "pretend", origin="cli")
|
|
|
|
def test_review_items_drive_their_task(self):
|
|
task = self.h.next_task("DeepSeek") if False else self.h.add_task("Claude", "make 0x445310 readable",
|
|
scope="fn:0x445310")
|
|
self.h.take_task("DeepSeek", task["id"])
|
|
item = self.h.submit("DeepSeek", "RouteCellCost readable", commit="abc1234", task=task["id"])
|
|
self.assertEqual(self.h.task(task["id"])[0]["state"], "review")
|
|
self.assertEqual(self.h.item(item["id"])[0]["scope"], "fn:0x445310") # scope comes from the task
|
|
self.h.transition("Claude", item["id"], "changes_requested", note="signed read")
|
|
self.assertEqual(self.h.task(task["id"])[0]["state"], "in_progress")
|
|
self.h.transition("DeepSeek", item["id"], "submitted", commit="def5678")
|
|
self.assertEqual(self.h.task(task["id"])[0]["state"], "review")
|
|
self.h.transition("Claude", item["id"], "approved")
|
|
self.h.transition("Claude", item["id"], "landed", commit="b4b44f0")
|
|
done = self.h.task(task["id"])[0]
|
|
self.assertEqual((done["state"], done["item_id"]), ("done", item["id"]))
|
|
self.assertEqual(self.h.active_claims(agent="DeepSeek"), [])
|
|
with self.assertRaises(HarborError):
|
|
self.h.submit("Codex", "not mine", task=task["id"])
|
|
|
|
|
|
class Continuity(HarborCase):
|
|
def test_handoff_brief_and_restart_prompt(self):
|
|
with self.assertRaises(HarborError):
|
|
self.h.handoff("Kimi", " ")
|
|
self.h.handoff("Kimi", "Reviewed batch 48.\nIn flight: 0x406F60 VisitTavern, half the strings checked.\nNext: batch 49.")
|
|
brief = self.h.brief("Kimi")
|
|
self.assertIn("Your last handoff", brief)
|
|
self.assertIn("In flight: 0x406F60", brief)
|
|
self.h.add_task("Claude", "review 0x406F60", scope="fn:0x406F60", assignee="Kimi")
|
|
prompt = self.h.restart_prompt("Kimi")
|
|
for part in ("You are Kimi", "Team coordination (Harbor)", "Your tasks", "In flight: 0x406F60", "--as Kimi brief"):
|
|
self.assertIn(part, prompt)
|
|
self.h.set_flag("stop:Kimi", "on", "Claude", "cli")
|
|
self.assertIn("STOPPED", self.h.restart_prompt("Kimi"))
|
|
self.assertNotIn("Your last handoff", self.h.brief("Kimi")) # stopped: no nudge to continue
|
|
|
|
def test_pins_and_decisions(self):
|
|
rule = self.h.send("Claude", "All", "Main-thread float math is single precision.", kind="decision")
|
|
self.h.pin("Codex", rule["id"], note="single precision rule")
|
|
self.assertIn("single precision rule", self.h.brief("Kimi"))
|
|
self.assertEqual([m["id"] for m in self.h.decisions()], [rule["id"]])
|
|
self.h.unpin("Kimi", rule["id"])
|
|
self.assertEqual(self.h.pins(), [])
|
|
with self.assertRaises(HarborError):
|
|
self.h.pin("Kimi", 99999)
|
|
|
|
def test_human_availability(self):
|
|
with self.assertRaises(HarborError):
|
|
self.h.set_presence("User", "cli", "away")
|
|
with self.assertRaises(HarborError):
|
|
self.h.set_presence("Codex", "web", "away")
|
|
self.h.set_presence("User", "web", "away", until="2h", note="decide with Claude")
|
|
self.assertIn("User is away until", self.h.brief("Codex"))
|
|
question = self.h.send("Codex", ["User"], "Should I rename 0x445310?")
|
|
self.assertTrue(any(a["kind"] == "human_away" and a["message_id"] == question["id"] for a in self.h.attention()))
|
|
self.clock.t += 3 * 3600
|
|
self.assertEqual(self.h.presence()["status"], "here") # the away time ran out
|
|
self.assertNotIn("is away", self.h.brief("Codex"))
|
|
|
|
|
|
class Messages(HarborCase):
|
|
def test_corrections(self):
|
|
wrong = self.h.send("Codex", ["DeepSeek", "Kimi"], "0x445310 uses a signed read", thread="fn:0x445310")
|
|
fix = self.h.send("Codex", None, "It is unsigned (movzx), not signed.", corrects=wrong["id"])
|
|
self.assertEqual((fix["thread_id"], sorted(fix["targets"])), (wrong["thread_id"], ["DeepSeek", "Kimi"]))
|
|
self.assertEqual(self.h.message(fix["id"])["kind"], "retraction")
|
|
self.assertEqual(self.h.corrections_for([wrong["id"]]), {wrong["id"]: [fix["id"]]})
|
|
self.assertIn(f"corrects #{wrong['id']}", self.mirror.read_text(encoding="utf-8"))
|
|
with self.assertRaises(HarborError):
|
|
self.h.send("Kimi", None, "not yours", corrects=wrong["id"])
|
|
self.assertTrue(self.h.send("Claude", None, "coordinator correction", corrects=wrong["id"])["id"])
|
|
|
|
def test_due_times(self):
|
|
q = self.h.send("Claude", ["Kimi"], "Is 0x406F60 exact?", due="30m")
|
|
self.assertIn("Answers due from you", self.h.brief("Kimi"))
|
|
self.assertFalse(any(a["kind"] == "due_overdue" for a in self.h.attention()))
|
|
self.clock.t += 3600
|
|
self.assertTrue(any(a["kind"] == "due_overdue" and a["message_id"] == q["id"] for a in self.h.attention()))
|
|
self.h.ack("Kimi", [q["id"]], note="yes, exact")
|
|
self.assertFalse(any(a["kind"] == "due_overdue" for a in self.h.attention()))
|
|
with self.assertRaises(HarborError):
|
|
self.h.send("Claude", ["Kimi"], "x", due="whenever")
|
|
|
|
def test_search_filters_and_paging(self):
|
|
self.h.send("Codex", "All", "LayoutTextMesh glyph key", thread="fn:0x50A720")
|
|
self.h.send("Kimi", ["Claude"], "LayoutTextMesh reviewed", kind="decision")
|
|
self.h.send("Kimi", ["Codex"], "unrelated words")
|
|
self.assertEqual(len(self.h.search("LayoutTextMesh")), 2)
|
|
self.assertEqual([m["sender"] for m in self.h.search("LayoutTextMesh from:kimi")], ["Kimi"])
|
|
self.assertEqual(len(self.h.search("kind:decision")), 1)
|
|
self.assertEqual(len(self.h.search("thread:fn:0x50A720")), 1)
|
|
self.assertEqual(len(self.h.search("to:Codex")), 1) # not its own broadcast
|
|
self.assertEqual(len(self.h.search("since:1h LayoutTextMesh")), 2)
|
|
ids = [m["id"] for m in self.h.recent(10)]
|
|
self.assertEqual([m["id"] for m in self.h.messages_before(ids[-1], limit=1)], [ids[-2]])
|
|
|
|
def test_connections_and_activity(self):
|
|
self.h.connect("Codex", "mcp", "codex")
|
|
self.h.activity("Codex", "shell", "codex")
|
|
links = {(r["channel"], r["host"]) for r in self.h.connections()["Codex"]}
|
|
self.assertEqual(links, {("mcp", "codex"), ("hook", "codex")})
|
|
codex = next(a for a in self.h.agents() if a["name"] == "Codex")
|
|
self.assertTrue(self.h.working(codex))
|
|
self.assertEqual(codex["last_tool"], "shell")
|
|
self.clock.t += 600
|
|
self.assertFalse(self.h.working(next(a for a in self.h.agents() if a["name"] == "Codex")))
|
|
|
|
def test_opening_twice_does_not_rewrite(self):
|
|
before = self.h.conn.execute("SELECT value FROM meta WHERE key='signature'").fetchone()["value"]
|
|
again = Harbor(self.project, clock=self.clock)
|
|
self.assertEqual(again.conn.execute("SELECT value FROM meta WHERE key='signature'").fetchone()["value"], before)
|
|
again.close()
|
|
|
|
|
|
class Doctor(unittest.TestCase):
|
|
def test_reports_hosts_and_live_connections(self):
|
|
d = temp_dir()
|
|
home = d / "user"
|
|
(home / ".codex").mkdir(parents=True)
|
|
(home / ".codex" / "config.toml").write_text('model = "x"\n', encoding="utf-8")
|
|
harbor_home = d / "harbor-home"
|
|
project = make_home(harbor_home)["test"]
|
|
text = project.source.read_text(encoding="utf-8").replace('name = "Codex"\n', 'name = "Codex"\nhost = "codex"\n')
|
|
project.source.write_text(text, encoding="utf-8")
|
|
saved = {k: os.environ.get(k) for k in ("HARBOR_INSTALL_HOME", "HARBOR_HOME", "HARBOR_PROJECT")}
|
|
os.environ.update(HARBOR_INSTALL_HOME=str(home), HARBOR_HOME=str(harbor_home))
|
|
os.environ.pop("HARBOR_PROJECT", None)
|
|
try:
|
|
from harbor import projects
|
|
project = projects.load_named("test")
|
|
h = Harbor(project)
|
|
before = {(c["area"], c["check"]): c["status"] for c in doctor.run(project, h, port=8797)}
|
|
self.assertEqual(before[("Codex", "MCP server")], "fail")
|
|
self.assertEqual(before[("Database", "integrity")], "ok")
|
|
self.assertEqual(before[("Server", "http://127.0.0.1:8797")], "warn")
|
|
install.install("codex", python="py -3.12")
|
|
h.connect("Codex", "mcp", "codex")
|
|
after = {(c["area"], c["check"]): c["status"] for c in doctor.run(project, h, port=8797)}
|
|
self.assertEqual((after[("Codex", "MCP server")], after[("Codex", "hooks")]), ("ok", "ok"))
|
|
self.assertEqual(after[("Codex", "hooks trusted")], "warn")
|
|
self.assertEqual(after[("Codex", "live connection")], "ok")
|
|
self.assertIn("FAIL", doctor.render(doctor.run(project, h, port=8797)) + "FAIL")
|
|
h.close()
|
|
finally:
|
|
for k, v in saved.items():
|
|
if v is None:
|
|
os.environ.pop(k, None)
|
|
else:
|
|
os.environ[k] = v
|
|
shutil.rmtree(d, ignore_errors=True)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|