"""Issue #72 - the multi-agent-mux-update skill. Every test updates a copy of one workspace installed from this checkout (MAM_REPO_URL, MAM_SKIP_VENV=1). herdr is a fake that answers `session list --json` and `--session S agent list` from a JSON spec, so the pre-check's view of the live agents is set per test; the monitor daemon is a stub reconcile.sh that holds .mam/monitor.lock like the real one. Nothing here touches a real herdr server or the control root's monitor. """ from __future__ import annotations import fcntl import hashlib import json import os import shutil import signal import sqlite3 import subprocess import sys import time from pathlib import Path import pytest import yaml REPO_ROOT = Path(__file__).resolve().parent.parent UPDATE_REL = ".agents/skills/multi-agent-mux-update/scripts/update.sh" MONITOR_REL = ".agents/skills/multi-agent-mux-monitor/scripts/reconcile.sh" FAKE_HERDR = r'''#!/usr/bin/env python3 import json, os, sys spec = json.load(open(os.environ["FAKE_HERDR_SPEC"])) a = sys.argv[1:] server = "default" if a[:1] == ["--session"]: server, a = a[1], a[2:] with open(os.environ["FAKE_HERDR_SPEC"] + ".calls", "a") as f: f.write(json.dumps([server] + a) + "\n") if a[:2] == ["session", "list"]: if spec.get("fail") == "session-list": sys.exit(1) print(json.dumps({"sessions": [{"name": n, "running": r} for n, r in spec.get("sessions", {"default": True}).items()]})) elif a[:2] == ["agent", "list"]: if spec.get("fail") == "agent-list:" + server: sys.exit(1) print(json.dumps({"result": {"agents": spec.get("agents", {}).get(server, [])}})) elif a[:2] == ["agent", "get"]: hit = [x for x in spec.get("agents", {}).get(server, []) if a[2] in (x.get("name"), x.get("pane_id"))] if not hit: sys.exit(1) print(json.dumps({"result": {"agent": hit[0]}})) elif a[:2] == ["pane", "close"]: spec.setdefault("agents", {})[server] = [x for x in spec.get("agents", {}).get(server, []) if x.get("pane_id") != a[2]] json.dump(spec, open(os.environ["FAKE_HERDR_SPEC"], "w")) ''' STUB_MONITOR = r'''#!/usr/bin/env bash # Issue #72 test stub of reconcile.sh: a wrapper whose python child holds monitor.lock. d="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../.." && pwd -P)" echo "start $$ $*" >> "$d/.mam/stub-monitor.log" python3 -c ' import fcntl, os, signal, sys, time if os.environ.get("STUB_MONITOR_IGNORE_TERM") == "1": signal.signal(signal.SIGTERM, signal.SIG_IGN) f = open(sys.argv[1], "w") fcntl.flock(f, fcntl.LOCK_EX) time.sleep(600) ' "$d/.mam/monitor.lock" & child=$! if [ "${STUB_MONITOR_IGNORE_TERM:-0}" = "1" ]; then trap '' TERM else trap 'kill $child 2>/dev/null; exit 143' TERM fi while kill -0 "$child" 2>/dev/null; do wait "$child"; done ''' def _clean_env(): env = {k: v for k, v in os.environ.items() if not k.startswith(("HERDR_", "XDG_", "MAM_", "STUB_")) and k not in ("WORKSPACE_ROOT", "AGENT_SESSIONS_YAML", "DELEGATE_JOB_LOGS_DIR", "PYTHONPATH")} env.update(MAM_REPO_URL=str(REPO_ROOT), MAM_SKIP_VENV="1", MAM_INSTALLER_URL="file://" + str(REPO_ROOT / "deploy" / "install.sh"), MAM_ENV_FILE="/nonexistent/.mam.env", PYTHONDONTWRITEBYTECODE="1") return env @pytest.fixture(scope="module") def template(tmp_path_factory): ws = tmp_path_factory.mktemp("tpl72") / "ws" ws.mkdir() res = subprocess.run(["bash", str(REPO_ROOT / "deploy" / "install.sh"), str(ws)], env=_clean_env(), capture_output=True, text=True, timeout=600) assert res.returncode == 0, res.stdout + res.stderr return ws class Box: def __init__(self, root: Path, ws: Path): self.root, self.ws = root, ws self.mam = ws / ".mam" self.bin = root / "bin" self.spec = root / "herdr.json" self.env = _clean_env() self.env["PATH"] = "%s:%s" % (self.bin, os.environ["PATH"]) self.env["FAKE_HERDR_SPEC"] = str(self.spec) self.helpers = [] self.herdr() def herdr(self, sessions=None, agents=None, fail=None): self.spec.write_text(json.dumps({"sessions": sessions or {"default": True}, "agents": agents or {}, "fail": fail})) def update(self, *args, env=None, timeout=300): e = dict(self.env, **(env or {})) return subprocess.run(["bash", str(self.ws / UPDATE_REL), "--yes", *args], cwd=str(self.ws), env=e, stdin=subprocess.DEVNULL, capture_output=True, text=True, timeout=timeout) def lib(self, script, env=None, timeout=120): e = dict(self.env, WORKSPACE_ROOT=str(self.ws), **(env or {})) return subprocess.run(["bash", "-c", 'source "$1/.agents/skills/lib.sh"\n' + script, "_", str(self.ws)], env=e, capture_output=True, text=True, timeout=timeout) def installer(self, body): p = self.root / "installer.sh" p.write_text("#!/usr/bin/env bash\nws=\"$1\"\n" + body) self.env["MAM_INSTALLER_URL"] = "file://" + str(p) def helper(self, *argv, **kw): p = subprocess.Popen(list(argv), start_new_session=True, **kw) self.helpers.append(p) return p def stub_monitor(self): p = self.ws / MONITOR_REL p.write_text(STUB_MONITOR) def start_monitor(self, env=None): """The way create and resume start it: lib.sh start_watchdog (detached, so not a child of this test that would linger as a zombie).""" res = self.lib('start_watchdog test "$WORKSPACE_ROOT"', env=dict(env or {})) assert res.returncode == 0 and res.stdout.strip(), res.stdout + res.stderr _until(lambda: _lock_held(self.mam / "monitor.lock")) def monitor_pids(self): return _exact_monitor_pids(self.ws / MONITOR_REL) def decoy(self): """A process that only names this workspace's monitor in an argument.""" return self.helper("/bin/bash", "-c", "sleep 120 & wait $!", "bash %s --subscribe" % (self.ws / MONITOR_REL)) def monitor_starts(self): p = self.mam / "stub-monitor.log" return p.read_text().count("start ") if p.exists() else 0 def _until(cond, timeout=15.0): end = time.monotonic() + timeout while time.monotonic() < end: if cond(): return True time.sleep(0.1) raise AssertionError("condition not met within %ss" % timeout) def _exact_monitor_pids(script) -> list: """`bash