fix(issue-50): review findings of PR #56 on macOS
- admission: a `submit --dry-run` record is marked dry_run and is not an open job of its session (review 1e25cf2f F1) - create: the monitor-row adoption requires the row's server to be the one this create spawned on (review 1e25cf2f F2) - pane lock: retry only on contention (BlockingIOError); the docs say the flock lasts while any fd-9 holder (the claim shell and the shim) lives (review 66680d9d) - tests on macOS: real-herdr box HOMEs under /tmp (sun_path is 104 bytes), resolved paths, the fake claude reads non-canonical input (MAX_CANON 1024) and the pane is read unwrapped; lock holders exec their sleep so killing them frees the lock; the adoption tests name the server Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VCgu9Fa9iT7TbJUSzdq4Ff
This commit is contained in:
1 parent
320aa9f7a0
commit
a9ffedac36
10 files changed
+75
-19
No files matched your search
@@ -620,7 +620,7 @@ def test_h26_new_session_claims_its_pane_under_the_servers_lock(mam_sandbox, moc
|
||||
locks = glob.glob(str(tmp_path / ".local" / "state" / "mam" / "pane-lock" / "*.lock"))
|
||||
assert len(locks) == 1, locks # one file per server, under the sandbox HOME
|
||||
n_calls = len(_load_mock_state(mock_herdr).get("calls", []))
|
||||
holder = subprocess.Popen(["bash", "-c", 'exec 9>>"$1"; python3 -m lib_py.pane_lock acquire --fd 9; sleep 600',
|
||||
holder = subprocess.Popen(["bash", "-c", 'exec 9>>"$1"; python3 -m lib_py.pane_lock acquire --fd 9; exec sleep 600',
|
||||
"holder", locks[0]], stdout=subprocess.PIPE, text=True,
|
||||
env=dict(os.environ, PYTHONPATH=str(tmp_path / ".agents" / "skills")))
|
||||
try:
|
||||
|
||||
@@ -159,6 +159,26 @@ def test_u1c_the_job_itself_and_unreadable_records_are_skipped(tmp_path):
|
||||
assert decide.open_jobs(str(tmp_path), "herdr:s", me) == ([], [])
|
||||
|
||||
|
||||
def test_u1e_a_dry_run_record_is_not_an_open_job(tmp_path):
|
||||
"""review 1e25cf2f F1: `submit --dry-run` registers a record it never
|
||||
dispatches; it stays pending (Issue #34 shows it as an open job), but the
|
||||
admission must not wait for it."""
|
||||
reg = tmp_path / "jobs"
|
||||
env = dict(os.environ, HOME=str(tmp_path), MAM_ENV_FILE=str(tmp_path / ".mam.env.absent"))
|
||||
res = subprocess.run(["bash", str(WRAPPER), "submit", "--agent", "human", "--agent-session", "herdr:s",
|
||||
"--dry-run", "--registry-dir", str(reg), "--prompt", "preview only"],
|
||||
capture_output=True, text=True, env=env, cwd=str(tmp_path), timeout=120)
|
||||
assert res.returncode == 0, res.stdout + res.stderr
|
||||
(dry,) = [p.stem for p in reg.glob("*.json")]
|
||||
assert _rec(reg, dry)["status"] == "pending" and _rec(reg, dry)["dry_run"] is True
|
||||
me = _register(reg)
|
||||
assert decide.open_jobs(str(reg), "herdr:s", me) == ([], [])
|
||||
assert decide.wait_open_jobs(_ns(reg, me)) == "clear"
|
||||
# control: the same record without the mark is an `injected` job that holds the session
|
||||
_edit(reg, dry, dry_run=None)
|
||||
assert decide.wait_open_jobs(_ns(reg, me)) == f"busy {dry}"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# U2 - FIFO release and the bound (wait_s=0: an immediate decision)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -55,6 +55,9 @@ FAKE_CLAUDE = r'''#!/usr/bin/env bash
|
||||
if [ "${1:-}" = "auth" ]; then echo '{"loggedIn": true}'; exit 0; fi
|
||||
printf '\n ▐▛███▛█ Claude Code v2.1.0 (fake)\n▝▜██████▀ Sonnet · Claude Max\n\n'
|
||||
printf '───────────────────────────────────────────────\n❯ Try "how does <filepath> work?"\n───────────────────────────────────────────────\n ⏵⏵ bypass permissions on (shift+tab to cycle)\n'
|
||||
# non-canonical input, as a real TUI reads it: a canonical tty line is capped at
|
||||
# MAX_CANON (1024 bytes on macOS), shorter than one injected prompt
|
||||
stty -icanon 2>/dev/null
|
||||
while IFS= read -r line; do printf '\n● got: %s\n❯ \n' "$line"; done
|
||||
'''
|
||||
|
||||
@@ -99,7 +102,7 @@ class Box:
|
||||
return ""
|
||||
|
||||
def pane_text(self, name=SESSION) -> str:
|
||||
res = self.herdr("agent", "read", name, "--source", "recent")
|
||||
res = self.herdr("agent", "read", name, "--source", "recent-unwrapped", "--lines", "1000")
|
||||
try:
|
||||
d = json.loads(res.stdout)["result"]
|
||||
return (d.get("read") or d).get("text", "")
|
||||
@@ -192,7 +195,9 @@ def _row(sess: str, name=SESSION, ws="") -> str:
|
||||
|
||||
|
||||
def _make_box(sess: str) -> Box:
|
||||
home = Path(tempfile.mkdtemp(prefix="mam50-")) # short: a unix socket path lives under it
|
||||
# short: a unix socket path lives under it (macOS sun_path is 104 bytes and
|
||||
# its TMPDIR is long); resolved, as the scripts print /private/tmp paths
|
||||
home = Path(tempfile.mkdtemp(prefix="mam50-", dir="/tmp")).resolve()
|
||||
box = Box(home, sess)
|
||||
for d in (box.bin, box.ws, box.ctl / ".agents", box.jobs, box.ctl / ".mam" / "delegate_job_logs"):
|
||||
d.mkdir(parents=True)
|
||||
@@ -382,7 +387,7 @@ def test_c4_the_loops_first_job_waits_for_the_onboarding_to_end(box, monkeypatch
|
||||
def test_c5_a_session_without_a_row_is_judged_by_the_records_alone(local_mqtt_broker):
|
||||
# resolve_herdr_session falls back to the workspace slug when the yaml has no
|
||||
# row, so the server is named exactly that: mam-<parent>-<work> of the control root
|
||||
home = Path(tempfile.mkdtemp(prefix="mam50-"))
|
||||
home = Path(tempfile.mkdtemp(prefix="mam50-", dir="/tmp")).resolve()
|
||||
sess = f"mam-{home.name}-ctl".lower().replace("_", "-")
|
||||
shutil.rmtree(home)
|
||||
_TIMERS.clear()
|
||||
|
||||
@@ -28,7 +28,7 @@ if str(SKILLS) not in sys.path:
|
||||
from lib_py import pane_lock # noqa: E402
|
||||
|
||||
# a bash holder, as the shim is: fd 9 on the file, the lock taken by the helper, then it sleeps
|
||||
HOLDER = 'exec 9>>"$1"; python3 -m lib_py.pane_lock acquire --fd 9; echo held; sleep "${2:-600}"'
|
||||
HOLDER = 'exec 9>>"$1"; python3 -m lib_py.pane_lock acquire --fd 9; echo held; exec sleep "${2:-600}"'
|
||||
|
||||
|
||||
def _holder(path: Path, seconds: str = "600", **env) -> subprocess.Popen:
|
||||
@@ -133,9 +133,12 @@ def test_p5_the_wait_knob_is_a_decimal_with_a_floor_of_one(monkeypatch):
|
||||
|
||||
|
||||
|
||||
def _seed_row(tmp_path, name, epoch, note="auto-registered by monitor", job=None):
|
||||
SERVER = "t50-srv" # the server _create names; a monitor row carries the server it found the pane on
|
||||
|
||||
|
||||
def _seed_row(tmp_path, name, epoch, note="auto-registered by monitor", job=None, server=SERVER):
|
||||
row = {"name": name, "status": "running", "role": "creator", "herdr_session_created_at": "2026-10-11T00:00:00Z",
|
||||
"herdr_session_epoch": epoch, "herdr_session": "default", "herdr_server": "default",
|
||||
"herdr_session_epoch": epoch, "herdr_session": server, "herdr_server": server,
|
||||
"pane": {"index": 0, "pid": 9999, "cmd": "claude", "cmd_full": "claude", "cwd": str(tmp_path)},
|
||||
"last_visible_status": "running"}
|
||||
if note:
|
||||
@@ -151,7 +154,7 @@ def _seed_row(tmp_path, name, epoch, note="auto-registered by monitor", job=None
|
||||
def _create(tmp_path, name, role="creator"):
|
||||
return subprocess.run(["bash", str(tmp_path / "skills" / "multi-agent-mux-create" / "scripts" / "create_session.sh"),
|
||||
"--workspace", str(tmp_path), "--agent", "claude", "--role", role, "--session", name,
|
||||
"--no-onboard"], capture_output=True, text=True, cwd=str(tmp_path), timeout=300)
|
||||
"--herdr-session", SERVER, "--no-onboard"], capture_output=True, text=True, cwd=str(tmp_path), timeout=300)
|
||||
|
||||
|
||||
def _rows(yaml_path):
|
||||
@@ -161,7 +164,7 @@ def _rows(yaml_path):
|
||||
|
||||
def test_a1_create_replaces_the_monitors_registration_of_its_own_pane(mam_sandbox, mock_herdr, mock_agents):
|
||||
name = "t50-adopted-creator-claude"
|
||||
yaml_path = _seed_row(mam_sandbox, name, int(time.time())) # registered after this create began
|
||||
yaml_path = _seed_row(mam_sandbox, name, int(time.time()) + 60) # registered after this create began
|
||||
res = _create(mam_sandbox, name, role="Creator") # the monitor guessed "creator" off the name
|
||||
assert res.returncode == 0, res.stdout + res.stderr
|
||||
assert f"NOTE: the monitor registered {name} while it was being created" in res.stdout
|
||||
@@ -170,7 +173,7 @@ def test_a1_create_replaces_the_monitors_registration_of_its_own_pane(mam_sandbo
|
||||
assert rows[0].get("last_visible_note") != "auto-registered by monitor" and rows[0].get("start_command")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("kind", ["real", "old-adoption", "adoption-with-job"])
|
||||
@pytest.mark.parametrize("kind", ["real", "old-adoption", "adoption-with-job", "other-server"])
|
||||
def test_a2_another_live_session_of_that_name_is_still_refused(mam_sandbox, mock_herdr, mock_agents, kind):
|
||||
name = "t50-taken-creator-claude"
|
||||
now = int(time.time())
|
||||
@@ -178,6 +181,8 @@ def test_a2_another_live_session_of_that_name_is_still_refused(mam_sandbox, mock
|
||||
yaml_path = _seed_row(mam_sandbox, name, now, note=None)
|
||||
elif kind == "old-adoption":
|
||||
yaml_path = _seed_row(mam_sandbox, name, now - 3600) # a pane from before this create
|
||||
elif kind == "other-server": # review 1e25cf2f F2: names are unique per server
|
||||
yaml_path = _seed_row(mam_sandbox, name, now + 60, server="different-server")
|
||||
else:
|
||||
yaml_path = _seed_row(mam_sandbox, name, now, job="deadbeef")
|
||||
res = _create(mam_sandbox, name)
|
||||
|
||||
Reference in new issue
Block a user