Harbor as of 2026-10-05: the coordination hub for the Open Sea's decomp team
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.
This commit is contained in:
commit
dbfd1720c5
43 files changed
+12440
No files matched your search
@@ -0,0 +1,134 @@
|
||||
"""Shared test fixtures: temporary projects under R:\\Workspace\\Data\\openseas-chat\\test-tmp (never C:)."""
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
import unittest
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
from harbor import projects # noqa: E402
|
||||
from harbor.core import Harbor # noqa: E402
|
||||
|
||||
TMP_ROOT = Path(os.environ.get("HARBOR_TEST_TMP", r"R:\Workspace\Data\openseas-chat\test-tmp"))
|
||||
LEGACY_LINE = re.compile(r'^\[([^\]]+)\]\s+([A-Za-z]+)\s*(?:→|->)\s*([A-Za-z]+):\s?(.*)$')
|
||||
ROSTER = [{"name": n, "role": r} for n, r in (("Claude", "Integration"), ("Codex", "Decompilation"),
|
||||
("DeepSeek", "Decompilation"), ("OpenCode", "Decompilation"),
|
||||
("Kimi", "Review"))] + [{"name": "User", "kind": "human"}]
|
||||
PROJECT_TOML = """name = "{name}"
|
||||
title = "Test {name}"
|
||||
database = '{db}'
|
||||
mirror = '{mirror}'
|
||||
coordinators = ["Claude"]
|
||||
reviewers = ["Claude"]
|
||||
|
||||
[identities]
|
||||
"worktree-deepseek" = "DeepSeek"
|
||||
|
||||
[attention]
|
||||
ack_overdue = 30
|
||||
item_stale = 120
|
||||
agent_quiet = 60
|
||||
""" + "".join(f'\n[[agents]]\nname = "{a["name"]}"\n' + (f'kind = "{a["kind"]}"\n' if a.get("kind") else "")
|
||||
for a in ROSTER)
|
||||
|
||||
|
||||
class Clock:
|
||||
def __init__(self, t=1_790_000_000.0):
|
||||
self.t = t
|
||||
|
||||
def __call__(self):
|
||||
return self.t
|
||||
|
||||
|
||||
def temp_dir():
|
||||
TMP_ROOT.mkdir(parents=True, exist_ok=True)
|
||||
fence = TMP_ROOT / ".harbor.toml" # tests never join a project marked above this folder
|
||||
if not fence.exists():
|
||||
fence.write_text("# Test folders belong to no Harbor project above this one.\nboundary = true\n", encoding="utf-8")
|
||||
return Path(tempfile.mkdtemp(dir=TMP_ROOT))
|
||||
|
||||
|
||||
def make_home(folder, names=("test",)):
|
||||
"""A Harbor home with registered projects; returns {name: Project}."""
|
||||
out = {}
|
||||
for name in names:
|
||||
pdir = folder / "projects"
|
||||
pdir.mkdir(parents=True, exist_ok=True)
|
||||
mirror = folder / name / "agents-chat.md"
|
||||
mirror.parent.mkdir(parents=True, exist_ok=True)
|
||||
mirror.write_text("# chat\n\n---\n", encoding="utf-8")
|
||||
(pdir / f"{name}.toml").write_text(PROJECT_TOML.format(name=name, db=folder / name / "harbor.db",
|
||||
mirror=mirror), encoding="utf-8")
|
||||
out[name] = projects.load_named(name, home=folder)
|
||||
(folder / "config.toml").write_text(f'default_project = "{names[0]}"\n', encoding="utf-8")
|
||||
return out
|
||||
|
||||
|
||||
class HarborCase(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.dir = temp_dir()
|
||||
self.project = make_home(self.dir)["test"]
|
||||
self.mirror = self.project.mirror
|
||||
self.clock = Clock()
|
||||
self.h = Harbor(self.project, clock=self.clock)
|
||||
|
||||
def tearDown(self):
|
||||
self.h.close()
|
||||
shutil.rmtree(self.dir, ignore_errors=True)
|
||||
|
||||
|
||||
def env_for(home, **extra):
|
||||
env = {k: v for k, v in os.environ.items() if not k.startswith("HARBOR_")}
|
||||
env.update(HARBOR_HOME=str(home), PYTHONDONTWRITEBYTECODE="1", HARBOR_NO_TOASTS="1", **extra)
|
||||
return env
|
||||
|
||||
|
||||
def run_cli(home, *args, cwd=None, stdin=None, **env):
|
||||
return subprocess.run([sys.executable, "-B", "-m", "harbor", *args], cwd=cwd or home, env=env_for(home, **env),
|
||||
input=stdin, capture_output=True, text=True, encoding="utf-8", timeout=60,
|
||||
**({} if cwd else {}))
|
||||
|
||||
|
||||
def run_module(home, *args, cwd=None, stdin=None, **env):
|
||||
environment = env_for(home, **env)
|
||||
environment["PYTHONPATH"] = str(ROOT)
|
||||
return subprocess.run([sys.executable, "-B", "-m", "harbor", *args], cwd=cwd or home, env=environment,
|
||||
input=stdin, capture_output=True, text=True, encoding="utf-8", timeout=60)
|
||||
|
||||
|
||||
def http(port, path, body=None, headers=None, browser=True, token=None):
|
||||
h = {}
|
||||
if body is not None:
|
||||
h["Content-Type"] = "application/json"
|
||||
if browser:
|
||||
h.update({"Sec-Fetch-Site": "same-origin", "X-Harbor-Token": token or "",
|
||||
"Origin": f"http://127.0.0.1:{port}"})
|
||||
h.update(headers or {})
|
||||
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||
request = urllib.request.Request(f"http://127.0.0.1:{port}{path}", data=data, headers=h)
|
||||
try:
|
||||
with urllib.request.urlopen(request, timeout=10) as response:
|
||||
raw = response.read()
|
||||
kind = response.headers.get("Content-Type", "")
|
||||
return response.status, (json.loads(raw or b"null") if "json" in kind else raw.decode("utf-8", "replace"))
|
||||
except urllib.error.HTTPError as error:
|
||||
return error.code, json.loads(error.read() or b"null")
|
||||
|
||||
|
||||
def wait_for_port(port, seconds=15):
|
||||
deadline = time.monotonic() + seconds
|
||||
while time.monotonic() < deadline:
|
||||
try:
|
||||
urllib.request.urlopen(f"http://127.0.0.1:{port}/participants", timeout=1).read()
|
||||
return True
|
||||
except OSError:
|
||||
time.sleep(0.1)
|
||||
return False
|
||||
@@ -0,0 +1,369 @@
|
||||
"""Harbor store tests (run: py -3.12 -B -m unittest discover -s R:\\Workspace\\Tools\\Harbor\\tests)."""
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from helpers import LEGACY_LINE, ROSTER, Clock, HarborCase, make_home, temp_dir
|
||||
from harbor import projects
|
||||
from harbor.core import Harbor, HarborError, parse_scope
|
||||
|
||||
TOOLS = Path(r"R:\Workspace\Tools")
|
||||
|
||||
|
||||
class Messages(HarborCase):
|
||||
def test_send_inbox_ack(self):
|
||||
r = self.h.send("Claude", ["DeepSeek"], "Please fix 0x529350", thread="fn:0x529350",
|
||||
subject="AudioManagerTick", kind="request", ack_required=True)
|
||||
self.assertEqual(r["targets"], ["DeepSeek"])
|
||||
self.assertEqual([m["id"] for m in self.h.inbox("DeepSeek")], [r["id"]])
|
||||
self.assertEqual(self.h.inbox("DeepSeek"), [])
|
||||
self.assertEqual(len(self.h.pending_acks(sender="Claude")), 1)
|
||||
self.h.ack("DeepSeek", [r["id"]], note="fixed")
|
||||
self.assertEqual(self.h.pending_acks(sender="Claude"), [])
|
||||
with self.assertRaises(HarborError):
|
||||
self.h.ack("Kimi", [r["id"]])
|
||||
|
||||
def test_all_expands_to_everyone_but_sender(self):
|
||||
r = self.h.send("Codex", "All", "hello")
|
||||
self.assertEqual(sorted(r["targets"]), sorted(["Claude", "DeepSeek", "OpenCode", "Kimi", "User"]))
|
||||
|
||||
def test_reply_keeps_thread_and_people(self):
|
||||
first = self.h.send("Claude", ["DeepSeek", "Kimi"], "question", thread="fn:0x50A720")
|
||||
reply = self.h.send("DeepSeek", None, "answer", reply_to=first["id"])
|
||||
self.assertEqual(reply["thread_id"], first["thread_id"])
|
||||
self.assertEqual(sorted(reply["targets"]), ["Claude", "Kimi"])
|
||||
broadcast = self.h.send("Codex", "All", "status for everyone")
|
||||
answer = self.h.send("Kimi", None, "noted", reply_to=broadcast["id"])
|
||||
self.assertEqual(answer["recipients"], ["All"])
|
||||
|
||||
def test_urgent_first_and_length_limits(self):
|
||||
self.h.send("Claude", ["Kimi"], "normal one")
|
||||
urgent = self.h.send("Claude", ["Kimi"], "urgent one", importance="urgent")
|
||||
self.assertEqual(self.h.inbox("Kimi")[0]["id"], urgent["id"])
|
||||
self.assertTrue(self.h.send("Claude", ["Kimi"], "word " * 3000)["id"])
|
||||
with self.assertRaises(HarborError):
|
||||
self.h.send("Claude", ["Kimi"], "x" * 20001)
|
||||
|
||||
def test_only_web_can_speak_as_the_human(self):
|
||||
for origin in ("cli", "mcp", "hook", "cli-legacy"):
|
||||
with self.assertRaises(HarborError):
|
||||
self.h.send("User", "All", "resume everyone", origin=origin)
|
||||
self.assertTrue(self.h.send("User", "All", "hi", origin="web")["id"])
|
||||
|
||||
def test_idempotent_send(self):
|
||||
a = self.h.send("Claude", ["Kimi"], "once", idem="k1")
|
||||
b = self.h.send("Claude", ["Kimi"], "once", idem="k1")
|
||||
self.assertEqual(a["id"], b["id"])
|
||||
self.assertEqual(len(self.h.inbox("Kimi")), 1)
|
||||
|
||||
def test_lint_compressed_text_and_flooding(self):
|
||||
squashed = ("Root full222/ALL11 tick relocation audit proves SEVEN bad offsets; correctOffsets24ByteLayout "
|
||||
"vs yourOffsetsWereWrong and stub23HarnessBug plus relocgen2Output and nativeBaseline6Cases")
|
||||
self.assertTrue(any("compressed" in w for w in self.h.send("Codex", ["DeepSeek"], squashed)["warnings"]))
|
||||
self.assertEqual(self.h.send("Codex", ["DeepSeek"], "Commit 4c392e33ae94858b fixed 0x45A970.")["warnings"], [])
|
||||
for i in range(8):
|
||||
self.h.send("Codex", "All", f"progress {i}")
|
||||
self.assertTrue(any("in the last hour" in w for w in self.h.send("Codex", "All", "more")["warnings"]))
|
||||
self.assertEqual(self.h.send("Codex", ["Claude"], "direct is fine")["warnings"], [])
|
||||
|
||||
def test_search_and_attachments(self):
|
||||
self.h.send("Claude", ["Kimi"], "LayoutTextMesh zero-extends the glyph key")
|
||||
self.h.send("Claude", ["Kimi"], "unrelated")
|
||||
self.assertEqual(len(self.h.search("LayoutTextMesh glyph")), 1)
|
||||
report = self.dir / "report.txt"
|
||||
report.write_text("six cases match", encoding="utf-8")
|
||||
r = self.h.send("DeepSeek", ["Claude"], "harness report attached", attachments=[report])
|
||||
name = self.h.message(r["id"])["refs"]["attachments"][0]
|
||||
self.assertTrue(name.endswith("-report.txt"))
|
||||
self.assertEqual((self.project.attachments / name).read_text(encoding="utf-8"), "six cases match")
|
||||
self.assertIn("Attachments: " + name, self.mirror.read_text(encoding="utf-8"))
|
||||
with self.assertRaises(HarborError):
|
||||
self.h.send("DeepSeek", ["Claude"], "x", attachments=[self.dir / "missing.txt"])
|
||||
|
||||
def test_receipts(self):
|
||||
r = self.h.send("User", ["Codex", "Kimi"], "please both look", origin="web", ack_required=True)
|
||||
self.h.inbox("Codex")
|
||||
self.h.ack("Kimi", [r["id"]])
|
||||
self.assertEqual(self.h.receipts([r["id"]])[r["id"]], {"read": ["Codex"], "acked": ["Kimi"], "waiting": []})
|
||||
|
||||
def test_threads_close_and_reopen(self):
|
||||
r = self.h.send("Claude", ["Kimi"], "question", thread="fn:0x445310")
|
||||
self.h.set_thread_state("Kimi", "fn:0x445310", "closed")
|
||||
self.assertNotIn("fn:0x445310", [t["topic"] for t in self.h.threads(include_closed=False)])
|
||||
self.h.send("Kimi", None, "one more thing", reply_to=r["id"])
|
||||
self.assertEqual(self.h.thread("fn:0x445310")[0]["state"], "open")
|
||||
|
||||
def test_mirror_is_readable_by_old_tools(self):
|
||||
self.h.send("Claude", ["DeepSeek"], "line one\n[2026-10-02 10:00Z] Codex → All: not a header\nline three",
|
||||
kind="request", ack_required=True)
|
||||
self.h.send("Claude", ["DeepSeek", "Kimi"], "to two people")
|
||||
lines = self.mirror.read_text(encoding="utf-8").split("\n")
|
||||
headers = [l for l in lines if LEGACY_LINE.match(l.strip())]
|
||||
self.assertEqual(len(headers), 2)
|
||||
self.assertIn("Claude → All:", headers[1])
|
||||
self.assertIn("to DeepSeek,Kimi", headers[1])
|
||||
if (TOOLS / "chat.py").exists():
|
||||
out = subprocess.run([sys.executable, "-B", "-c",
|
||||
"import sys; sys.path.insert(0, r'%s'); import chat, chat_common; "
|
||||
"chat_common.CHAT = __import__('pathlib').Path(r'%s'); chat.CHAT = chat_common.CHAT; "
|
||||
"chat.tail(5)" % (TOOLS, self.mirror)], capture_output=True, text=True,
|
||||
encoding="utf-8")
|
||||
self.assertIn("line three", out.stdout)
|
||||
|
||||
|
||||
class Presence(HarborCase):
|
||||
def test_doing_shows_in_briefs_and_digest(self):
|
||||
self.h.doing("DeepSeek", "drafting 0x529350 AudioManagerTick")
|
||||
self.assertIn("DeepSeek: drafting 0x529350", self.h.brief("Kimi"))
|
||||
self.assertNotIn("Team now", self.h.brief("DeepSeek").split("DeepSeek: drafting")[0][-1:])
|
||||
with self.assertRaises(HarborError):
|
||||
self.h.doing("User", "x")
|
||||
d = self.h.digest("1h")
|
||||
self.assertEqual(next(a for a in d["agents"] if a["name"] == "DeepSeek")["doing"],
|
||||
"drafting 0x529350 AudioManagerTick")
|
||||
self.h.doing("DeepSeek", "")
|
||||
self.assertIsNone(next(a for a in self.h.agents() if a["name"] == "DeepSeek")["doing"])
|
||||
|
||||
def test_wait_returns_on_message_stop_or_timeout(self):
|
||||
self.h.clock = __import__("time").time
|
||||
self.assertTrue(self.h.wait("Kimi", timeout=0.3, poll=0.05)["timed_out"])
|
||||
def later():
|
||||
time.sleep(0.4)
|
||||
sender = Harbor(self.project)
|
||||
sender.send("Claude", ["Kimi"], "your turn")
|
||||
sender.close()
|
||||
threading.Thread(target=later).start()
|
||||
started = time.monotonic()
|
||||
r = self.h.wait("Kimi", timeout=10, poll=0.05)
|
||||
self.assertLess(time.monotonic() - started, 5)
|
||||
self.assertEqual([m["body"] for m in r["messages"]], ["your turn"])
|
||||
self.h.set_flag("stop:Kimi", "on", "Claude", "cli")
|
||||
self.h.mark_read("Kimi")
|
||||
self.assertEqual(self.h.wait("Kimi", timeout=10, poll=0.05)["stopped"], ["stop:Kimi"])
|
||||
|
||||
def test_digest_and_attention(self):
|
||||
self.h.send("Codex", ["Claude"], "a")
|
||||
self.h.send("User", ["Codex"], "status please", origin="web")
|
||||
item = self.h.submit("DeepSeek", "thing", scope="fn:0x445310", commit="abc")
|
||||
self.h.transition("Claude", item["id"], "in_review")
|
||||
self.h.send("Claude", ["Kimi"], "please confirm", ack_required=True)
|
||||
self.h.touch("OpenCode")
|
||||
d = self.h.digest("1h")
|
||||
codex = next(a for a in d["agents"] if a["name"] == "Codex")
|
||||
self.assertEqual((codex["messages"], codex["threads"]), (1, 1))
|
||||
self.assertEqual([e["to_state"] for e in d["item_events"]], ["submitted", "in_review"])
|
||||
self.assertEqual(self.h.digest("1h")["to_human"], [])
|
||||
self.h.send("Codex", ["User"], "here is the status")
|
||||
self.assertEqual([m["body"] for m in self.h.digest("1h")["to_human"]], ["here is the status"])
|
||||
self.assertEqual(self.h.attention(), [])
|
||||
self.clock.t += 3 * 3600
|
||||
kinds = sorted(a["kind"] for a in self.h.attention())
|
||||
self.assertEqual(kinds, ["ack_overdue", "agent_quiet", "agent_quiet", "agent_quiet", "agent_quiet", "item_stale"])
|
||||
self.h.set_flag("stop:all", "on", "Claude", "cli")
|
||||
self.assertNotIn("agent_quiet", [a["kind"] for a in self.h.attention()])
|
||||
with self.assertRaises(HarborError):
|
||||
self.h.digest("yesterday-ish")
|
||||
|
||||
|
||||
class Claims(HarborCase):
|
||||
def test_claims_conflicts_ttl_and_bands(self):
|
||||
self.h.claim("Claude", "range:0x440000-0x47FFFF", kind="band", on_behalf_of="DeepSeek")
|
||||
self.h.claim("Claude", "range:0x480000-0x4FFFFF", kind="band", on_behalf_of="OpenCode")
|
||||
with self.assertRaises(HarborError):
|
||||
self.h.claim("Kimi", "range:0x400000-0x401000", kind="band")
|
||||
d = self.h.claim("DeepSeek", "fn:0x445310", kind="draft")
|
||||
with self.assertRaises(HarborError):
|
||||
self.h.claim("OpenCode", "0x445310", kind="draft")
|
||||
with self.assertRaises(HarborError):
|
||||
self.h.claim("OpenCode", "fn:0x450000", kind="draft")
|
||||
self.assertTrue(self.h.claim("OpenCode", "fn:0x490000", kind="draft")["id"])
|
||||
self.assertTrue(self.h.claim("Kimi", "fn:0x445310", kind="review")["id"])
|
||||
renewed = self.h.claim("DeepSeek", "fn:0x445310", kind="draft", ttl_hours=1)
|
||||
self.assertEqual((renewed["renewed"], renewed["id"]), (True, d["id"]))
|
||||
self.clock.t += 2 * 3600
|
||||
self.assertEqual([c for c in self.h.active_claims(kind="draft") if c["scope"] == "fn:0x445310"], [])
|
||||
over = self.h.claim("Claude", "fn:0x450010", kind="draft", override=True)
|
||||
self.assertEqual(len(over["conflicts"]), 1)
|
||||
self.assertEqual(self.h.release("Claude", scope="fn:0x450010"), [over["id"]])
|
||||
self.assertTrue(self.h.claim("Codex", "task:write the README")["id"])
|
||||
with self.assertRaises(HarborError):
|
||||
self.h.claim("Kimi", "task:Write the README")
|
||||
|
||||
def test_scope_parsing(self):
|
||||
self.assertEqual(parse_scope("0x445310")[0], "fn:0x445310")
|
||||
self.assertEqual(parse_scope("range:0x440000-0x47ffff")[1:], (0x440000, 0x47FFFF))
|
||||
with self.assertRaises(HarborError):
|
||||
parse_scope("somewhere")
|
||||
|
||||
|
||||
class Review(HarborCase):
|
||||
def test_review_item_lifecycle(self):
|
||||
item = self.h.submit("DeepSeek", "RouteCellCost readable", scope="fn:0x445310", commit="abc1234",
|
||||
branch="deepseek/decomp")
|
||||
self.assertEqual([m["kind"] for m in self.h.inbox("Claude")], ["request"])
|
||||
with self.assertRaises(HarborError):
|
||||
self.h.transition("DeepSeek", item["id"], "approved")
|
||||
self.h.transition("Claude", item["id"], "in_review")
|
||||
self.h.transition("Claude", item["id"], "changes_requested", note="movsx: signed read")
|
||||
self.assertEqual(self.h.inbox("DeepSeek")[-1]["kind"], "request")
|
||||
self.h.transition("DeepSeek", item["id"], "submitted", commit="def5678")
|
||||
self.h.transition("Kimi", item["id"], "approved")
|
||||
with self.assertRaises(HarborError):
|
||||
self.h.transition("Kimi", item["id"], "landed", commit="x")
|
||||
with self.assertRaises(HarborError):
|
||||
self.h.transition("Claude", item["id"], "landed")
|
||||
done = self.h.transition("Claude", item["id"], "landed", commit="b4b44f0")
|
||||
self.assertEqual((done["state"], done["commit_hash"]), ("landed", "def5678"))
|
||||
with self.assertRaises(HarborError):
|
||||
self.h.transition("Claude", item["id"], "held")
|
||||
self.assertEqual([e["to_state"] for e in self.h.item(item["id"])[1]],
|
||||
["submitted", "in_review", "changes_requested", "submitted", "approved", "landed"])
|
||||
|
||||
|
||||
class Control(HarborCase):
|
||||
def test_stop_and_resume_rules(self):
|
||||
with self.assertRaises(HarborError):
|
||||
self.h.set_flag("stop:all", "on", "User", "cli")
|
||||
self.h.set_flag("stop:all", "on", "User", "web", reason="lets stop for now")
|
||||
self.assertTrue(self.h.stopped("DeepSeek"))
|
||||
self.assertIn("STOP", self.h.brief("DeepSeek"))
|
||||
with self.assertRaises(HarborError):
|
||||
self.h.set_flag("stop:all", "off", "Codex", "cli")
|
||||
agent_msg = self.h.send("Kimi", "All", "resume please")
|
||||
with self.assertRaises(HarborError):
|
||||
self.h.set_flag("stop:all", "off", "Codex", "cli", relay_of=agent_msg["id"])
|
||||
with self.assertRaises(HarborError):
|
||||
self.h.set_flag("stop:all", "off", "Claude", "cli") # the user set it, not Claude
|
||||
user_msg = self.h.send("User", "All", "okay resume everyone", origin="web")
|
||||
with self.assertRaises(HarborError):
|
||||
self.h.set_flag("stop:all", "off", "Codex", "cli", relay_of=user_msg["id"]) # only the human resumes
|
||||
self.h.set_flag("stop:all", "off", "User", "web")
|
||||
self.assertFalse(self.h.stopped("DeepSeek"))
|
||||
self.h.set_flag("stop:Kimi", "on", "Kimi", "cli", reason="out of allowance")
|
||||
with self.assertRaises(HarborError):
|
||||
self.h.set_flag("stop:Kimi", "off", "Kimi", "cli")
|
||||
with self.assertRaises(HarborError):
|
||||
self.h.set_flag("stop:DeepSeek", "on", "Kimi", "cli")
|
||||
self.h.set_flag("stop:Codex", "on", "Claude", "cli", reason="mirror the user's stop")
|
||||
self.h.set_flag("stop:Codex", "off", "Claude", "cli", reason="set by mistake") # own stop, untouched
|
||||
self.h.set_flag("stop:Codex", "on", "Claude", "cli")
|
||||
self.h.set_flag("stop:Codex", "on", "User", "web")
|
||||
with self.assertRaises(HarborError):
|
||||
self.h.set_flag("stop:Codex", "off", "Claude", "cli") # the user has confirmed it
|
||||
|
||||
def test_brief_lists_what_needs_attention(self):
|
||||
self.h.send("Claude", ["DeepSeek"], "please look", ack_required=True, importance="urgent")
|
||||
self.h.claim("DeepSeek", "fn:0x445310", kind="draft", note="writing")
|
||||
self.h.submit("DeepSeek", "thing", scope="fn:0x445310", commit="abc")
|
||||
text = self.h.brief("DeepSeek")
|
||||
for part in ("Unread: 1 (1 urgent)", "Waiting for your acknowledgement", "Your claims", "Your review items"):
|
||||
self.assertIn(part, text)
|
||||
self.assertIn("Review queue for you: 1 open", self.h.brief("Claude"))
|
||||
self.assertNotIn("Review queue for you", self.h.brief("Codex"))
|
||||
|
||||
def test_legacy_import_is_idempotent_and_read(self):
|
||||
legacy = self.dir / "old-chat.md"
|
||||
legacy.write_text("# chat\n\n---\n\n[2026-10-01 13:32Z] User → All: rundown please\n\n"
|
||||
"[2026-10-01 13:40Z] Codex → User: here it is\nsecond line\n\n"
|
||||
"[2026-10-01 13:40Z] Codex → User: here it is\nsecond line\n\n"
|
||||
"[2026-10-02 08:00Z] Kimi → Codex: next day\n", encoding="utf-8")
|
||||
self.assertEqual(self.h.import_legacy(legacy), {"parsed": 4, "added": 4})
|
||||
self.assertEqual(self.h.import_legacy(legacy)["added"], 0)
|
||||
self.assertEqual(self.h.inbox("Codex"), [])
|
||||
self.assertEqual(sorted(t["topic"] for t in self.h.threads(include_legacy=True)),
|
||||
["legacy:2026-10-01", "legacy:2026-10-02"])
|
||||
|
||||
def test_backup_keeps_newest(self):
|
||||
self.h.send("Claude", ["Kimi"], "keep me")
|
||||
for i in range(3):
|
||||
self.clock.t += 60
|
||||
self.h.backup(keep=2)
|
||||
copies = sorted(self.project.backups.glob("harbor-*.db"))
|
||||
self.assertEqual(len(copies), 2)
|
||||
import sqlite3
|
||||
with sqlite3.connect(str(copies[-1])) as c:
|
||||
self.assertEqual(c.execute("SELECT body FROM messages").fetchone()[0], "keep me")
|
||||
|
||||
|
||||
class Projects(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.dir = temp_dir()
|
||||
|
||||
def tearDown(self):
|
||||
shutil.rmtree(self.dir, ignore_errors=True)
|
||||
|
||||
def test_roster_comes_from_the_project(self):
|
||||
p = projects.project_from_dict({"name": "garden", "human": "Kai", "coordinators": ["Lead"],
|
||||
"database": str(self.dir / "g.db"),
|
||||
"agents": [{"name": "Lead"}, {"name": "Helper"}]})
|
||||
h = Harbor(p)
|
||||
self.assertEqual(h.agent_names(), ["Lead", "Helper", "Kai"])
|
||||
self.assertEqual(sorted(h.send("Lead", "All", "hi")["targets"]), ["Helper", "Kai"])
|
||||
with self.assertRaises(HarborError):
|
||||
h.send("Kai", "All", "pretend", origin="cli")
|
||||
with self.assertRaises(HarborError):
|
||||
h.send("Lead", ["Codex"], "not on this team")
|
||||
item = h.submit("Helper", "first task") # reviewers default to Lead
|
||||
self.assertEqual(h.inbox("Lead")[-1]["refs"]["item"], item["id"])
|
||||
h.close()
|
||||
smaller = projects.project_from_dict({"name": "garden", "human": "Kai", "database": str(self.dir / "g.db"),
|
||||
"agents": [{"name": "Lead"}]})
|
||||
h = Harbor(smaller)
|
||||
self.assertEqual([a["name"] for a in h.agents()], ["Lead", "Kai"]) # Helper keeps history, inactive
|
||||
h.close()
|
||||
other = projects.project_from_dict({"name": "other", "database": str(self.dir / "g.db"),
|
||||
"agents": [{"name": "Lead"}]})
|
||||
with self.assertRaises(HarborError):
|
||||
Harbor(other) # the database belongs to garden
|
||||
|
||||
def test_validation(self):
|
||||
for bad in ({"name": "Bad Name"}, {"name": "x", "agents": [{"name": "All"}]},
|
||||
{"name": "x", "agents": [{"name": "A"}, {"name": "A"}]},
|
||||
{"name": "x", "coordinators": ["Ghost"], "agents": [{"name": "A"}]}):
|
||||
with self.assertRaises(projects.ProjectError):
|
||||
projects.project_from_dict(bad, home=self.dir)
|
||||
|
||||
def test_resolution_order_and_identities(self):
|
||||
made = make_home(self.dir, names=("first", "second"))
|
||||
self.assertEqual(projects.resolve(home=self.dir, cwd=self.dir).name, "first") # home default
|
||||
self.assertEqual(projects.resolve("second", home=self.dir).name, "second") # explicit
|
||||
tree = self.dir / "repo" / "worktree-deepseek" / "src"
|
||||
tree.mkdir(parents=True)
|
||||
(self.dir / "repo" / ".harbor.toml").write_text('project = "second"\n', encoding="utf-8")
|
||||
found = projects.resolve(home=self.dir, cwd=tree)
|
||||
self.assertEqual(found.name, "second") # nearest marker
|
||||
self.assertEqual(found.identity_for(tree), "DeepSeek")
|
||||
self.assertIsNone(found.identity_for(self.dir))
|
||||
with self.assertRaises(projects.ProjectError):
|
||||
projects.resolve("missing", home=self.dir)
|
||||
self.assertEqual(made["first"].ai_names, ["Claude", "Codex", "DeepSeek", "OpenCode", "Kimi"])
|
||||
|
||||
|
||||
class Concurrency(unittest.TestCase):
|
||||
def test_many_processes_write_without_loss(self):
|
||||
d = temp_dir()
|
||||
project = make_home(d)["test"]
|
||||
Harbor(project).close()
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
script = ("import sys; sys.path.insert(0, r'%s'); from harbor import projects; from harbor.core import Harbor; "
|
||||
"h = Harbor(projects.load_named('test', home=r'%s')); "
|
||||
"[h.send(sys.argv[1], ['Claude'], f'{sys.argv[1]} message {i}') for i in range(40)]" % (root, d))
|
||||
senders = ["Codex", "DeepSeek", "OpenCode", "Kimi", "Codex", "DeepSeek"]
|
||||
procs = [subprocess.Popen([sys.executable, "-B", "-c", script, s]) for s in senders]
|
||||
self.assertEqual([p.wait(timeout=180) for p in procs], [0] * len(senders))
|
||||
h = Harbor(project)
|
||||
rows = h.conn.execute("SELECT COUNT(*) n, COUNT(DISTINCT id) u FROM messages").fetchone()
|
||||
self.assertEqual((rows["n"], rows["u"]), (240, 240))
|
||||
headers = [l for l in project.mirror.read_text(encoding="utf-8").split("\n") if LEGACY_LINE.match(l.strip())]
|
||||
self.assertEqual(len(headers), 240)
|
||||
self.assertEqual(h.unread_count("Claude")[0], 240)
|
||||
h.close()
|
||||
shutil.rmtree(d, ignore_errors=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,205 @@
|
||||
"""Questions with answer buttons, team memory, collision radar, pulse, metrics, wins, export, handoff reminders."""
|
||||
import json
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
from helpers import ROOT, HarborCase, env_for, http, make_home, run_module, temp_dir, wait_for_port
|
||||
from harbor import hooks, projects
|
||||
from harbor.core import Harbor, HarborError
|
||||
|
||||
|
||||
class Questions(HarborCase):
|
||||
def test_ask_answer_and_fallback(self):
|
||||
q = self.h.ask("Codex", "Rename 0x445310 to RouteCellCost?", ["Yes", "No", "Later"], due="2h", fallback="Claude")
|
||||
self.assertEqual((q["to_name"], q["options"]), ("User", ["Yes", "No", "Later"]))
|
||||
self.assertEqual(self.h.questions_by_message([q["message_id"]])[q["message_id"]]["id"], q["id"])
|
||||
self.assertTrue(any(a["kind"] == "questions" for a in self.h.attention()))
|
||||
with self.assertRaises(HarborError):
|
||||
self.h.answer("Kimi", q["id"], "Yes") # not for Kimi
|
||||
with self.assertRaises(HarborError):
|
||||
self.h.answer("Claude", q["id"], "Yes") # the human is here
|
||||
with self.assertRaises(HarborError):
|
||||
self.h.answer("User", q["id"], "Yes", origin="cli") # the human answers on the page
|
||||
with self.assertRaises(HarborError):
|
||||
self.h.answer("User", q["id"], "Maybe", origin="web") # not an option
|
||||
self.h.set_presence("User", "web", "away", until="8h")
|
||||
done = self.h.answer("Claude", q["id"], "Yes") # the fallback while away
|
||||
self.assertEqual((done["answer"], done["answered_by"]), ("Yes", "Claude"))
|
||||
reply = self.h.inbox("Codex")[-1]
|
||||
self.assertEqual((reply["kind"], reply["reply_to"]), ("answer", q["message_id"]))
|
||||
self.assertIn("Yes", reply["body"])
|
||||
with self.assertRaises(HarborError):
|
||||
self.h.answer("User", q["id"], "No", origin="web") # answered already
|
||||
|
||||
def test_agent_to_agent_and_free_text(self):
|
||||
q = self.h.ask("Codex", "Which header owns g_audio_manager?", to="Kimi")
|
||||
self.assertIn("Questions for you", self.h.brief("Kimi"))
|
||||
self.assertIn("Your open questions", self.h.brief("Codex"))
|
||||
self.h.answer("Kimi", q["id"], "audio_manager.h, declared extern")
|
||||
self.assertEqual(self.h.questions(open_only=True), [])
|
||||
with self.assertRaises(HarborError):
|
||||
self.h.ask("Codex", "x", fallback="Kimi") # fallback must coordinate
|
||||
|
||||
|
||||
class Memory(HarborCase):
|
||||
def test_notes_and_context(self):
|
||||
self.h.note("Kimi", "fn:0x52BD10", "ABI: stdcall, ret 4; the third push is its argument")
|
||||
old = self.h.note("Codex", "0x445310", "uses a signed read")
|
||||
self.h.note("Codex", "fn:0x445310", "uses an unsigned read (movzx)", replaces=old["id"])
|
||||
self.h.send("DeepSeek", "All", "0x445310 RouteCellCost draft is ready")
|
||||
self.h.add_task("Claude", "make 0x445310 readable", scope="fn:0x445310")
|
||||
self.h.submit("DeepSeek", "RouteCellCost readable", scope="fn:0x445310", commit="abc")
|
||||
text = self.h.context_text("fn:0x445310")
|
||||
for part in ("unsigned read", "review item", "task #", "RouteCellCost draft"):
|
||||
self.assertIn(part, text)
|
||||
self.assertNotIn("uses a signed read", text) # replaced notes drop out
|
||||
self.assertEqual(len(self.h.notes("range:0x440000-0x47FFFF")), 1)
|
||||
self.assertIn("nothing yet", self.h.context_text("fn:0x400000"))
|
||||
task = self.h.next_task("Codex")
|
||||
self.assertEqual(task["scope"], "fn:0x445310")
|
||||
|
||||
|
||||
class Collisions(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.dir = temp_dir()
|
||||
data = {"name": "radar", "coordinators": ["Lead"], "database": str(self.dir / "r.db"),
|
||||
"agents": [{"name": "Lead"}, {"name": "Ann", "workdir": str(self.dir / "wt-ann")},
|
||||
{"name": "Bob", "workdir": str(self.dir / "wt-bob")}]}
|
||||
self.h = Harbor(projects.project_from_dict(data))
|
||||
|
||||
def tearDown(self):
|
||||
self.h.close()
|
||||
shutil.rmtree(self.dir, ignore_errors=True)
|
||||
|
||||
def test_same_file_in_two_worktrees(self):
|
||||
paths = self.h.edited_paths("Edit", {"file_path": str(self.dir / "wt-ann" / "annotations" / "names.csv")})
|
||||
self.assertEqual(self.h.record_touches("Ann", paths, "Edit"), [])
|
||||
patch = "*** Begin Patch\n*** Update File: annotations/names.csv\n@@\n-a\n+b\n*** End Patch"
|
||||
paths = self.h.edited_paths("apply_patch", {"input": patch})
|
||||
self.assertEqual(paths, ["annotations/names.csv"])
|
||||
hits = self.h.record_touches("Bob", paths, "apply_patch", cwd=str(self.dir / "wt-bob"))
|
||||
self.assertEqual([(c["path"], c["other"]) for c in hits], [("annotations/names.csv", "Ann")])
|
||||
alert = self.h.inbox("Ann")[-1]
|
||||
self.assertEqual((alert["sender"], alert["kind"], alert["topic"]), ("Bob", "finding", "file:annotations/names.csv"))
|
||||
self.assertEqual(self.h.inbox("Lead")[-1]["id"], alert["id"])
|
||||
self.h.record_touches("Bob", paths, "apply_patch")
|
||||
self.assertEqual(len([m for m in self.h.inbox("Ann", unread_only=False) if m["kind"] == "finding"]), 1)
|
||||
self.assertIn("Files you share", self.h.brief("Ann"))
|
||||
self.assertTrue(any(a["kind"] == "collision" for a in self.h.attention()))
|
||||
self.assertEqual(self.h.edited_paths("Read", {"file_path": "x.c"}), [])
|
||||
|
||||
def test_hook_records_edits(self):
|
||||
(self.dir / ".harbor.toml").write_text(f"project = 'radar'\n", encoding="utf-8")
|
||||
home = self.dir / "home"
|
||||
(home / "projects").mkdir(parents=True)
|
||||
data = (f"name = 'radar'\ncoordinators = ['Lead']\ndatabase = '{self.dir / 'r.db'}'\n"
|
||||
f"[[agents]]\nname = 'Lead'\n[[agents]]\nname = 'Ann'\nworkdir = '{self.dir / 'wt-ann'}'\n"
|
||||
f"[[agents]]\nname = 'Bob'\nworkdir = '{self.dir / 'wt-bob'}'\n")
|
||||
(home / "projects" / "radar.toml").write_text(data, encoding="utf-8")
|
||||
for agent, wt in (("Ann", "wt-ann"), ("Bob", "wt-bob")):
|
||||
payload = {"cwd": str(self.dir), "tool_name": "Write",
|
||||
"tool_input": {"file_path": str(self.dir / wt / "src" / "x.c")}}
|
||||
r = run_module(home, "--as", agent, "--project", "radar", "hook", "pre-tool", "--host", "claude",
|
||||
stdin=json.dumps(payload))
|
||||
self.assertEqual((r.returncode, r.stdout), (0, ""), r.stderr)
|
||||
self.assertTrue(any(m["kind"] == "finding" for m in self.h.inbox("Ann")))
|
||||
|
||||
|
||||
class PulseScore(HarborCase):
|
||||
def test_pulse_metrics_wins_export(self):
|
||||
for _ in range(3):
|
||||
self.h.activity("Codex", "shell", "codex")
|
||||
self.h.send("Codex", ["Claude"], "hi")
|
||||
rows = self.h.pulse(hours=1)["agents"]["Codex"]
|
||||
self.assertEqual((sum(r["tools"] for r in rows), sum(r["messages"] for r in rows)), (3, 1))
|
||||
self.h.metric("Claude", "Readable", 778)
|
||||
self.h.metric("Claude", "readable", 779, note="b4b44f0")
|
||||
m = self.h.metrics()["readable"]
|
||||
self.assertEqual(([p["value"] for p in m["series"]], m["latest"]["note"]), ([778, 779], "b4b44f0"))
|
||||
with self.assertRaises(HarborError):
|
||||
self.h.metric("Claude", "readable", "lots")
|
||||
item = self.h.submit("DeepSeek", "RouteCellCost readable", scope="fn:0x445310", commit="abc")
|
||||
self.h.transition("Claude", item["id"], "approved")
|
||||
self.h.transition("Claude", item["id"], "landed", commit="b4b44f0")
|
||||
self.assertEqual([w["title"] for w in self.h.wins()], ["RouteCellCost readable"])
|
||||
win = self.h.inbox("User")[-1]
|
||||
self.assertEqual(win["topic"], "wins")
|
||||
md = self.h.export_thread(f"item:{item['id']}")
|
||||
self.assertTrue(md.startswith("# Review: RouteCellCost readable"))
|
||||
self.assertIn("approved -> landed", md)
|
||||
|
||||
def test_handoff_reminder_once(self):
|
||||
for _ in range(31):
|
||||
self.h.activity("Kimi", "shell")
|
||||
text = hooks.handoff_reminder(self.h, "Kimi")
|
||||
self.assertIn("handoff is missing", text)
|
||||
self.assertIsNone(hooks.handoff_reminder(self.h, "Kimi"))
|
||||
self.h.handoff("Codex", "x")
|
||||
self.assertIsNone(hooks.handoff_reminder(self.h, "Codex")) # not active enough anyway
|
||||
|
||||
def test_stop_message_asks_for_a_handoff(self):
|
||||
self.h.set_flag("stop:Kimi", "on", "Claude", "cli")
|
||||
self.assertIn("write a handoff", self.h.inbox("Kimi")[-1]["body"])
|
||||
|
||||
|
||||
class ServerExtras(unittest.TestCase):
|
||||
PORT = 8794
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.dir = temp_dir()
|
||||
cls.project = make_home(cls.dir)["test"]
|
||||
cls.proc = subprocess.Popen([sys.executable, "-B", "-m", "harbor", "serve", "--port", str(cls.PORT)],
|
||||
cwd=cls.dir, env=env_for(cls.dir, PYTHONPATH=str(ROOT)),
|
||||
stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
assert wait_for_port(cls.PORT)
|
||||
cls.token = re.search(r'name="harbor-token" content="([^"]+)"', http(cls.PORT, "/")[1]).group(1)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
cls.proc.terminate()
|
||||
cls.proc.wait(timeout=10)
|
||||
cls.proc.stdout.close()
|
||||
shutil.rmtree(cls.dir, ignore_errors=True)
|
||||
|
||||
def test_answer_from_the_page_and_new_endpoints(self):
|
||||
h = Harbor(self.project)
|
||||
q = h.ask("Codex", "Ship it?", ["Yes", "No"])
|
||||
h.note("Kimi", "fn:0x445310", "unsigned read")
|
||||
h.activity("Codex", "shell")
|
||||
h.close()
|
||||
shown = {m["id"]: m for m in http(self.PORT, "/messages")[1]}[q["message_id"]]
|
||||
self.assertEqual((shown["question"]["options"], shown["question"]["answer"]), (["Yes", "No"], None))
|
||||
self.assertEqual(len(http(self.PORT, "/api/overview")[1]["questions"]), 1)
|
||||
status, r = http(self.PORT, "/api/answer", {"question": q["id"], "answer": "Yes"}, token=self.token)
|
||||
self.assertEqual((status, r["question"]["answer"]), (200, "Yes"))
|
||||
shown = {m["id"]: m for m in http(self.PORT, "/messages")[1]}[q["message_id"]]
|
||||
self.assertEqual(shown["question"]["answered_by"], "User")
|
||||
self.assertIn("Codex", http(self.PORT, "/api/pulse?hours=2")[1]["agents"])
|
||||
self.assertIn("unsigned read", http(self.PORT, "/api/context?scope=fn:0x445310")[1]["text"])
|
||||
status, md = http(self.PORT, "/api/export?thread=questions")
|
||||
self.assertEqual(status, 200)
|
||||
self.assertIn("Ship it?", md)
|
||||
|
||||
|
||||
class CliExtras(unittest.TestCase):
|
||||
def test_commands(self):
|
||||
d = temp_dir()
|
||||
make_home(d)
|
||||
try:
|
||||
r = run_module(d, "--as", "Codex", "ask", "Merge", "now?", "--options", "Yes|No")
|
||||
self.assertIn("question #1", r.stdout, r.stderr)
|
||||
self.assertIn("note #1", run_module(d, "--as", "Kimi", "note", "fn:0x445310", "unsigned", "read").stdout)
|
||||
self.assertIn("unsigned read", run_module(d, "--as", "Kimi", "context", "0x445310").stdout)
|
||||
self.assertIn("readable = 779", run_module(d, "--as", "Claude", "metric", "readable", "779").stdout)
|
||||
self.assertIn("Activity per hour", run_module(d, "--as", "Claude", "pulse").stdout)
|
||||
self.assertIn("# Questions", run_module(d, "export", "questions").stdout)
|
||||
finally:
|
||||
shutil.rmtree(d, ignore_errors=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,166 @@
|
||||
"""Git radar, bulk tasks and templates, the watchdog, policies and Windows notifications."""
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
os.environ["HARBOR_NO_TOASTS"] = "1"
|
||||
from helpers import ROOT, env_for, http, make_home, temp_dir, wait_for_port
|
||||
from harbor import notify, projects
|
||||
from harbor.core import Harbor, HarborError
|
||||
|
||||
|
||||
def git(cwd, *args):
|
||||
subprocess.run(["git", "-C", str(cwd), *args], check=True, capture_output=True,
|
||||
env={**os.environ, "GIT_AUTHOR_NAME": "t", "GIT_AUTHOR_EMAIL": "t@x", "GIT_COMMITTER_NAME": "t",
|
||||
"GIT_COMMITTER_EMAIL": "t@x"})
|
||||
|
||||
|
||||
class Clock:
|
||||
def __init__(self, t=1_790_000_000.0):
|
||||
self.t = t
|
||||
|
||||
def __call__(self):
|
||||
return self.t
|
||||
|
||||
|
||||
class Flow(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.dir = temp_dir()
|
||||
repo = self.dir / "repo"
|
||||
repo.mkdir()
|
||||
git(repo, "init", "-q", "-b", "main")
|
||||
(repo / "a.c").write_text("int a;\n", encoding="utf-8")
|
||||
git(repo, "add", "a.c")
|
||||
git(repo, "commit", "-q", "-m", "base")
|
||||
git(repo, "worktree", "add", "-q", "-b", "ann/work", str(self.dir / "wt-ann"))
|
||||
for i in range(3):
|
||||
(self.dir / "wt-ann" / f"f{i}.c").write_text(f"int f{i};\n", encoding="utf-8")
|
||||
git(self.dir / "wt-ann", "add", f"f{i}.c")
|
||||
git(self.dir / "wt-ann", "commit", "-q", "-m", f"draft {i}")
|
||||
(self.dir / "wt-ann" / "scratch.txt").write_text("x", encoding="utf-8")
|
||||
data = {"name": "flow", "coordinators": ["Lead"], "reviewers": ["Lead"], "database": str(self.dir / "f.db"),
|
||||
"policy": {"land_requires": ["Rev"], "all_per_hour": 2},
|
||||
"templates": {"decomp": {"steps": [
|
||||
{"name": "draft", "title": "Make {scope} readable", "assignee": "{owner}"},
|
||||
{"name": "names", "title": "Name what {scope} calls", "assignee": "{owner}", "after": ["draft"]}]}},
|
||||
"agents": [{"name": "Lead", "workdir": str(repo)}, {"name": "Ann", "workdir": str(self.dir / "wt-ann")},
|
||||
{"name": "Rev"}]}
|
||||
self.clock = Clock()
|
||||
self.h = Harbor(projects.project_from_dict(data), clock=self.clock)
|
||||
|
||||
def tearDown(self):
|
||||
self.h.close()
|
||||
shutil.rmtree(self.dir, ignore_errors=True)
|
||||
|
||||
def test_git_radar(self):
|
||||
g = self.h.git_status("Ann", max_age=0)
|
||||
self.assertEqual((g["branch"], g["dirty"], g["behind_main"], g["ahead_of_main"]), ("ann/work", 1, 0, 3))
|
||||
self.assertEqual(len(g["unsubmitted"]), 3)
|
||||
sha = g["last_commit"]["sha"]
|
||||
self.h.submit("Ann", "drafts", commit=sha)
|
||||
self.assertEqual([c["sha"] for c in self.h.git_status("Ann", max_age=0)["unsubmitted"]][:1] != [sha], True)
|
||||
self.assertEqual(self.h.git_status("Lead", max_age=0)["unsubmitted"], []) # main is the landing branch
|
||||
self.assertIsNone(self.h.git_status("Rev"))
|
||||
|
||||
def test_templates_bulk_and_band_routing(self):
|
||||
self.h.claim("Lead", "range:0x440000-0x47FFFF", kind="band", on_behalf_of="Ann")
|
||||
steps = self.h.run_template("Lead", "decomp", scope="fn:0x445310")
|
||||
self.assertEqual([(t["title"], t["assignee"]) for t in steps],
|
||||
[("Make fn:0x445310 readable", "Ann"), ("Name what fn:0x445310 calls", "Ann")])
|
||||
self.assertEqual(steps[1]["waiting_on"], [steps[0]["id"]])
|
||||
r = self.h.bulk_tasks("Lead", "# held functions\n0x445310 again\nfn:0x450000 | RouteCellCost\n0x500000\n"
|
||||
"Write the README\n")
|
||||
self.assertEqual([t["title"] for t in r["created"]],
|
||||
["Make 0x450000 readable (RouteCellCost)", "Make 0x500000 readable", "Write the README"])
|
||||
self.assertEqual([t["assignee"] for t in r["created"]], ["Ann", None, None])
|
||||
self.assertEqual(r["skipped"], ["fn:0x445310 already has an open task"])
|
||||
with self.assertRaises(HarborError):
|
||||
self.h.run_template("Lead", "nope")
|
||||
|
||||
def test_policies(self):
|
||||
item = self.h.submit("Ann", "x", commit="abc")
|
||||
self.h.transition("Lead", item["id"], "approved")
|
||||
with self.assertRaises(HarborError):
|
||||
self.h.transition("Lead", item["id"], "landed", commit="b4") # Rev has not approved
|
||||
self.h.transition("Rev", item["id"], "approved") if False else None
|
||||
self.h.transition("Lead", item["id"], "changes_requested")
|
||||
self.h.transition("Ann", item["id"], "submitted", commit="def")
|
||||
self.h.transition("Rev", item["id"], "approved")
|
||||
self.h.transition("Lead", item["id"], "landed", commit="b4")
|
||||
self.h.send("Ann", "All", "one")
|
||||
self.h.send("Ann", "All", "two")
|
||||
with self.assertRaises(HarborError):
|
||||
self.h.send("Ann", "All", "three") # capped at 2 per hour
|
||||
self.h.send("Ann", ["Lead"], "direct is fine")
|
||||
self.h.send("Lead", "All", "coordinators are not capped")
|
||||
|
||||
def test_watchdog(self):
|
||||
for _ in range(160):
|
||||
self.h.activity("Ann", "shell")
|
||||
self.assertEqual([b["agent"] for b in self.h.busy_without_progress()], ["Ann"])
|
||||
self.h.doing("Ann", "still on the proof")
|
||||
self.assertEqual(self.h.busy_without_progress(), [])
|
||||
|
||||
def test_project_validation(self):
|
||||
bad = {"name": "x", "agents": [{"name": "A"}], "templates": {"t": {"steps": [{"title": "a", "after": ["zz"]}]}}}
|
||||
with self.assertRaises(projects.ProjectError):
|
||||
projects.project_from_dict(bad, home=self.dir)
|
||||
|
||||
|
||||
class Notifications(unittest.TestCase):
|
||||
def test_script_and_selection(self):
|
||||
script = notify.toast_script("Codex asks you", "Rename <x> & 'y'?")
|
||||
self.assertIn("<x> & ''y''", script)
|
||||
d = temp_dir()
|
||||
h = Harbor(make_home(d)["test"])
|
||||
try:
|
||||
h.send("Codex", "All", "before")
|
||||
notify.set_enabled(h, True)
|
||||
self.assertTrue(notify.enabled(h))
|
||||
h.send("Codex", "All", "a broadcast")
|
||||
h.send("Codex", ["User"], "a direct message")
|
||||
h.ask("Kimi", "Ship?", ["Yes", "No"])
|
||||
pending, top = notify.pending_for_human(h)
|
||||
self.assertEqual([m["body"].split("\n")[0] for m in pending], ["a direct message", "Ship?"])
|
||||
notify.mark(h, top)
|
||||
self.assertEqual(notify.pending_for_human(h)[0], [])
|
||||
self.assertFalse(notify.toast("t", "b")) # disabled in tests
|
||||
finally:
|
||||
h.close()
|
||||
shutil.rmtree(d, ignore_errors=True)
|
||||
|
||||
|
||||
class ServerFlow(unittest.TestCase):
|
||||
PORT = 8793
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.dir = temp_dir()
|
||||
cls.project = make_home(cls.dir)["test"]
|
||||
cls.proc = subprocess.Popen([sys.executable, "-B", "-m", "harbor", "serve", "--port", str(cls.PORT)],
|
||||
cwd=cls.dir, env=env_for(cls.dir, PYTHONPATH=str(ROOT)),
|
||||
stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
assert wait_for_port(cls.PORT)
|
||||
cls.token = re.search(r'name="harbor-token" content="([^"]+)"', http(cls.PORT, "/")[1]).group(1)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
cls.proc.terminate()
|
||||
cls.proc.wait(timeout=10)
|
||||
cls.proc.stdout.close()
|
||||
shutil.rmtree(cls.dir, ignore_errors=True)
|
||||
|
||||
def test_bulk_and_toasts(self):
|
||||
status, r = http(self.PORT, "/api/bulk", {"text": "0x445310 One\n0x445320 Two"}, token=self.token)
|
||||
self.assertEqual((status, r["created"]), (200, 2))
|
||||
self.assertEqual(len(http(self.PORT, "/api/overview")[1]["tasks"]), 2)
|
||||
self.assertEqual(http(self.PORT, "/api/toasts", {"on": True}, token=self.token)[1]["on"], True)
|
||||
self.assertTrue(http(self.PORT, "/api/overview")[1]["toasts"])
|
||||
self.assertEqual(http(self.PORT, "/api/toasts", {"on": False}, token=self.token)[1]["on"], False)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,155 @@
|
||||
"""Installer tests against a fake user home (HARBOR_INSTALL_HOME) - real host configs are never touched."""
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import tomllib
|
||||
import unittest
|
||||
|
||||
import yaml
|
||||
|
||||
from helpers import make_home, temp_dir
|
||||
from harbor import install
|
||||
|
||||
CODEX_CONFIG = '''model = "gpt"
|
||||
notify = ["x.exe", "turn-ended"]
|
||||
|
||||
[features]
|
||||
|
||||
[mcp_servers.node_repl]
|
||||
command = "node"
|
||||
|
||||
[tui.model_availability_nux]
|
||||
seen = true
|
||||
'''
|
||||
DSH_PATCH = '''# Your patch layer
|
||||
- id: ui-chat
|
||||
name: "@deepseek-ai/dsh-client-ui-chat"
|
||||
config:
|
||||
transcriptView: standard
|
||||
- id: tools
|
||||
config:
|
||||
mode: !!js process.env.DSH_TOOLS_MODE
|
||||
'''
|
||||
|
||||
|
||||
class Installer(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.dir = temp_dir()
|
||||
self.home = self.dir / "user"
|
||||
(self.home / ".codex").mkdir(parents=True)
|
||||
(self.home / ".codex" / "config.toml").write_text(CODEX_CONFIG, encoding="utf-8")
|
||||
(self.home / ".codex" / "AGENTS.md").write_text("# My rules\n\nBe brief.\n", encoding="utf-8")
|
||||
(self.home / ".config" / "opencode").mkdir(parents=True)
|
||||
(self.home / ".config" / "opencode" / "opencode.jsonc").write_text('{\n "$schema": "https://opencode.ai/config.json"\n}', encoding="utf-8")
|
||||
(self.home / ".dsh" / "profiles" / "desktop").mkdir(parents=True)
|
||||
(self.home / ".dsh" / "profiles" / "desktop" / "cordis.patch.yml").write_text(DSH_PATCH, encoding="utf-8")
|
||||
self.harbor_home = self.dir / "harbor-home"
|
||||
make_home(self.harbor_home)
|
||||
self.saved = {k: os.environ.get(k) for k in ("HARBOR_INSTALL_HOME", "HARBOR_HOME", "HARBOR_PROJECT", "APPDATA")}
|
||||
os.environ.update(HARBOR_INSTALL_HOME=str(self.home), HARBOR_HOME=str(self.harbor_home),
|
||||
APPDATA=str(self.dir / "appdata"))
|
||||
os.environ.pop("HARBOR_PROJECT", None)
|
||||
self.cwd = os.getcwd()
|
||||
os.chdir(self.dir) # resolve projects from the test folder, not from wherever the suite runs
|
||||
|
||||
def tearDown(self):
|
||||
os.chdir(self.cwd)
|
||||
for k, v in self.saved.items():
|
||||
if v is None:
|
||||
os.environ.pop(k, None)
|
||||
else:
|
||||
os.environ[k] = v
|
||||
shutil.rmtree(self.dir, ignore_errors=True)
|
||||
|
||||
def read(self, *parts):
|
||||
return (self.home.joinpath(*parts)).read_text(encoding="utf-8")
|
||||
|
||||
def test_codex_install_is_idempotent_and_reversible(self):
|
||||
install.install("codex", python="py -3.12")
|
||||
config = tomllib.loads(self.read(".codex", "config.toml"))
|
||||
server = config["mcp_servers"]["harbor"]
|
||||
self.assertEqual(server["env"]["HARBOR_AGENT"], "Codex")
|
||||
self.assertEqual(server["args"][-3:], ["--as", "Codex", "mcp"])
|
||||
self.assertEqual(config["mcp_servers"]["node_repl"]["command"], "node") # untouched
|
||||
self.assertTrue(config["tui"]["model_availability_nux"]["seen"])
|
||||
hooks = json.loads(self.read(".codex", "hooks.json"))["hooks"]
|
||||
self.assertEqual(sorted(hooks), ["PreToolUse", "SessionStart", "Stop", "UserPromptSubmit"])
|
||||
self.assertIn("hook pre-tool --host codex", hooks["PreToolUse"][0]["hooks"][0]["command"])
|
||||
self.assertIn("Be brief.", self.read(".codex", "AGENTS.md"))
|
||||
self.assertIn("you are **Codex**", self.read(".codex", "AGENTS.md"))
|
||||
once = self.read(".codex", "config.toml")
|
||||
report = install.install("codex", python="py -3.12")
|
||||
self.assertEqual(self.read(".codex", "config.toml"), once)
|
||||
self.assertTrue(all(line.startswith("unchanged") for line in report if line.startswith(("unchanged", "update", "create"))))
|
||||
install.install("codex", remove=True)
|
||||
self.assertEqual(self.read(".codex", "config.toml"), CODEX_CONFIG)
|
||||
self.assertEqual(self.read(".codex", "AGENTS.md"), "# My rules\n\nBe brief.\n")
|
||||
self.assertFalse((self.home / ".codex" / "hooks.json").exists())
|
||||
backups = list((self.harbor_home / "backups" / "integrations").rglob("config.toml"))
|
||||
self.assertTrue(backups)
|
||||
|
||||
def test_codex_keeps_other_hooks(self):
|
||||
(self.home / ".codex" / "hooks.json").write_text(json.dumps(
|
||||
{"hooks": {"Stop": [{"hooks": [{"type": "command", "command": "mine.exe"}]}]}}), encoding="utf-8")
|
||||
install.install("codex", python="py -3.12")
|
||||
stop = json.loads(self.read(".codex", "hooks.json"))["hooks"]["Stop"]
|
||||
self.assertEqual(len(stop), 2)
|
||||
install.install("codex", remove=True)
|
||||
self.assertEqual(json.loads(self.read(".codex", "hooks.json")),
|
||||
{"hooks": {"Stop": [{"hooks": [{"type": "command", "command": "mine.exe"}]}]}})
|
||||
|
||||
def test_opencode(self):
|
||||
install.install("opencode", python="py -3.12")
|
||||
config = json.loads(self.read(".config", "opencode", "opencode.jsonc"))
|
||||
self.assertEqual(config["$schema"], "https://opencode.ai/config.json")
|
||||
self.assertEqual(config["mcp"]["harbor"]["environment"], {"HARBOR_AGENT": "OpenCode", "HARBOR_HOST": "opencode"})
|
||||
plugin = self.read(".config", "opencode", "plugin", "harbor.js")
|
||||
self.assertIn('const AGENT = "OpenCode";', plugin)
|
||||
self.assertIn('const COMMAND = ["py", "-3.12", "-B",', plugin)
|
||||
self.assertIn("export const HarborPlugin", plugin)
|
||||
install.install("opencode", remove=True)
|
||||
self.assertEqual(json.loads(self.read(".config", "opencode", "opencode.jsonc")),
|
||||
{"$schema": "https://opencode.ai/config.json"})
|
||||
self.assertFalse((self.home / ".config" / "opencode" / "plugin" / "harbor.js").exists())
|
||||
|
||||
def test_deepseek(self):
|
||||
install.install("deepseek", python="py -3.12")
|
||||
patch = self.read(".dsh", "profiles", "desktop", "cordis.patch.yml")
|
||||
self.assertTrue(patch.startswith(DSH_PATCH.rstrip("\n")))
|
||||
rows = install._TolerantLoader.load(patch)
|
||||
inserted = rows[-1]["insert"]
|
||||
self.assertEqual([r["id"] for r in inserted], ["mcp-harbor", "hooks-harbor"])
|
||||
self.assertEqual(inserted[0]["config"]["env"], {"HARBOR_AGENT": "DeepSeek", "HARBOR_HOST": "deepseek"})
|
||||
hooks = json.loads((self.harbor_home / "integrations" / "deepseek" / "hooks.json").read_text(encoding="utf-8"))
|
||||
self.assertIn("--host deepseek", hooks["hooks"]["Stop"][0]["hooks"][0]["command"])
|
||||
install.install("deepseek", remove=True)
|
||||
self.assertEqual(self.read(".dsh", "profiles", "desktop", "cordis.patch.yml"), DSH_PATCH)
|
||||
|
||||
def test_kimi_plugin_is_bound_to_a_project(self):
|
||||
install.install("kimi", project="test", python="py -3.12", register=False)
|
||||
folder = self.harbor_home / "integrations" / "kimi" / "harbor-test"
|
||||
manifest = json.loads((folder / "kimi.plugin.json").read_text(encoding="utf-8"))
|
||||
self.assertEqual(manifest["mcpServers"]["harbor"]["env"], {"HARBOR_AGENT": "Kimi", "HARBOR_PROJECT": "test", "HARBOR_HOST": "kimi"})
|
||||
self.assertEqual([h["event"] for h in manifest["hooks"]], ["UserPromptSubmit", "PreToolUse", "Stop"])
|
||||
self.assertTrue(all("--project test" in h["command"] for h in manifest["hooks"]))
|
||||
self.assertTrue((folder / "skills" / "harbor-test" / "SKILL.md").exists())
|
||||
|
||||
def test_refusals(self):
|
||||
with self.assertRaises(install.InstallError):
|
||||
install.install("codex", agent="Nobody")
|
||||
(self.home / ".config" / "opencode" / "opencode.jsonc").write_text('{ // mine\n}', encoding="utf-8")
|
||||
with self.assertRaises(install.InstallError):
|
||||
install.install("opencode")
|
||||
(self.home / ".codex" / "config.toml").write_text(CODEX_CONFIG + "\n[mcp_servers.harbor]\ncommand='x'\n",
|
||||
encoding="utf-8")
|
||||
with self.assertRaises(install.InstallError):
|
||||
install.install("codex")
|
||||
|
||||
def test_dry_run_writes_nothing(self):
|
||||
report = install.install("deepseek", dry_run=True, python="py -3.12")
|
||||
self.assertTrue(any(line.startswith("update") for line in report))
|
||||
self.assertEqual(self.read(".dsh", "profiles", "desktop", "cordis.patch.yml"), DSH_PATCH)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,347 @@
|
||||
"""Command line, host hooks, web server and MCP tools, each against a temporary Harbor home."""
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import unittest
|
||||
import urllib.request
|
||||
|
||||
from helpers import ROOT, env_for, http, make_home, run_module, temp_dir, wait_for_port
|
||||
from harbor.core import Harbor
|
||||
|
||||
|
||||
class Cli(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.dir = temp_dir()
|
||||
self.project = make_home(self.dir)["test"]
|
||||
|
||||
def tearDown(self):
|
||||
shutil.rmtree(self.dir, ignore_errors=True)
|
||||
|
||||
def test_round_trip_and_identity_rules(self):
|
||||
r = run_module(self.dir, "--as", "Codex", "send", "Kimi", "please", "review", "--thread", "fn:0x445310", "--ack")
|
||||
self.assertEqual(r.returncode, 0, r.stderr)
|
||||
self.assertIn("thread", r.stdout)
|
||||
r = run_module(self.dir, "--json", "inbox", HARBOR_AGENT="Kimi")
|
||||
self.assertEqual(json.loads(r.stdout)[0]["body"], "please review")
|
||||
tree = self.dir / "worktree-deepseek"
|
||||
tree.mkdir()
|
||||
(self.dir / ".harbor.toml").write_text('project = "test"\n', encoding="utf-8")
|
||||
r = run_module(self.dir, "doing", "drafting", "0x445310", cwd=tree) # identity from the folder
|
||||
self.assertIn("DeepSeek: drafting 0x445310", r.stdout)
|
||||
self.assertIn("doing: drafting 0x445310", run_module(self.dir, "--as", "Claude", "status").stdout)
|
||||
r = run_module(self.dir, "--as", "User", "send", "All", "pretend")
|
||||
self.assertNotEqual(r.returncode, 0)
|
||||
self.assertIn("chat page", r.stderr)
|
||||
r = run_module(self.dir, "send", "All", "who am I")
|
||||
self.assertIn("say who you are", r.stderr)
|
||||
r = run_module(self.dir, "--as", "Codex", "wait", "--timeout", "0.2")
|
||||
self.assertIn("No new messages", r.stdout)
|
||||
self.assertIn("From", run_module(self.dir, "digest", "--since", "1h").stdout)
|
||||
|
||||
def test_init_creates_a_project_and_marker(self):
|
||||
home = self.dir / "home2"
|
||||
folder = self.dir / "garden"
|
||||
home.mkdir()
|
||||
folder.mkdir()
|
||||
r = run_module(home, "init", "garden", "--title", "Garden app", "--agent", "Lead:Builds it",
|
||||
"--agent", "Helper", "--coordinator", "Lead", "--marker", str(folder))
|
||||
self.assertEqual(r.returncode, 0, r.stderr)
|
||||
r = run_module(home, "--as", "Helper", "whoami", cwd=folder)
|
||||
self.assertIn("project garden", r.stdout)
|
||||
r = run_module(home, "--as", "Helper", "send", "Lead", "hello", cwd=folder)
|
||||
self.assertEqual(r.returncode, 0, r.stderr)
|
||||
self.assertIn("garden", run_module(home, "projects").stdout)
|
||||
|
||||
|
||||
class Hooks(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.dir = temp_dir()
|
||||
self.project = make_home(self.dir)["test"]
|
||||
(self.dir / ".harbor.toml").write_text('project = "test"\n', encoding="utf-8")
|
||||
self.h = Harbor(self.project)
|
||||
|
||||
def tearDown(self):
|
||||
self.h.close()
|
||||
shutil.rmtree(self.dir, ignore_errors=True)
|
||||
|
||||
def hook(self, event, payload=None, host="claude", agent="DeepSeek"):
|
||||
return run_module(self.dir, "--as", agent, "hook", event, "--host", host,
|
||||
stdin=json.dumps(payload or {"cwd": str(self.dir)}))
|
||||
|
||||
def test_session_start_and_prompt_submit(self):
|
||||
self.h.send("Claude", ["DeepSeek"], "please look at 0x445310", importance="urgent")
|
||||
r = self.hook("session-start")
|
||||
context = json.loads(r.stdout)["hookSpecificOutput"]["additionalContext"]
|
||||
self.assertIn("please look at 0x445310", context)
|
||||
r = self.hook("prompt-submit", host="generic")
|
||||
self.assertIn("1 unread", r.stdout)
|
||||
|
||||
def denied(self, r):
|
||||
return r.returncode == 0 and r.stdout.strip() and \
|
||||
json.loads(r.stdout)["hookSpecificOutput"]["permissionDecision"] == "deny"
|
||||
|
||||
def test_pre_tool_blocks_only_while_stopped(self):
|
||||
call = {"cwd": str(self.dir), "tool_name": "Bash", "tool_input": {"command": "git commit -am x"}}
|
||||
self.assertEqual((self.hook("pre-tool", call).returncode, self.hook("pre-tool", call).stdout), (0, ""))
|
||||
self.h.set_flag("stop:all", "on", "Claude", "cli", reason="user asked")
|
||||
for host in ("claude", "codex", "deepseek", "kimi"):
|
||||
r = self.hook("pre-tool", call, host=host)
|
||||
self.assertTrue(self.denied(r), (host, r.stdout, r.stderr))
|
||||
self.assertIn("STOP in force", json.loads(r.stdout)["hookSpecificOutput"]["permissionDecisionReason"])
|
||||
r = self.hook("pre-tool", call, host="opencode")
|
||||
self.assertEqual(r.returncode, 2)
|
||||
self.assertIn("STOP in force", r.stderr)
|
||||
harbor_call = {"cwd": str(self.dir), "tool_name": "Bash", "tool_input": {"command": "harbor inbox"}}
|
||||
self.assertEqual(self.hook("pre-tool", harbor_call).stdout, "") # may still read messages
|
||||
sneaky = {"cwd": str(self.dir), "tool_name": "Bash", "tool_input": {"command": "harbor inbox && git push"}}
|
||||
self.assertTrue(self.denied(self.hook("pre-tool", sneaky)))
|
||||
mcp_call = {"cwd": str(self.dir), "tool_name": "mcp__harbor__inbox", "tool_input": {}}
|
||||
self.assertEqual(self.hook("pre-tool", mcp_call).stdout, "")
|
||||
|
||||
def test_stop_hook_keeps_agent_for_urgent_messages_once(self):
|
||||
self.assertEqual(self.hook("stop").stdout, "")
|
||||
self.h.send("Claude", ["DeepSeek"], "answer this first", ack_required=True)
|
||||
r = self.hook("stop", host="deepseek")
|
||||
decision = json.loads(r.stdout)
|
||||
self.assertEqual((r.returncode, decision["decision"]), (0, "block"))
|
||||
self.assertIn("#", decision["reason"])
|
||||
self.assertEqual(self.hook("stop", host="deepseek").stdout, "") # no loop on the same messages
|
||||
self.h.send("Claude", ["DeepSeek"], "and this", importance="urgent")
|
||||
self.assertEqual(self.hook("stop", {"cwd": str(self.dir), "stop_hook_active": True}).stdout, "")
|
||||
r = self.hook("stop", host="opencode")
|
||||
self.assertEqual(r.returncode, 2) # a new urgent message nudges once
|
||||
self.h.send("Claude", ["DeepSeek"], "plain note")
|
||||
self.assertEqual(self.hook("stop", host="codex").stdout, "") # plain notes do not interrupt
|
||||
|
||||
def test_outside_a_project_hooks_are_silent(self):
|
||||
elsewhere = self.dir / "elsewhere"
|
||||
elsewhere.mkdir()
|
||||
(self.dir / ".harbor.toml").unlink()
|
||||
self.h.set_flag("stop:all", "on", "Claude", "cli")
|
||||
r = self.hook("pre-tool", {"cwd": str(elsewhere), "tool_name": "Bash"})
|
||||
self.assertEqual((r.returncode, r.stdout, r.stderr), (0, "", ""))
|
||||
r = run_module(self.dir, "--as", "DeepSeek", "--project", "test", "hook", "pre-tool", "--host", "kimi",
|
||||
stdin=json.dumps({"cwd": str(elsewhere), "tool_name": "Shell"}))
|
||||
self.assertTrue(self.denied(r)) # a bound plugin still gates
|
||||
|
||||
def test_kimi_gets_the_brief_on_prompts(self):
|
||||
self.assertEqual(self.hook("prompt-submit", host="kimi").stdout, "")
|
||||
self.h.send("Claude", ["DeepSeek"], "please check 0x445310")
|
||||
self.assertIn("Harbor brief for DeepSeek", self.hook("prompt-submit", host="kimi").stdout)
|
||||
|
||||
def test_idle_and_unknown_agents(self):
|
||||
self.h.send("Claude", ["DeepSeek"], "new work")
|
||||
idle = json.loads(self.hook("idle", host="opencode").stdout)
|
||||
self.assertEqual((idle["unread"], bool(idle["prompt"])), (1, True))
|
||||
again = json.loads(self.hook("idle", host="opencode").stdout)
|
||||
self.assertEqual((again["unread"], again["prompt"]), (1, None)) # one nudge per message
|
||||
r = run_module(self.dir, "hook", "pre-tool", stdin=json.dumps({"cwd": str(self.dir)}))
|
||||
self.assertEqual((r.returncode, r.stdout), (0, "")) # not an agent: stay silent
|
||||
broken = {k: v for k, v in env_for(self.dir).items()}
|
||||
r = subprocess.run([sys.executable, "-B", "-m", "harbor", "--as", "DeepSeek", "--project", "nope", "hook",
|
||||
"pre-tool"], input="{}", capture_output=True, text=True, cwd=self.dir,
|
||||
env={**broken, "PYTHONPATH": str(ROOT)})
|
||||
self.assertEqual(r.returncode, 0) # fails open
|
||||
|
||||
|
||||
class Server(unittest.TestCase):
|
||||
PORT = 8798
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.dir = temp_dir()
|
||||
cls.projects = make_home(cls.dir, names=("alpha", "beta"))
|
||||
env = env_for(cls.dir, PYTHONPATH=str(ROOT))
|
||||
cls.proc = subprocess.Popen([sys.executable, "-B", "-m", "harbor", "serve", "--port", str(cls.PORT)],
|
||||
cwd=cls.dir, env=env, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
assert wait_for_port(cls.PORT), cls.proc.stdout.read() if cls.proc.poll() is not None else "no server"
|
||||
page = http(cls.PORT, "/")[1]
|
||||
cls.token = re.search(r'name="harbor-token" content="([^"]+)"', page).group(1)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
cls.proc.terminate()
|
||||
cls.proc.wait(timeout=10)
|
||||
cls.proc.stdout.close()
|
||||
shutil.rmtree(cls.dir, ignore_errors=True)
|
||||
|
||||
def post(self, path, body, **kw):
|
||||
return http(self.PORT, path, body, token=self.token, **kw)
|
||||
|
||||
def test_human_messages_only_from_the_page(self):
|
||||
self.assertEqual(http(self.PORT, "/send", {"to": "All", "text": "scripted"}, browser=False)[0], 403)
|
||||
self.assertEqual(self.post("/send", {"to": "All", "text": "x"}, headers={"X-Harbor-Token": "wrong"})[0], 403)
|
||||
self.assertEqual(self.post("/send", {"to": "All", "text": "x"}, headers={"Origin": "http://evil.example"})[0], 403)
|
||||
self.assertEqual(self.post("/send", {"to": "All", "text": "x"}, headers={"Sec-Fetch-Site": "cross-site"})[0], 403)
|
||||
self.assertEqual(http(self.PORT, "/messages", headers={"Host": "attacker.example:8798"})[0], 403)
|
||||
status, reply = self.post("/send", {"to": "Codex, Kimi", "text": "Please both look"})
|
||||
self.assertEqual(status, 200, reply)
|
||||
last = http(self.PORT, "/messages")[1][-1]
|
||||
self.assertEqual((last["from"], last["recipients"], last["origin"]), ("User", ["Codex", "Kimi"], "web"))
|
||||
self.assertEqual(http(self.PORT, f"/messages?after={last['id']}")[1], [])
|
||||
|
||||
def test_mentions_receipts_and_projects(self):
|
||||
status, reply = self.post("/p/beta/send", {"to": "All", "text": "@kimi @Codex: can you check this?",
|
||||
"ack": True})
|
||||
self.assertEqual((status, reply["recipients"]), (200, ["Kimi", "Codex"]))
|
||||
beta = Harbor(self.projects["beta"])
|
||||
beta.ack("Kimi", [reply["id"]])
|
||||
beta.close()
|
||||
receipts = http(self.PORT, f"/p/beta/api/receipts?ids={reply['id']}")[1][str(reply["id"])]
|
||||
self.assertEqual((receipts["acked"], receipts["waiting"]), (["Kimi"], ["Codex"]))
|
||||
self.assertEqual(http(self.PORT, "/messages")[1][-1]["text"] if http(self.PORT, "/messages")[1] else None,
|
||||
http(self.PORT, "/p/alpha/messages")[1][-1]["text"] if http(self.PORT, "/p/alpha/messages")[1] else None)
|
||||
self.assertNotIn("@kimi", json.dumps(http(self.PORT, "/p/alpha/messages")[1])) # projects stay apart
|
||||
listing = http(self.PORT, "/api/projects")[1]
|
||||
self.assertEqual([p["name"] for p in listing], ["alpha", "beta"])
|
||||
self.assertEqual(http(self.PORT, "/p/nope/messages")[0], 404)
|
||||
self.assertIn('<base href="/p/beta/">', http(self.PORT, "/p/beta/board")[1])
|
||||
status, css = http(self.PORT, "/p/beta/static/harbor.css") # the shared look, per project too
|
||||
self.assertEqual((status, ":root" in css), (200, True))
|
||||
self.assertEqual(http(self.PORT, "/static/icons.svg")[0], 200)
|
||||
for name in ("themes.css", "themes.js", "InterVariable.woff2"): # themes and the Inter font
|
||||
with urllib.request.urlopen(f"http://127.0.0.1:{self.PORT}/static/{name}", timeout=10) as r:
|
||||
self.assertEqual(r.status, 200, name)
|
||||
self.assertGreater(len(r.read()), 1000, name)
|
||||
self.assertEqual(http(self.PORT, "/static/server.py")[0], 404) # only the listed files
|
||||
|
||||
def test_board_flags_digest_and_files(self):
|
||||
status, reply = self.post("/api/flag", {"target": "all", "value": "on", "reason": "going home"})
|
||||
self.assertEqual(status, 200, reply)
|
||||
overview = http(self.PORT, "/api/overview")[1]
|
||||
self.assertEqual(next(a for a in overview["agents"] if a["name"] == "Codex")["stopped"], ["stop:all"])
|
||||
self.assertEqual(self.post("/api/flag", {"target": "all", "value": "off"})[0], 200)
|
||||
self.assertEqual(self.post("/api/flag", {"target": "Nobody", "value": "on"})[0], 400)
|
||||
digest = http(self.PORT, "/api/digest?since=1h")[1]
|
||||
self.assertEqual([f["value"] for f in digest["flags"]], ["on", "off"])
|
||||
alpha = Harbor(self.projects["alpha"])
|
||||
report = self.dir / "r.txt"
|
||||
report.write_text("hello", encoding="utf-8")
|
||||
sent = alpha.send("Kimi", ["User"], "report", attachments=[report])
|
||||
alpha.close()
|
||||
name = http(self.PORT, "/messages")[1][-1]["attachments"][0]
|
||||
self.assertEqual(http(self.PORT, f"/files/{name}")[1], "hello")
|
||||
self.assertEqual(http(self.PORT, "/files/..%2F..%2Fsecret")[0], 404)
|
||||
self.assertEqual(self.post("/api/thread-state", {"thread": "control", "state": "closed"})[0], 200)
|
||||
self.assertTrue(sent["id"])
|
||||
|
||||
|
||||
class ServerWork(unittest.TestCase):
|
||||
PORT = 8796
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.dir = temp_dir()
|
||||
cls.project = make_home(cls.dir)["test"]
|
||||
env = env_for(cls.dir, PYTHONPATH=str(ROOT))
|
||||
cls.proc = subprocess.Popen([sys.executable, "-B", "-m", "harbor", "serve", "--port", str(cls.PORT)],
|
||||
cwd=cls.dir, env=env, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
assert wait_for_port(cls.PORT)
|
||||
cls.token = re.search(r'name="harbor-token" content="([^"]+)"', http(cls.PORT, "/")[1]).group(1)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
cls.proc.terminate()
|
||||
cls.proc.wait(timeout=10)
|
||||
cls.proc.stdout.close()
|
||||
shutil.rmtree(cls.dir, ignore_errors=True)
|
||||
|
||||
def post(self, path, body):
|
||||
return http(self.PORT, path, body, token=self.token)
|
||||
|
||||
def test_tasks_presence_ping_prompt_doctor(self):
|
||||
status, r = self.post("/api/task", {"action": "add", "title": "Name 0x445310", "scope": "fn:0x445310",
|
||||
"priority": "high", "assignee": "Codex"})
|
||||
self.assertEqual(status, 200, r)
|
||||
task_id = r["task"]["id"]
|
||||
overview = http(self.PORT, "/api/overview")[1]
|
||||
self.assertEqual([t["id"] for t in overview["tasks"]], [task_id])
|
||||
self.assertEqual(self.post("/api/task", {"action": "update", "id": task_id, "state": "cancelled"})[0], 200)
|
||||
self.assertEqual(http(self.PORT, "/api/tasks")[1], [])
|
||||
self.assertEqual(len(http(self.PORT, "/api/tasks?all=1")[1]), 1)
|
||||
status, r = self.post("/api/presence", {"status": "away", "until": "2h", "note": "decide with Claude"})
|
||||
self.assertEqual((status, r["presence"]["status"]), (200, "away"))
|
||||
self.assertEqual(http(self.PORT, "/api/overview")[1]["presence"]["note"], "decide with Claude")
|
||||
status, r = self.post("/api/ping", {"agent": "Kimi"})
|
||||
self.assertEqual(status, 200, r)
|
||||
h = Harbor(self.project)
|
||||
ping = h.inbox("Kimi", mark_read=False)[-1]
|
||||
self.assertTrue(ping["ack_required"] and ping["importance"] == "urgent" and ping["sender"] == "User")
|
||||
self.assertIn("User is away", h.brief("Kimi"))
|
||||
h.handoff("Kimi", "halfway through batch 49")
|
||||
first = h.send("Codex", "All", "wrong claim")
|
||||
h.send("Codex", None, "right claim", corrects=first["id"])
|
||||
h.close()
|
||||
self.assertIn("halfway through batch 49", http(self.PORT, "/api/prompt?agent=Kimi")[1]["prompt"])
|
||||
marked = {m["id"]: m for m in http(self.PORT, "/messages")[1]}
|
||||
self.assertEqual(marked[first["id"]]["corrected_by"], [first["id"] + 1])
|
||||
self.assertEqual(self.post("/api/pin", {"id": first["id"]})[0], 200)
|
||||
self.assertTrue({m["id"]: m for m in http(self.PORT, "/messages")[1]}[first["id"]]["pinned"])
|
||||
self.assertEqual(len(http(self.PORT, f"/messages?before={first['id']}")[1]), first["id"] - 1)
|
||||
checks = http(self.PORT, "/api/doctor")[1]
|
||||
self.assertEqual(next(c for c in checks if c["area"] == "Server")["status"], "ok")
|
||||
self.assertEqual(self.post("/api/presence", {"status": "gone"})[0], 400)
|
||||
|
||||
|
||||
class Mcp(unittest.TestCase):
|
||||
def test_tools_bind_identity_and_round_trip(self):
|
||||
from fastmcp import Client
|
||||
from fastmcp.exceptions import ToolError
|
||||
from harbor import mcp_server
|
||||
d = temp_dir()
|
||||
project = make_home(d)["test"]
|
||||
mcp_server._state.clear()
|
||||
mcp_server._state["project"] = project
|
||||
saved = os.environ.get("HARBOR_AGENT")
|
||||
|
||||
async def scenario():
|
||||
async with Client(mcp_server.mcp) as client:
|
||||
names = {t.name for t in await client.list_tools()}
|
||||
self.assertTrue({"brief", "doing", "wait_for_messages", "send", "reply", "ack", "claim", "submit",
|
||||
"review", "land", "digest", "close_thread"} <= names)
|
||||
os.environ["HARBOR_AGENT"] = "DeepSeek"
|
||||
sent = (await client.call_tool("send", {"to": "Kimi", "text": "Please review 0x50A720",
|
||||
"thread": "fn:0x50A720", "ack": True})).data
|
||||
await client.call_tool("doing", {"text": "waiting for Kimi"})
|
||||
await client.call_tool("claim", {"scope": "fn:0x50A720"})
|
||||
item = (await client.call_tool("submit", {"title": "LayoutTextMesh", "scope": "fn:0x50A720",
|
||||
"commit": "abc1234"})).data
|
||||
os.environ["HARBOR_AGENT"] = "Kimi"
|
||||
self.assertIn("Please review 0x50A720", (await client.call_tool("brief", {})).data)
|
||||
waited = (await client.call_tool("wait_for_messages", {"timeout_seconds": 2})).data
|
||||
self.assertEqual([m["id"] for m in waited["messages"]], [sent["id"]])
|
||||
await client.call_tool("ack", {"message_ids": [sent["id"]], "note": "on it"})
|
||||
with self.assertRaises(ToolError):
|
||||
await client.call_tool("claim", {"scope": "fn:0x50A720"})
|
||||
with self.assertRaises(ToolError):
|
||||
await client.call_tool("land", {"item_id": item["id"], "commit": "f00"})
|
||||
status = (await client.call_tool("status", {})).data
|
||||
self.assertEqual(next(a for a in status["agents"] if a["name"] == "DeepSeek")["doing"],
|
||||
"waiting for Kimi")
|
||||
os.environ["HARBOR_AGENT"] = "User"
|
||||
with self.assertRaises(ToolError):
|
||||
await client.call_tool("send", {"to": "All", "text": "pretending"})
|
||||
return sent
|
||||
|
||||
try:
|
||||
sent = asyncio.run(scenario())
|
||||
finally:
|
||||
if saved is None:
|
||||
os.environ.pop("HARBOR_AGENT", None)
|
||||
else:
|
||||
os.environ["HARBOR_AGENT"] = saved
|
||||
mcp_server._state.pop("h").close()
|
||||
mcp_server._state.clear()
|
||||
h = Harbor(project)
|
||||
self.assertEqual(h.message(sent["id"])["origin"], "mcp")
|
||||
self.assertEqual(h.pending_acks(sender="DeepSeek"), [])
|
||||
h.close()
|
||||
shutil.rmtree(d, ignore_errors=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,168 @@
|
||||
"""Trial runs, roll calls, launch status, the live activity feed, experts and thread summaries."""
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
os.environ["HARBOR_NO_TOASTS"] = "1"
|
||||
from helpers import ROOT, HarborCase, env_for, http, make_home, run_module, temp_dir, wait_for_port
|
||||
from harbor.core import Harbor, HarborError
|
||||
from harbor.launch import KEEP_ACTIVITY, describe_target
|
||||
|
||||
|
||||
class TrialRuns(HarborCase):
|
||||
def test_trial_run_ends_by_itself(self):
|
||||
self.h.set_flag("stop:Codex", "on", "User", "web")
|
||||
with self.assertRaises(HarborError):
|
||||
self.h.set_flag("stop:Codex", "off", "Claude", "cli", until="30m") # only the human
|
||||
self.h.set_flag("stop:Codex", "off", "User", "web", until="30m")
|
||||
self.assertEqual(self.h.stopped("Codex"), [])
|
||||
self.assertIsNotNone(self.h.trial("Codex"))
|
||||
self.assertIn("Trial run: you may work until", self.h.brief("Codex"))
|
||||
self.assertIn("trial run until", self.h.inbox("Codex")[-1]["body"])
|
||||
self.clock.t += 31 * 60
|
||||
stops = self.h.stopped("Codex")
|
||||
self.assertEqual(len(stops), 1)
|
||||
self.assertIn("trial run ended", stops[0]["reason"])
|
||||
self.assertIsNone(self.h.trial("Codex"))
|
||||
with self.assertRaises(HarborError):
|
||||
self.h.claim("Codex", "fn:0x445310") # stopped again
|
||||
self.h.set_flag("stop:Codex", "off", "User", "web") # a real resume
|
||||
self.assertEqual(self.h.stopped("Codex"), [])
|
||||
|
||||
def test_trial_for_everyone_and_a_personal_stop(self):
|
||||
self.h.set_flag("stop:all", "on", "User", "web")
|
||||
self.h.set_flag("stop:all", "off", "User", "web", until="15m")
|
||||
self.h.set_flag("stop:Kimi", "on", "User", "web")
|
||||
self.assertEqual(self.h.stopped("Codex"), [])
|
||||
self.assertTrue(self.h.stopped("Kimi")) # its own stop holds
|
||||
self.clock.t += 16 * 60
|
||||
self.assertTrue(self.h.stopped("Codex"))
|
||||
|
||||
|
||||
class RollCall(HarborCase):
|
||||
def test_rollcall_works_while_stopped(self):
|
||||
self.h.set_flag("stop:all", "on", "User", "web")
|
||||
with self.assertRaises(HarborError):
|
||||
self.h.rollcall("Claude", "cli")
|
||||
status = self.h.rollcall("User", "web", agents=["Codex", "Kimi"])
|
||||
self.assertEqual([a["agent"] for a in status["agents"]], ["Codex", "Kimi"])
|
||||
q = next(q for q in self.h.questions(to="Codex"))
|
||||
self.h.inbox("Codex")
|
||||
self.clock.t += 42
|
||||
self.h.answer("Codex", q["id"], "Here") # stopped, still answers
|
||||
rows = {a["agent"]: a for a in self.h.rollcall_status()["agents"]}
|
||||
self.assertEqual((rows["Codex"]["answer"], rows["Codex"]["seconds"]), ("Here", 42))
|
||||
self.assertIsNone(rows["Kimi"]["answer"])
|
||||
self.assertTrue(any(m["kind"] == "answer" for m in self.h.inbox("User")))
|
||||
launch = {a["agent"]: a for a in self.h.launch_status()}
|
||||
self.assertEqual(launch["Codex"]["state"], "stopped")
|
||||
self.assertEqual(launch["Codex"]["rollcall"]["answer"], "Here")
|
||||
self.h.brief("Kimi")
|
||||
self.assertIsNotNone({a["agent"]: a for a in self.h.launch_status()}["Kimi"]["brief_at"])
|
||||
|
||||
|
||||
class Activity(HarborCase):
|
||||
def test_feed_targets_and_pruning(self):
|
||||
self.assertEqual(describe_target("Edit", {"file_path": "x.c"}, ["x.c"]), "x.c")
|
||||
masked = describe_target("Bash", {"command": "curl -H 'Authorization: Bearer abc123' --token=s3cret x"}, [])
|
||||
self.assertNotIn("abc123", masked)
|
||||
self.assertNotIn("s3cret", masked)
|
||||
self.assertEqual(describe_target("shell", {"command": ["bash", "-lc", "make all"]}, []), "make all")
|
||||
for i in range(KEEP_ACTIVITY + 25):
|
||||
self.h.activity("Codex", "shell", "codex", target=f"step {i}")
|
||||
feed = self.h.activity_feed(limit=5, agent="Codex")
|
||||
self.assertEqual(feed[0]["target"], f"step {KEEP_ACTIVITY + 24}")
|
||||
self.assertEqual(self.h.conn.execute("SELECT COUNT(*) FROM activity_log WHERE agent='Codex'").fetchone()[0],
|
||||
KEEP_ACTIVITY)
|
||||
|
||||
def test_hook_records_targets(self):
|
||||
(self.dir / ".harbor.toml").write_text('project = "test"\n', encoding="utf-8")
|
||||
payload = {"cwd": str(self.dir), "tool_name": "Edit", "tool_input": {"file_path": "annotations/names.csv"}}
|
||||
run_module(self.dir, "--as", "DeepSeek", "hook", "pre-tool", "--host", "claude", stdin=json.dumps(payload))
|
||||
self.assertEqual(self.h.activity_feed(limit=1)[0]["target"], "annotations/names.csv")
|
||||
|
||||
|
||||
class Knowledge(HarborCase):
|
||||
def test_experts_and_summaries(self):
|
||||
self.h.note("Kimi", "fn:0x52BD10", "stdcall, ret 4")
|
||||
self.h.claim("DeepSeek", "fn:0x52BD10")
|
||||
item = self.h.submit("DeepSeek", "AudioManagerTick", scope="fn:0x52BD10", commit="abc")
|
||||
self.h.transition("Kimi", item["id"], "approved")
|
||||
self.h.send("Codex", "All", "0x52BD10 takes the manager in ECX")
|
||||
ranked = [e["agent"] for e in self.h.experts("fn:0x52BD10")]
|
||||
self.assertEqual(sorted(ranked[:2]), ["DeepSeek", "Kimi"])
|
||||
self.assertIn("who knows this", self.h.context_text("fn:0x52BD10"))
|
||||
for i in range(21):
|
||||
self.h.send("Codex" if i % 2 else "Kimi", ["Claude"], f"point {i}", thread="fn:0x52BD10")
|
||||
self.assertIn("Long threads without a recent summary", self.h.brief("Kimi"))
|
||||
self.h.summarize("Kimi", "fn:0x52BD10", "Decided: stdcall ret 4; DeepSeek fixes the header.")
|
||||
self.assertNotIn("Long threads without", self.h.brief("Kimi"))
|
||||
self.assertIn("summary of fn:0x52BD10", self.h.context_text("fn:0x52BD10"))
|
||||
with self.assertRaises(HarborError):
|
||||
self.h.summarize("Kimi", "fn:0x52BD10", "x" * 4001)
|
||||
|
||||
|
||||
class ServerLaunch(unittest.TestCase):
|
||||
PORT = 8792
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.dir = temp_dir()
|
||||
cls.project = make_home(cls.dir)["test"]
|
||||
cls.proc = subprocess.Popen([sys.executable, "-B", "-m", "harbor", "serve", "--port", str(cls.PORT)],
|
||||
cwd=cls.dir, env=env_for(cls.dir, PYTHONPATH=str(ROOT)),
|
||||
stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
assert wait_for_port(cls.PORT)
|
||||
cls.token = re.search(r'name="harbor-token" content="([^"]+)"', http(cls.PORT, "/")[1]).group(1)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
cls.proc.terminate()
|
||||
cls.proc.wait(timeout=10)
|
||||
cls.proc.stdout.close()
|
||||
shutil.rmtree(cls.dir, ignore_errors=True)
|
||||
|
||||
def post(self, path, body):
|
||||
return http(self.PORT, path, body, token=self.token)
|
||||
|
||||
def test_launch_endpoints(self):
|
||||
self.assertEqual(self.post("/api/flag", {"target": "Codex", "value": "on"})[0], 200)
|
||||
status, r = self.post("/api/flag", {"target": "Codex", "value": "off", "until": "30m", "reason": "trial run"})
|
||||
self.assertEqual(status, 200, r)
|
||||
launch = {a["agent"]: a for a in http(self.PORT, "/api/launch")[1]["agents"]}
|
||||
self.assertEqual(launch["Codex"]["state"], "trial")
|
||||
status, r = self.post("/api/rollcall", {"agents": ["Kimi"]})
|
||||
self.assertEqual((status, [a["agent"] for a in r["rollcall"]["agents"]]), (200, ["Kimi"]))
|
||||
h = Harbor(self.project)
|
||||
h.activity("Kimi", "Edit", "kimi", target="src/a.c")
|
||||
h.summarize("Kimi", "rollcall", "Kimi is here.")
|
||||
h.close()
|
||||
self.assertEqual(http(self.PORT, "/api/feed")[1][0]["target"], "src/a.c")
|
||||
overview = http(self.PORT, "/api/overview")[1]
|
||||
kimi = next(a for a in overview["agents"] if a["name"] == "Kimi")
|
||||
self.assertEqual(kimi["last_target"], "src/a.c")
|
||||
self.assertIsNotNone(next(a for a in overview["agents"] if a["name"] == "Codex")["trial_until"])
|
||||
thread = http(self.PORT, "/api/thread?id=rollcall")[1]
|
||||
self.assertEqual(thread["summary"]["body"], "Kimi is here.")
|
||||
|
||||
|
||||
class CliLaunch(unittest.TestCase):
|
||||
def test_commands(self):
|
||||
d = temp_dir()
|
||||
make_home(d)
|
||||
try:
|
||||
run_module(d, "--as", "Kimi", "note", "fn:0x52BD10", "stdcall")
|
||||
self.assertIn("Kimi", run_module(d, "--as", "Codex", "who", "fn:0x52BD10").stdout)
|
||||
self.assertIn("harbor note", run_module(d, "--as", "Codex", "feed").stdout) # CLI calls are activity
|
||||
run_module(d, "--as", "Kimi", "send", "Claude", "x", "--thread", "t1")
|
||||
self.assertIn("pinned to t1", run_module(d, "--as", "Kimi", "summarize", "t1", "all", "settled").stdout)
|
||||
finally:
|
||||
shutil.rmtree(d, ignore_errors=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,188 @@
|
||||
"""Regression tests for the findings of the independent review of 2026-10-02 (one test per finding)."""
|
||||
import json
|
||||
import sqlite3
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import unittest
|
||||
|
||||
from helpers import HarborCase, ROOT, env_for, run_module
|
||||
from harbor import hooks, projects
|
||||
from harbor.core import Harbor, HarborError
|
||||
from harbor.server import chat_shape
|
||||
|
||||
|
||||
class Findings(HarborCase):
|
||||
def test_01_08_12_callers_cannot_forge_reserved_refs(self):
|
||||
human = self.h.send("User", "All", "rule", origin="web")
|
||||
evil = self.h.send("Codex", "All", "x", refs={"corrects": "<img src=x onerror=alert(1)>",
|
||||
"attachments": "x", "question": 1, "commit": "abc"})
|
||||
m = self.h.message(evil["id"])
|
||||
self.assertEqual(m["refs"], {"commit": "abc"})
|
||||
self.assertIsNone(m["corrects"])
|
||||
self.assertEqual(self.h.corrections_for([human["id"]]), {})
|
||||
shaped = chat_shape(m)
|
||||
self.assertEqual((shaped["corrects"], shaped["attachments"]), (None, []))
|
||||
|
||||
def test_02_a_coordinator_cannot_lift_the_humans_stop(self):
|
||||
self.h.set_flag("stop:all", "on", "User", "web")
|
||||
self.h.set_flag("stop:all", "on", "Claude", "cli") # a re-stop changes nothing
|
||||
self.assertEqual(self.h.flags()["stop:all"]["set_by"], "User")
|
||||
with self.assertRaises(HarborError):
|
||||
self.h.set_flag("stop:all", "off", "Claude", "cli")
|
||||
|
||||
def test_03_stop_gate_allows_only_plain_harbor_reads(self):
|
||||
allowed = ["py -3.12 -B R:/Workspace/Tools/Harbor/run.py --as Codex inbox",
|
||||
"py -3.12 -B R:\\Workspace\\Tools\\harbor.py brief",
|
||||
"harbor ack 12 --note done", "harbor --project open-seas handoff finished the strings"]
|
||||
refused = ["harbor inbox\nRemove-Item -Recurse R:/Workspace/build", "harbor inbox & del /q x",
|
||||
"harbor inbox > src/main.c", "git push --force origin harbor-fixes",
|
||||
"py R:/Tools/Harbor/build.py", "harbor next", "harbor claim fn:0x1", "harbor resume Codex --relay 9",
|
||||
"harbor inbox | tee x", "harbor send All `whoami`", "harbor send All $(id)"]
|
||||
for c in allowed:
|
||||
self.assertTrue(hooks._tool_is_harbor({"tool_name": "shell", "tool_input": {"command": c}}), c)
|
||||
for c in refused:
|
||||
self.assertFalse(hooks._tool_is_harbor({"tool_name": "shell", "tool_input": {"command": c}}), c)
|
||||
self.assertTrue(hooks._tool_is_harbor({"tool_name": "shell",
|
||||
"tool_input": {"command": ["bash", "-lc", "harbor inbox"]}}))
|
||||
self.assertFalse(hooks._tool_is_harbor({"tool_name": "shell",
|
||||
"tool_input": {"command": ["bash", "-lc", "rm -rf x"]}}))
|
||||
self.assertTrue(hooks._tool_is_harbor({"tool_name": "mcp__harbor__inbox"}))
|
||||
self.assertTrue(hooks._tool_is_harbor({"tool_name": "harbor_ack"}))
|
||||
for name in ("mcp__harbor__next_task", "mcp__harbor__land", "mcp__harbor__resume", "my_harbor_tool", "Harbor"):
|
||||
self.assertFalse(hooks._tool_is_harbor({"tool_name": name}), name)
|
||||
|
||||
def test_04_only_the_human_resumes_and_relays_only_stop(self):
|
||||
self.h.set_flag("stop:Codex", "on", "User", "web")
|
||||
ok = self.h.send("User", ["Codex"], "ok you can resume", origin="web")
|
||||
with self.assertRaises(HarborError):
|
||||
self.h.set_flag("stop:Codex", "off", "Codex", "cli", relay_of=ok["id"]) # resume never relays
|
||||
lunch = self.h.send("User", ["Kimi"], "lunch? then stop DeepSeek", origin="web")
|
||||
with self.assertRaises(HarborError):
|
||||
self.h.set_flag("stop:DeepSeek", "on", "Codex", "cli", relay_of=lunch["id"]) # not addressed to Codex
|
||||
vague = self.h.send("User", ["Codex"], "hello there DeepSeek", origin="web")
|
||||
with self.assertRaises(HarborError):
|
||||
self.h.set_flag("stop:DeepSeek", "on", "Codex", "cli", relay_of=vague["id"]) # does not ask to stop
|
||||
ask = self.h.send("User", ["Codex"], "please stop DeepSeek for now", origin="web")
|
||||
self.h.set_flag("stop:DeepSeek", "on", "Codex", "cli", relay_of=ask["id"])
|
||||
self.assertTrue(self.h.stopped("DeepSeek"))
|
||||
self.h.set_flag("stop:DeepSeek", "off", "User", "web")
|
||||
with self.assertRaises(HarborError):
|
||||
self.h.set_flag("stop:DeepSeek", "on", "Codex", "cli", relay_of=ask["id"]) # used once already
|
||||
late = self.h.send("User", ["Codex"], "stop DeepSeek", origin="web")
|
||||
self.clock.t += 4 * 3600
|
||||
with self.assertRaises(HarborError):
|
||||
self.h.set_flag("stop:DeepSeek", "on", "Codex", "cli", relay_of=late["id"]) # too old
|
||||
|
||||
def test_05_state_changes_are_checked_inside_the_transaction(self):
|
||||
task = self.h.add_task("Claude", "no scope")
|
||||
stale = self.h.task(task["id"])[0]
|
||||
self.h.take_task("Codex", task["id"])
|
||||
other = Harbor(self.project, clock=self.clock)
|
||||
|
||||
def race(c):
|
||||
other._move_task(c, stale, "Kimi", "in_progress", assignee="Kimi")
|
||||
with self.assertRaises(HarborError):
|
||||
other._write(race)
|
||||
other.close()
|
||||
self.assertEqual(self.h.task(task["id"])[0]["assignee"], "Codex")
|
||||
item = self.h.submit("DeepSeek", "x", commit="abc")
|
||||
self.h.transition("Claude", item["id"], "approved")
|
||||
snapshot = self.h.item(item["id"])[0]
|
||||
self.h.transition("Claude", item["id"], "landed", commit="b4b44f0")
|
||||
self.h.item = lambda _id, _s=snapshot: (_s, []) # Kimi still sees "approved"
|
||||
with self.assertRaises(HarborError):
|
||||
self.h.transition("Kimi", item["id"], "changes_requested")
|
||||
del self.h.item
|
||||
self.assertEqual(self.h.item(item["id"])[0]["state"], "landed")
|
||||
|
||||
def test_06_pre_tool_decides_quickly_while_the_database_is_locked(self):
|
||||
(self.dir / ".harbor.toml").write_text('project = "test"\n', encoding="utf-8")
|
||||
self.h.set_flag("stop:Codex", "on", "Claude", "cli")
|
||||
Harbor(self.project).close() # settle the schema signature
|
||||
lock = sqlite3.connect(str(self.project.db), timeout=1)
|
||||
lock.execute("BEGIN IMMEDIATE")
|
||||
try:
|
||||
started = time.monotonic()
|
||||
r = run_module(self.dir, "--as", "Codex", "hook", "pre-tool", "--host", "codex",
|
||||
stdin=json.dumps({"cwd": str(self.dir), "tool_name": "shell", "tool_input": {"command": "ls"}}))
|
||||
took = time.monotonic() - started
|
||||
finally:
|
||||
lock.rollback()
|
||||
lock.close()
|
||||
self.assertLess(took, 15)
|
||||
self.assertEqual(json.loads(r.stdout)["hookSpecificOutput"]["permissionDecision"], "deny")
|
||||
|
||||
def test_07_unknown_roster_names_do_not_crash(self):
|
||||
self.h.conn.execute("INSERT INTO agents(name, role, kind, active) VALUES ('Newbie', '', 'ai', 1)")
|
||||
self.assertIn("Newbie", [a["name"] for a in self.h.agents()])
|
||||
|
||||
def test_09_submit_validates_reviewers_before_writing(self):
|
||||
task = self.h.add_task("Claude", "t")
|
||||
self.h.take_task("Codex", task["id"])
|
||||
with self.assertRaises(HarborError):
|
||||
self.h.submit("Codex", "x", task=task["id"], reviewers=["Kimmi"])
|
||||
self.assertEqual(self.h.items(), [])
|
||||
self.assertEqual(self.h.task(task["id"])[0]["state"], "in_progress")
|
||||
|
||||
def test_10_review_events_respect_the_task_state_machine(self):
|
||||
task = self.h.add_task("Claude", "t")
|
||||
self.h.take_task("Codex", task["id"])
|
||||
item = self.h.submit("Codex", "x", task=task["id"])
|
||||
self.h.update_task("Claude", task["id"], "cancelled", note="not needed")
|
||||
self.h.transition("Claude", item["id"], "changes_requested")
|
||||
self.assertEqual(self.h.task(task["id"])[0]["state"], "cancelled")
|
||||
|
||||
def test_11_a_reply_answers_a_due_message(self):
|
||||
q = self.h.send("Claude", ["User"], "Rename it?", due="30m")
|
||||
self.h.send("User", None, "yes", reply_to=q["id"], origin="web")
|
||||
self.clock.t += 3600
|
||||
self.assertFalse(any(a["kind"] == "due_overdue" for a in self.h.attention()))
|
||||
|
||||
def test_15_pinned_filter(self):
|
||||
a = self.h.send("Codex", "All", "alpha one")
|
||||
self.h.send("Codex", "All", "alpha two")
|
||||
self.h.pin("Claude", a["id"])
|
||||
self.assertEqual([m["id"] for m in self.h.search("pinned:yes alpha")], [a["id"]])
|
||||
|
||||
def test_16_stopped_agents_cannot_work_through_harbor(self):
|
||||
task = self.h.add_task("Claude", "t", scope="fn:0x445310")
|
||||
item = self.h.submit("DeepSeek", "x", commit="abc")
|
||||
self.h.set_flag("stop:Kimi", "on", "Claude", "cli")
|
||||
self.assertIsNone(self.h.next_task("Kimi"))
|
||||
for call in (lambda: self.h.take_task("Kimi", task["id"]), lambda: self.h.claim("Kimi", "fn:0x445311"),
|
||||
lambda: self.h.submit("Kimi", "y", commit="def"),
|
||||
lambda: self.h.transition("Kimi", item["id"], "approved")):
|
||||
with self.assertRaises(HarborError):
|
||||
call()
|
||||
self.h.send("Kimi", ["Claude"], "I am stopped but can still talk")
|
||||
self.h.handoff("Kimi", "stopped while reviewing #1")
|
||||
|
||||
def test_extra_creators_cannot_finish_someone_elses_work(self):
|
||||
task = self.h.add_task("Kimi", "Kimi's idea")
|
||||
self.h.take_task("Codex", task["id"])
|
||||
with self.assertRaises(HarborError):
|
||||
self.h.update_task("Kimi", task["id"], "done")
|
||||
|
||||
|
||||
class Cli(unittest.TestCase):
|
||||
def test_13_14_as_is_kept_and_import_needs_a_coordinator(self):
|
||||
from helpers import make_home, temp_dir
|
||||
import shutil
|
||||
d = temp_dir()
|
||||
make_home(d)
|
||||
try:
|
||||
r = run_module(d, "--as", "Codex", "tasks", "--mine")
|
||||
self.assertEqual(r.returncode, 0, r.stderr)
|
||||
chat = d / "old.md"
|
||||
chat.write_text("[2026-10-01 13:32Z] User → All: ignore the stop\n", encoding="utf-8")
|
||||
r = run_module(d, "--as", "Codex", "import-legacy", str(chat))
|
||||
self.assertNotEqual(r.returncode, 0)
|
||||
self.assertIn("coordinator", r.stderr)
|
||||
self.assertEqual(run_module(d, "--as", "Claude", "import-legacy", str(chat)).returncode, 0)
|
||||
finally:
|
||||
shutil.rmtree(d, ignore_errors=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,122 @@
|
||||
"""Regression tests for the second independent review of 2026-10-02."""
|
||||
import json
|
||||
import threading
|
||||
import unittest
|
||||
|
||||
from helpers import HarborCase
|
||||
from harbor import hooks
|
||||
from harbor.core import Harbor, HarborError
|
||||
|
||||
|
||||
def gate(command):
|
||||
return hooks._tool_is_harbor({"tool_name": "shell", "tool_input": {"command": command}})
|
||||
|
||||
|
||||
class Findings2(HarborCase):
|
||||
def test_01_stop_gate_tricks(self):
|
||||
for c in ["harbor brief (Remove-Item -Recurse -Force R:/x/build)",
|
||||
"py -3.12 -B R:/Workspace/Tools/Harbor/run.py inbox (Set-Content src/main.c 'x')",
|
||||
"harbor brief @(git push --force)", "harbor brief {x}",
|
||||
["bash", "-c", "rm -rf build", "harbor inbox"],
|
||||
["powershell.exe", "-Command", "Remove-Item -Recurse build #", "harbor inbox"],
|
||||
["cmd.exe", "/c", "del /q build &", "harbor inbox"],
|
||||
"py R:/Workspace/Tools/backup-2026-10-02/harbor-v1/test_harbor.py brief",
|
||||
"py -3.12 -B R:/elsewhere/run.py inbox"]:
|
||||
self.assertFalse(gate(c), c)
|
||||
|
||||
def test_09_plain_harbor_commands_still_work_while_stopped(self):
|
||||
for c in ["harbor --json inbox", 'py -3.12 -B "R:/Workspace/Tools/Harbor/run.py" --as Codex inbox',
|
||||
'harbor handoff "parser done, lexer in flight"', 'harbor send Claude "half done"',
|
||||
"C:/Users/Administrator/AppData/Local/Programs/Python/Python312/python.exe -B "
|
||||
"R:/Workspace/Tools/Harbor/run.py brief", "py -3.12 -B R:\\Workspace\\Tools\\harbor.py brief",
|
||||
["bash", "-lc", "harbor inbox"], ["powershell", "-Command", "harbor brief"]]:
|
||||
self.assertTrue(gate(c), c)
|
||||
|
||||
def test_02_answers_cannot_be_relayed(self):
|
||||
self.h.set_flag("stop:Codex", "on", "User", "web")
|
||||
q = self.h.ask("Kimi", "Should Codex resume the parser work now? stop Codex?", ["No", "Yes"])
|
||||
self.h.answer("User", q["id"], "No", origin="web")
|
||||
answer_id = self.h.question(q["id"])["answer_message_id"]
|
||||
self.assertEqual(self.h.message(answer_id)["origin"], "system")
|
||||
with self.assertRaises(HarborError):
|
||||
self.h.set_flag("stop:Codex", "off", "Kimi", "cli", relay_of=answer_id)
|
||||
with self.assertRaises(HarborError):
|
||||
self.h.set_flag("stop:DeepSeek", "on", "Kimi", "cli", relay_of=answer_id) # not a typed note
|
||||
|
||||
def test_03_negations_do_not_resume(self):
|
||||
self.h.set_flag("stop:all", "on", "User", "web")
|
||||
m = self.h.send("User", "All", "Before anyone continues, I need to look at the build", origin="web")
|
||||
with self.assertRaises(HarborError):
|
||||
self.h.set_flag("stop:all", "off", "Codex", "cli", relay_of=m["id"])
|
||||
|
||||
def test_04_metric_must_be_finite(self):
|
||||
for bad in ("inf", "nan", "-inf"):
|
||||
with self.assertRaises(HarborError):
|
||||
self.h.metric("Claude", "x", bad)
|
||||
|
||||
def test_05_stopped_coordinator_does_not_block_assigned_work(self):
|
||||
task = self.h.add_task("Claude", "assigned", scope="fn:0x445310", assignee="Codex")
|
||||
self.h.set_flag("stop:Claude", "on", "Claude", "cli")
|
||||
self.assertEqual(self.h.next_task("Codex")["id"], task["id"])
|
||||
|
||||
def test_06_one_answer_wins(self):
|
||||
q = self.h.ask("Codex", "Ship?", ["Yes", "No"], fallback="Claude")
|
||||
self.h.set_presence("User", "web", "away")
|
||||
other = Harbor(self.project, clock=self.clock)
|
||||
results = []
|
||||
|
||||
def answer(h, who, value, origin):
|
||||
try:
|
||||
results.append(h.answer(who, q["id"], value, origin=origin)["answer"])
|
||||
except HarborError as error:
|
||||
results.append(str(error))
|
||||
t = threading.Thread(target=answer, args=(other, "Claude", "Yes", "cli"))
|
||||
t.start()
|
||||
answer(self.h, "User", "No", "web")
|
||||
t.join()
|
||||
other.close()
|
||||
final = self.h.question(q["id"])["answer"]
|
||||
self.assertEqual(sum(1 for r in results if r in ("Yes", "No")), 1, results)
|
||||
self.assertEqual(len([m for m in self.h.inbox("Codex", unread_only=False) if m["kind"] == "answer"]), 1)
|
||||
self.assertIn(final, results)
|
||||
|
||||
def test_07_relayed_stops_are_not_the_coordinators_to_lift(self):
|
||||
ask = self.h.send("User", ["Claude"], "stop everyone please", origin="web")
|
||||
self.h.set_flag("stop:all", "on", "Claude", "cli", relay_of=ask["id"])
|
||||
with self.assertRaises(HarborError):
|
||||
self.h.set_flag("stop:all", "off", "Claude", "cli")
|
||||
self.h.set_flag("stop:Codex", "on", "Codex", "cli", relay_of=ask["id"]) # self-stop: relay unused
|
||||
self.assertIsNone(self.h.flags()["stop:Codex"]["relay_of"])
|
||||
|
||||
def test_08_fallback_is_not_the_asker_and_the_human_hears_the_answer(self):
|
||||
with self.assertRaises(HarborError):
|
||||
self.h.ask("Claude", "May I force-push main?", ["Yes", "No"], fallback="Claude")
|
||||
q = self.h.ask("Codex", "Rename it?", ["Yes", "No"], fallback="Claude")
|
||||
self.h.set_presence("User", "web", "dnd")
|
||||
self.h.answer("Claude", q["id"], "Yes")
|
||||
told = [m for m in self.h.inbox("User") if m["kind"] == "answer"]
|
||||
self.assertEqual(len(told), 1)
|
||||
self.assertIn("answered by Claude while User was away", told[0]["body"])
|
||||
|
||||
def test_10_options_with_odd_spacing(self):
|
||||
q = self.h.ask("Codex", "Which?", ["Keep both", "Drop\nlast"])
|
||||
self.assertEqual(self.h.question(q["id"])["options"], ["Keep both", "Drop last"])
|
||||
self.h.answer("User", q["id"], "Keep both", origin="web")
|
||||
|
||||
def test_11_notes_are_replaced_only_by_their_author(self):
|
||||
kimi = self.h.note("Kimi", "fn:0x52BD10", "stdcall, ret 4")
|
||||
with self.assertRaises(HarborError):
|
||||
self.h.note("Codex", "file:unrelated.c", "x", replaces=kimi["id"])
|
||||
self.assertEqual(len(self.h.notes("fn:0x52BD10")), 1)
|
||||
self.h.note("Claude", "fn:0x52BD10", "stdcall, ret 4 (checked)", replaces=kimi["id"])
|
||||
|
||||
def test_12_paths(self):
|
||||
self.assertEqual(self.h._relative("Codex", ".github/workflows/ci.yml"), ".github/workflows/ci.yml")
|
||||
self.assertEqual(self.h._relative("Codex", ".env"), ".env")
|
||||
self.assertEqual(self.h._relative("Codex", "./src/a.c"), "src/a.c")
|
||||
patch = {"input": "*** Begin Patch\n*** Update File: R:\\ws\\new\\a.c\n*** Delete File: src/b.c\n*** End Patch"}
|
||||
self.assertEqual(self.h.edited_paths("shell", patch), ["R:\\ws\\new\\a.c", "src/b.c"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,373 @@
|
||||
"""Regression tests for the correctness review of rounds five and six."""
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import threading
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
os.environ["HARBOR_NO_TOASTS"] = "1"
|
||||
from helpers import ROOT, HarborCase, make_home, run_module, temp_dir
|
||||
from harbor import notify, projects
|
||||
from harbor.core import Harbor, HarborError
|
||||
|
||||
|
||||
class StopState(HarborCase):
|
||||
def test_02_trial_for_one_agent_while_everyone_is_stopped(self):
|
||||
self.h.set_flag("stop:all", "on", "User", "web")
|
||||
self.clock.t += 60
|
||||
self.h.set_flag("stop:Codex", "off", "User", "web", until="30m")
|
||||
self.assertEqual((self.h.stopped("Codex"), bool(self.h.trial("Codex"))), ([], True))
|
||||
self.assertTrue(self.h.stopped("Kimi"))
|
||||
self.clock.t += 60
|
||||
self.h.set_flag("stop:all", "on", "Claude", "cli") # an agent's re-stop changes nothing
|
||||
self.assertFalse(self.h.stopped("Codex"))
|
||||
self.h.set_flag("stop:all", "on", "User", "web") # the human pressing it again is a
|
||||
self.assertTrue(self.h.stopped("Codex")) # new decision: it covers everyone
|
||||
|
||||
def test_03_coordinator_stop_and_lift_keep_the_trial_end(self):
|
||||
self.h.set_flag("stop:Codex", "on", "User", "web")
|
||||
self.h.set_flag("stop:Codex", "off", "User", "web", until="30m")
|
||||
self.h.set_flag("stop:Codex", "on", "Claude", "cli")
|
||||
self.h.set_flag("stop:Codex", "off", "Claude", "cli")
|
||||
self.assertIsNotNone(self.h.trial("Codex"))
|
||||
self.clock.t += 31 * 60
|
||||
self.assertTrue(self.h.stopped("Codex")) # the trial still ends
|
||||
|
||||
def test_04_ended_trial_is_a_stop_everywhere(self):
|
||||
(self.dir / "x").mkdir()
|
||||
self.h.set_flag("stop:Codex", "on", "User", "web")
|
||||
self.h.set_flag("stop:Codex", "off", "User", "web", until="1m")
|
||||
self.clock.t += 120
|
||||
self.assertTrue(self.h.stopped("Codex"))
|
||||
self.assertIn("trial run ended", self.h.stopped("Codex")[0]["reason"])
|
||||
launch = {a["agent"]: a for a in self.h.launch_status()}
|
||||
self.assertEqual(launch["Codex"]["state"], "stopped")
|
||||
|
||||
def test_07_launch_shows_team_trials(self):
|
||||
self.h.set_flag("stop:all", "on", "User", "web")
|
||||
self.clock.t += 60
|
||||
self.h.set_flag("stop:all", "off", "User", "web", until="15m")
|
||||
self.assertEqual({a["agent"]: a for a in self.h.launch_status()}["Kimi"]["state"], "trial")
|
||||
|
||||
|
||||
class RollCalls(HarborCase):
|
||||
def test_06_second_roll_call_keeps_the_first(self):
|
||||
self.h.rollcall("User", "web")
|
||||
codex_q = next(q for q in self.h.questions(to="Codex"))
|
||||
self.h.answer("Codex", codex_q["id"], "Here")
|
||||
self.h.rollcall("User", "web", agents=["Kimi"])
|
||||
rows = {r["agent"]: r for r in self.h.rollcall_status()["agents"]}
|
||||
self.assertEqual(rows["Codex"]["answer"], "Here")
|
||||
self.assertEqual(len(rows), 5)
|
||||
self.assertEqual(len([q for q in self.h.questions(to="Kimi")]), 1) # the older one is closed
|
||||
|
||||
|
||||
class Notifications(HarborCase):
|
||||
def test_05_backlog_over_fifty(self):
|
||||
notify.set_enabled(self.h, True)
|
||||
for i in range(60):
|
||||
self.h.send("Codex", "All", f"broadcast {i}")
|
||||
self.h.send("Codex", ["User"], "the one that matters")
|
||||
pending, top = notify.pending_for_human(self.h)
|
||||
self.assertEqual([m["body"] for m in pending], ["the one that matters"])
|
||||
|
||||
|
||||
class FlowFixes(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.dir = temp_dir()
|
||||
|
||||
def tearDown(self):
|
||||
shutil.rmtree(self.dir, ignore_errors=True)
|
||||
|
||||
def project(self, **extra):
|
||||
data = {"name": "f", "coordinators": ["Lead"], "database": str(self.dir / "f.db"),
|
||||
"agents": [{"name": "Lead"}, {"name": "Ann"}, {"name": "Bob"}], **extra}
|
||||
return projects.project_from_dict(data)
|
||||
|
||||
def test_08_09_bulk_and_templates_by_a_worker(self):
|
||||
h = Harbor(self.project(templates={"t": {"steps": [{"name": "a", "title": "Fix {name} in {scope}",
|
||||
"assignee": "Bob"}]}}))
|
||||
h.claim("Lead", "range:0x440000-0x47FFFF", kind="band", on_behalf_of="Bob")
|
||||
r = h.bulk_tasks("Ann", "0x445310 One\n0x445320 Two")
|
||||
self.assertEqual([t["assignee"] for t in r["created"]], [None, None]) # no routing to someone else
|
||||
with self.assertRaises(HarborError):
|
||||
h.run_template("Ann", "t", scope="fn:0x445400")
|
||||
self.assertEqual(len(h.tasks()), 2) # nothing half-created
|
||||
task = h.run_template("Lead", "t", scope="fn:0x445400")[0]
|
||||
self.assertEqual(task["title"], "Fix {name} in fn:0x445400")
|
||||
h.close()
|
||||
|
||||
def test_11_policy_checked_at_load(self):
|
||||
p = self.project(policy={"land_requires": "Ann"})
|
||||
self.assertEqual(p.policy["land_requires"], ["Ann"])
|
||||
for bad in ({"land_requires": ["Ghost"]}, {"all_per_hour": "lots"}):
|
||||
with self.assertRaises(projects.ProjectError):
|
||||
self.project(policy=bad)
|
||||
h = Harbor(self.project(policy={"land_requires": ["Ann"]}))
|
||||
item = h.submit("Ann", "x", commit="abc")
|
||||
h.transition("Lead", item["id"], "approved")
|
||||
h.transition("Lead", item["id"], "landed", commit="b4") # the author is not required
|
||||
h.close()
|
||||
|
||||
def test_10_rebase_does_not_look_new(self):
|
||||
env = {**os.environ, "GIT_AUTHOR_NAME": "t", "GIT_AUTHOR_EMAIL": "t@x", "GIT_COMMITTER_NAME": "t",
|
||||
"GIT_COMMITTER_EMAIL": "t@x", "GIT_AUTHOR_DATE": "2026-01-01T10:00:00", "GIT_COMMITTER_DATE": "2026-01-01T10:00:00"}
|
||||
repo = self.dir / "r"
|
||||
repo.mkdir()
|
||||
g = lambda *a, e=env: subprocess.run(["git", "-C", str(repo), *a], check=True, capture_output=True, env=e)
|
||||
g("init", "-q", "-b", "main")
|
||||
(repo / "a").write_text("a")
|
||||
g("add", "a")
|
||||
g("commit", "-q", "-m", "base")
|
||||
g("checkout", "-q", "-b", "work")
|
||||
(repo / "b").write_text("b")
|
||||
g("add", "b")
|
||||
g("commit", "-q", "-m", "draft")
|
||||
h = Harbor(projects.project_from_dict({"name": "g", "database": str(self.dir / "g.db"),
|
||||
"agents": [{"name": "Ann", "workdir": str(repo)}]}))
|
||||
h.clock = lambda: 1767268800.0 + 3600 * 5 # 2026-01-01 15:00 UTC
|
||||
sha = h.git_status("Ann", max_age=0)["last_commit"]["sha"]
|
||||
h.submit("Ann", "draft", commit=sha, reviewers=["User"])
|
||||
h.clock = lambda: 1767268800.0 + 3600 * 6
|
||||
g("checkout", "-q", "main")
|
||||
(repo / "c").write_text("c")
|
||||
g("add", "c")
|
||||
g("commit", "-q", "-m", "main moves", e={**env, "GIT_AUTHOR_DATE": "2026-01-01T16:00:00",
|
||||
"GIT_COMMITTER_DATE": "2026-01-01T16:00:00"})
|
||||
g("checkout", "-q", "work")
|
||||
g("rebase", "-q", "main", e={**env, "GIT_COMMITTER_DATE": "2026-01-01T16:30:00"})
|
||||
self.assertEqual(h.git_status("Ann", max_age=0)["unsubmitted"], [])
|
||||
h.close()
|
||||
|
||||
|
||||
class Pages(unittest.TestCase):
|
||||
def test_01_no_code_hidden_behind_a_line_comment(self):
|
||||
for page in ("app.html",):
|
||||
scripts = re.findall(r"<script>([\s\S]*?)</script>", (ROOT / "harbor" / "web" / page).read_text(encoding="utf-8"))
|
||||
self.assertTrue(scripts, page)
|
||||
for line in "\n".join(scripts).splitlines():
|
||||
code = re.sub(r"'(?:\\.|[^'\\])*'|\"(?:\\.|[^\"\\])*\"|`(?:\\.|[^`\\])*`|/(?:\\.|[^/\\\n])+/[gimsuy]*", "", line)
|
||||
if "//" in code:
|
||||
after = code.split("//", 1)[1]
|
||||
self.assertFalse(re.search(r"\b(const|let|var|function|if|return)\b", after), (page, line[:120]))
|
||||
|
||||
|
||||
class Race(HarborCase):
|
||||
def test_answer_race_for_real(self):
|
||||
q = self.h.ask("Codex", "Ship?", ["Yes", "No"], fallback="Claude")
|
||||
self.h.set_presence("User", "web", "away")
|
||||
results, gate = [], threading.Barrier(2)
|
||||
|
||||
def answer(who, value, origin):
|
||||
h = Harbor(self.project, clock=self.clock)
|
||||
try:
|
||||
gate.wait()
|
||||
results.append(h.answer(who, q["id"], value, origin=origin)["answer"])
|
||||
except HarborError as error:
|
||||
results.append(f"refused: {error}")
|
||||
finally:
|
||||
h.close()
|
||||
threads = [threading.Thread(target=answer, args=a) for a in (("Claude", "Yes", "cli"), ("User", "No", "web"))]
|
||||
[t.start() for t in threads]
|
||||
[t.join() for t in threads]
|
||||
self.assertEqual(sum(1 for r in results if not r.startswith("refused")), 1, results)
|
||||
self.assertEqual(len([m for m in self.h.inbox("Codex", unread_only=False) if m["kind"] == "answer"]), 1)
|
||||
|
||||
|
||||
class LaunchFindings(unittest.TestCase):
|
||||
"""What the first launch with the real agent apps showed."""
|
||||
|
||||
def gate(self, command=None, tool="shell"):
|
||||
from harbor import hooks
|
||||
payload = {"tool_name": tool}
|
||||
if command is not None:
|
||||
payload["tool_input"] = {"command": command}
|
||||
return hooks._tool_is_harbor(payload)
|
||||
|
||||
def test_messages_may_hold_shell_characters_inside_double_quotes(self):
|
||||
run = "py -3.12 -B R:/Workspace/Tools/Harbor/run.py --as Kimi "
|
||||
for c in [run + 'send Claude --thread launch "Kimi is online (through the command; MCP was blocked) & ready"',
|
||||
'harbor ack 1708 --note "ok, I\'m stopped <as asked> {no work} @Claude ^"',
|
||||
'harbor send Claude "it\u2019s fine"', "harbor send Claude 'plain words'"]:
|
||||
self.assertTrue(self.gate(c), c)
|
||||
for c in ['harbor send All "$(id)"', 'harbor send All "a" ; rm x', "harbor send All '\"'; rm x; '\"'",
|
||||
'harbor send All "x\\" ; rm y ; \\"z"', 'harbor send All "100%"', 'harbor send All "line\nRemove-Item x"',
|
||||
'harbor send All "a\u201d ; rm x ; \u201cb"', "harbor send All 'it (x)'", 'harbor send All "unterminated',
|
||||
'harbor send All "`whoami`"', 'harbor send All \\"x\\"']:
|
||||
self.assertFalse(self.gate(c), c)
|
||||
|
||||
def test_every_hosts_harbor_tool_names_and_tool_loaders(self):
|
||||
for name in ("mcp__plugin-harbor-open-seas_harbor__send", "mcp__harbor-open-seas__brief", "harbor-open-seas.inbox",
|
||||
"mcp__harbor__wait_for_messages", "harbor_ack", "select_tools", "ToolSearch"):
|
||||
self.assertTrue(self.gate(tool=name), name)
|
||||
for name in ("mcp__plugin-harbor-open-seas_harbor__land", "mcp__plugin-harbor-open-seas_harbor__next_task",
|
||||
"mcp__other__send", "my_harbor_tool", "Harbor", "mcp__plugin-harbor-open-seas_harbor__resume"):
|
||||
self.assertFalse(self.gate(tool=name), name)
|
||||
|
||||
def test_the_stop_message_says_sending_is_allowed(self):
|
||||
from harbor import hooks
|
||||
|
||||
class FakeHarbor:
|
||||
human = "User"
|
||||
text = hooks._stop_text(FakeHarbor(), "Kimi", [{"key": "stop:Kimi", "set_by": "User", "set_at": "now", "reason": None}])
|
||||
self.assertIn("send", text)
|
||||
self.assertIn("double quotes", text)
|
||||
|
||||
|
||||
class AllocationWhileStopped(HarborCase):
|
||||
def test_coordinator_records_bands_for_stopped_agents_but_no_work(self):
|
||||
self.h.set_flag("stop:Codex", "on", "User", "web")
|
||||
self.h.claim("Claude", "range:0x401000-0x43FFFF", kind="band", on_behalf_of="Codex")
|
||||
self.h.claim("Claude", "fn:0x401200", kind="naming", on_behalf_of="Codex")
|
||||
with self.assertRaises(HarborError):
|
||||
self.h.claim("Claude", "fn:0x401300", kind="draft", on_behalf_of="Codex")
|
||||
with self.assertRaises(HarborError):
|
||||
self.h.claim("Codex", "fn:0x401400")
|
||||
|
||||
|
||||
class BulkNotice(HarborCase):
|
||||
def test_bulk_sends_one_notice_per_assignee(self):
|
||||
self.h.claim("Claude", "range:0x401000-0x43FFFF", kind="band", on_behalf_of="Codex")
|
||||
before = len(self.h.inbox("Codex", unread_only=False, limit=500, mark_read=False))
|
||||
r = self.h.bulk_tasks("Claude", "0x401100 One\n0x401200 Two\n0x401300 Three")
|
||||
self.assertEqual([t["assignee"] for t in r["created"]], ["Codex"] * 3)
|
||||
new = self.h.inbox("Codex", unread_only=False, limit=500, mark_read=False)[before:]
|
||||
self.assertEqual(len(new), 1)
|
||||
self.assertIn("3 new tasks", new[0]["body"])
|
||||
|
||||
|
||||
class Autopilot(HarborCase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.h.project.autopilot = True
|
||||
|
||||
def test_running_agents_keep_going_and_stopped_or_stuck_ones_rest(self):
|
||||
self.h.claim("Claude", "range:0x401000-0x43FFFF", kind="band", on_behalf_of="Codex")
|
||||
t = self.h.add_task("Claude", "Make 0x401100 readable", scope="fn:0x401100", assignee="Codex")
|
||||
self.assertIn(f"#{t['id']}", self.h.continuation("Codex")) # offers the next task
|
||||
self.h.take_task("Codex", t["id"])
|
||||
self.assertIn(f"Continue task #{t['id']}", self.h.continuation("Codex"))
|
||||
self.assertIsNotNone(self.h.continuation("Codex"))
|
||||
self.assertIsNone(self.h.continuation("Codex")) # three idle turn ends: rest
|
||||
self.assertEqual([a["agent"] for a in self.h.attention() if a["kind"] == "resting"], ["Codex"])
|
||||
self.h.activity("Codex", "Edit", "codex", target="src/x.c") # real work resets the guard
|
||||
self.assertIsNotNone(self.h.continuation("Codex"))
|
||||
self.assertEqual([a for a in self.h.attention() if a["kind"] == "resting"], [])
|
||||
self.h.set_flag("stop:Codex", "on", "User", "web")
|
||||
self.assertIsNone(self.h.continuation("Codex")) # the human's stop rests it
|
||||
|
||||
def test_band_fallback_and_reviewer_work(self):
|
||||
self.h.claim("Claude", "range:0x401000-0x43FFFF", kind="band", on_behalf_of="Codex")
|
||||
self.assertIn("own band", self.h.continuation("Codex"))
|
||||
self.assertIn("wait_for_messages", self.h.continuation("Kimi"))
|
||||
item = self.h.submit("Codex", "Readable 0x401100", commit="abc123")
|
||||
self.assertIn(f"#{item['id']}", self.h.continuation("Kimi"))
|
||||
|
||||
|
||||
class LandMany(HarborCase):
|
||||
def test_batch_landing_sends_one_message_per_author_and_one_win(self):
|
||||
ids = [self.h.submit("Codex", f"Readable 0x40{i}000", commit=f"c{i}")["id"] for i in range(3)]
|
||||
before = self.h.conn.execute("SELECT COUNT(*) FROM messages").fetchone()[0]
|
||||
landed = self.h.land_many("Claude", ids, "abc1234", note="readable check passed")
|
||||
self.assertEqual([i["state"] for i in landed], ["landed"] * 3)
|
||||
self.assertEqual(self.h.conn.execute("SELECT COUNT(*) FROM messages").fetchone()[0] - before, 2)
|
||||
with self.assertRaises(HarborError):
|
||||
self.h.land_many("Codex", ids, "abc1234")
|
||||
|
||||
|
||||
class AuthorRefreshesCommit(HarborCase):
|
||||
def test_author_hands_in_a_newer_commit_while_submitted_or_approved(self):
|
||||
item = self.h.submit("Codex", "Readable 0x401000", commit="c1")
|
||||
self.h.transition("Codex", item["id"], "submitted", note="newer", commit="c2")
|
||||
self.assertEqual(self.h.item(item["id"])[0]["commit_hash"], "c2")
|
||||
self.h.transition("Kimi", item["id"], "approved")
|
||||
self.h.transition("Codex", item["id"], "submitted", note="one more fix", commit="c3")
|
||||
self.assertEqual(self.h.item(item["id"])[0]["state"], "submitted") # back for review with c3
|
||||
with self.assertRaises(HarborError):
|
||||
self.h.transition("Codex", item["id"], "submitted") # no commit: nothing to refresh
|
||||
with self.assertRaises(HarborError):
|
||||
self.h.transition("Kimi", item["id"], "submitted", commit="c4") # only the author
|
||||
|
||||
|
||||
class ProjectRefresh(unittest.TestCase):
|
||||
def test_edits_to_the_project_file_apply_without_a_restart(self):
|
||||
d = temp_dir()
|
||||
try:
|
||||
path = d / "p.toml"
|
||||
body = 'name = "p"\ndatabase = \'{db}\'\nreviewers = ["Ann"]\n[[agents]]\nname = "Ann"\n[[agents]]\nname = "Bob"\n'
|
||||
path.write_text(body.format(db=d / "p.db"), encoding="utf-8")
|
||||
h = Harbor(projects.load_file(path))
|
||||
self.assertFalse(h.refresh_project())
|
||||
path.write_text(body.format(db=d / "p.db").replace('reviewers = ["Ann"]', 'reviewers = ["Bob"]\nautopilot = true'),
|
||||
encoding="utf-8")
|
||||
os.utime(path, (os.path.getmtime(path) + 5,) * 2)
|
||||
self.assertTrue(h.refresh_project())
|
||||
self.assertEqual((h.project.reviewers, h.project.autopilot), (("Bob",), True))
|
||||
path.write_text("this is not toml [", encoding="utf-8")
|
||||
os.utime(path, (os.path.getmtime(path) + 10,) * 2)
|
||||
self.assertFalse(h.refresh_project()) # keeps the last good one
|
||||
self.assertEqual(h.project.reviewers, ("Bob",))
|
||||
h.close()
|
||||
finally:
|
||||
shutil.rmtree(d, ignore_errors=True)
|
||||
|
||||
|
||||
class SharedFiles(HarborCase):
|
||||
def test_shared_annotation_files_are_not_collisions(self):
|
||||
self.h.project.shared_files = ("annotations/*.csv", "annotations/prototypes.h")
|
||||
self.h.record_touches("Codex", ["annotations/names.csv", "annotations/prototypes.h", "src/a.c"])
|
||||
cols = self.h.record_touches("Kimi", ["annotations/names.csv", "annotations/prototypes.h", "src/a.c"])
|
||||
self.assertEqual([c["path"] for c in cols], ["src/a.c"])
|
||||
self.assertEqual([c["path"] for c in self.h.recent_collisions()], ["src/a.c"])
|
||||
|
||||
|
||||
class WorkingWithoutHooks(HarborCase):
|
||||
def test_harbor_calls_and_file_scans_count_as_work_but_not_as_hooks(self):
|
||||
self.h.activity("DeepSeek", "harbor submit", "deepseek", channel="mcp")
|
||||
row = next(a for a in self.h.agents() if a["name"] == "DeepSeek")
|
||||
self.assertTrue(self.h.working(row))
|
||||
self.assertIsNone({r["agent"]: r for r in self.h.launch_status()}["DeepSeek"]["hooks"])
|
||||
self.h.activity("DeepSeek", "edit", "", target="src/x.c", channel="files")
|
||||
self.assertIn("files", {c["channel"] for c in self.h.connections()["DeepSeek"]})
|
||||
|
||||
|
||||
class UnreadQuestions(HarborCase):
|
||||
def test_unread_questions_to_an_agent_need_attention_until_read(self):
|
||||
q = self.h.send("DeepSeek", ["Claude"], "May I take the 0x4C slice?", kind="question")
|
||||
self.h.send("DeepSeek", ["All"], "status for everyone", kind="request")
|
||||
kinds = lambda: [a for a in self.h.attention() if a["kind"] == "unread_questions"]
|
||||
self.assertEqual(kinds(), []) # not yet 20 minutes old
|
||||
self.clock.t += 25 * 60
|
||||
found = kinds()
|
||||
self.assertEqual([(a["agent"], f"#{q['id']}" in a["text"]) for a in found], [("Claude", True)])
|
||||
self.h.inbox("Claude") # reading it clears the item
|
||||
self.assertEqual(kinds(), [])
|
||||
|
||||
|
||||
class AttentionWithoutFalseAlarms(HarborCase):
|
||||
def test_stopped_agents_owe_no_acks_and_file_scopes_carry_their_address(self):
|
||||
from harbor.util import parse_scope
|
||||
self.assertEqual(parse_scope("file:src/game/_unassigned/4C6540_UnkPediaTextHelper.c")[1:], (0x4C6540, 0x4C6540))
|
||||
self.assertEqual(parse_scope("file:annotations/names.csv")[1:], (None, None))
|
||||
m = self.h.send("Codex", ["Kimi"], "please confirm", ack_required=True)
|
||||
self.clock.t += 120 * 60
|
||||
self.assertTrue(any(a["kind"] == "ack_overdue" for a in self.h.attention()))
|
||||
self.h.set_flag("stop:Kimi", "on", "Claude", "cli")
|
||||
self.assertFalse(any(a["kind"] == "ack_overdue" and f"#{m['id']}" in a["text"] for a in self.h.attention()))
|
||||
|
||||
|
||||
class HooksVisible(HarborCase):
|
||||
def test_mcp_without_hooks_needs_attention(self):
|
||||
self.h.connect("Codex", "mcp", "codex")
|
||||
no_hooks = lambda: [a["agent"] for a in self.h.attention() if a["kind"] == "no_hooks"]
|
||||
launch = lambda: {r["agent"]: r for r in self.h.launch_status()}["Codex"]["hooks"]
|
||||
self.assertEqual((no_hooks(), launch()), (["Codex"], None))
|
||||
self.h.connect("Codex", "hook", "codex")
|
||||
self.assertEqual(no_hooks(), [])
|
||||
self.assertTrue(launch())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,224 @@
|
||||
"""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()
|
||||
Reference in new issue
Block a user