Review round 1 (planner-reviewer-claude-01, reviewer-agy-01, planner-reviewer-codex-01): one blocking defect and two P2s, all confirmed. B-1. handle_startup_dialogs took its ready tokens only from the environment, which wait_for_tui_ready fills when it evaluates the adapter facts. Called on its own - as resume_session.sh calls it, with no readiness wait before it - it had no tokens, its early-return branch could never fire, and every resume of every agent idled through the full 20 s timeout (base: 0.03 s). rc was 0 throughout, so nothing failed; the resume tests just took ~30 s each. _mam_load_dialog_facts now also takes MAM_STRONG_READY_TOKENS / MAM_READY_TOKENS from the facts, but only when the caller has not set them, so a caller-set value (wait_for_tui_ready's own, or a test steering readiness) is never clobbered - the concern that made it take a single fact in the first place. S5d pins one-iteration return on a ready first frame for five adapters with no environment; S5e pins that caller tokens survive; R4 gets a wall-clock cap so a timeout-idle can no longer pass as success. The old _MAM_READY_TOKENS_CLAUDE fallback was left defined but unread - removed. R1 / N-2. delegate_wait_started treated any status past `pending` as the agent's acknowledgement. codex reproduced three false positives: `error` and `cancelled` records with no started line, and a `running` record from another dispatch. The status branch is gone; the evidence is a started line carrying this dispatch's token, which the local publisher writes before it syncs the status, so nothing that really started is missed (S7c re-pinned on the line with running/completed/error statuses; S7f/S7g pin the three negatives). N-4, taken with it: the events.log line is parsed as JSON rather than pattern- matched. publish_event nests dispatch_token under `data`, and the old regex only matched because of key order (S7h, real line shape and a reordered one). R2 / N-1. Two tests hardcoded <repo>/.venv/bin/python and failed with FileNotFoundError in every review worktree. sys.executable. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
128 lines
5.9 KiB
Python
128 lines
5.9 KiB
Python
"""Issue #24, resume path: start-up dialogs are answered for every agent.
|
|
|
|
resume_session.sh called handle_startup_dialogs only for claude and passed no
|
|
agent, so a resumed agy/grok/codex sat on its directory-trust prompt exactly as
|
|
the create path did before this branch. These fix the call contract resume
|
|
now shares with create.
|
|
|
|
What is real: the shipped create/stop/resume scripts run end to end against
|
|
the mock herdr in a sandbox. What is not: the mock's pane always shows a
|
|
ready banner, so no test here drives a real trust dialog - the create-side
|
|
tests (test_issue23_24_startup_path.py, S4-S6) fix which keys each dialog
|
|
gets, and R4 shows the same shipped function is now reached from resume.
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
import subprocess
|
|
import time
|
|
|
|
import pytest
|
|
|
|
SCRIPTS = ("multi-agent-mux-create/scripts/create_session.sh",
|
|
"multi-agent-mux-stop/scripts/stop_session.sh",
|
|
"multi-agent-mux-resume/scripts/resume_session.sh")
|
|
|
|
|
|
def _script(tmp_path, rel):
|
|
return str(tmp_path / ".agents" / "skills" / rel)
|
|
|
|
|
|
def _create_and_stop(tmp_path, agent, name):
|
|
ws = str(tmp_path / ("ws_" + name))
|
|
os.makedirs(ws, exist_ok=True)
|
|
create, stop, _ = (_script(tmp_path, s) for s in SCRIPTS)
|
|
subprocess.run(["bash", create, "--workspace", ws, "--agent", agent,
|
|
"--role", "creator", "--session", name, "--no-onboard"],
|
|
check=True, cwd=str(tmp_path), capture_output=True, text=True)
|
|
subprocess.run(["bash", stop, "--session", name, "--agent", agent],
|
|
check=True, cwd=str(tmp_path), capture_output=True, text=True)
|
|
return ws
|
|
|
|
|
|
def _resume(tmp_path, agent, name, ws, extra=()):
|
|
_, _, resume = (_script(tmp_path, s) for s in SCRIPTS)
|
|
return subprocess.run(["bash", resume, "--workspace", ws, "--agent", agent,
|
|
"--session", name, *extra],
|
|
cwd=str(tmp_path), capture_output=True, text=True)
|
|
|
|
|
|
def _record_dialog_calls(tmp_path):
|
|
"""Override handle_startup_dialogs in the SANDBOX copy of lib.sh (never the
|
|
repo's) so the call resume makes - session, timeout, agent, MAM_AUTO_TRUST
|
|
- is written down. Bash keeps the last definition, so appending wins."""
|
|
log = tmp_path / "dialog_calls.log"
|
|
lib = tmp_path / ".agents" / "skills" / "lib.sh"
|
|
with open(lib, "a") as f:
|
|
f.write('\nhandle_startup_dialogs() {\n'
|
|
' printf \'%%s\\n\' "sess=$1 timeout=${2:-} agent=${3:-<none>} '
|
|
'auto_trust=${MAM_AUTO_TRUST:-<unset>}" >> "%s"\n'
|
|
'}\n' % log)
|
|
return log
|
|
|
|
|
|
def _calls(log):
|
|
return log.read_text().splitlines() if log.exists() else []
|
|
|
|
|
|
@pytest.mark.parametrize("agent", ["agy", "codex"])
|
|
def test_r1_resume_answers_dialogs_for_a_non_claude_agent_and_names_it(
|
|
mam_sandbox, mock_herdr, mock_agents, agent):
|
|
"""R1: the gate is gone and the agent is passed - the two halves of the
|
|
create contract resume was missing."""
|
|
tmp_path = mam_sandbox
|
|
name = "test-r1-" + agent
|
|
ws = _create_and_stop(tmp_path, agent, name)
|
|
log = _record_dialog_calls(tmp_path) # armed after create, so only resume is recorded
|
|
res = _resume(tmp_path, agent, name, ws)
|
|
assert res.returncode == 0, res.stdout + res.stderr
|
|
assert _calls(log) == ["sess=%s timeout=20 agent=%s auto_trust=1" % (name, agent)]
|
|
|
|
|
|
def test_r2_claude_keeps_its_dialog_handling(mam_sandbox, mock_herdr, mock_agents):
|
|
"""R2: claude is still handled, now by the same call as everyone else."""
|
|
tmp_path = mam_sandbox
|
|
ws = _create_and_stop(tmp_path, "claude", "test-r2-claude")
|
|
log = _record_dialog_calls(tmp_path)
|
|
res = _resume(tmp_path, "claude", "test-r2-claude", ws)
|
|
assert res.returncode == 0, res.stdout + res.stderr
|
|
assert _calls(log) == ["sess=test-r2-claude timeout=20 agent=claude auto_trust=1"]
|
|
|
|
|
|
def test_r3_no_auto_trust_reaches_the_dialog_handler(mam_sandbox, mock_herdr, mock_agents):
|
|
"""R3: --no-auto-trust on resume sets MAM_AUTO_TRUST=0 for the handler,
|
|
the same switch with the same meaning as create's."""
|
|
tmp_path = mam_sandbox
|
|
ws = _create_and_stop(tmp_path, "codex", "test-r3-codex")
|
|
log = _record_dialog_calls(tmp_path)
|
|
res = _resume(tmp_path, "codex", "test-r3-codex", ws, extra=("--no-auto-trust",))
|
|
assert res.returncode == 0, res.stdout + res.stderr
|
|
assert _calls(log) == ["sess=test-r3-codex timeout=20 agent=codex auto_trust=0"]
|
|
|
|
|
|
def test_r4_the_shipped_handler_really_runs_for_a_non_claude_resume(
|
|
mam_sandbox, mock_herdr, mock_agents):
|
|
"""R4 (no override): with the real handle_startup_dialogs, a codex resume
|
|
completes and the pane is read after the spawn - the handler was reached
|
|
and returned on the ready banner. Before this change no pane read happened
|
|
between new-session and the YAML update for a non-claude agent."""
|
|
tmp_path = mam_sandbox
|
|
ws = _create_and_stop(tmp_path, "codex", "test-r4-codex")
|
|
before = len(json.load(open(mock_herdr)).get("calls", []))
|
|
t0 = time.monotonic()
|
|
res = _resume(tmp_path, "codex", "test-r4-codex", ws)
|
|
elapsed = time.monotonic() - t0
|
|
assert res.returncode == 0, res.stdout + res.stderr
|
|
# Review B-1: the handler used to idle its whole 20 s timeout here because
|
|
# it had no ready tokens on the resume path. rc 0 alone cannot tell early
|
|
# return from timeout; the clock can.
|
|
assert elapsed < 12, "resume took %.1fs: the dialog handler idled to its timeout" % elapsed
|
|
calls = json.load(open(mock_herdr)).get("calls", [])[before:]
|
|
|
|
def _has(c, *verbs): # the mock records argv with a leading `--session <name>`
|
|
return any(tuple(c[i:i + len(verbs)]) == verbs for i in range(len(c)))
|
|
|
|
spawn = next(i for i, c in enumerate(calls) if _has(c, "agent", "start"))
|
|
reads = [c for c in calls[spawn + 1:] if _has(c, "agent", "read") or _has(c, "pane", "read")]
|
|
assert reads, "no pane read after the spawn: handle_startup_dialogs never ran\n%s" % calls
|