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.
135 lines
5.0 KiB
Python
135 lines
5.0 KiB
Python
"""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
|