Files
multi-agent-mux/tests/test_issue24_resume_dialogs.py
GodopuandClaude Fable 5.1 05b0ea4762 fix(issue-23-24): merge review - the environment form of the auto-trust switch reaches the handler; a rate-limited dialog does not hide the next one
Two probes from the merge-side codex review (its report was lost to an API
outage; the probes and their results were recovered from its transcript):

- create_session.sh and resume_session.sh set AUTO_TRUST=1 and then
  exported MAM_AUTO_TRUST from it, so the documented MAM_AUTO_TRUST=0
  without a flag reached handle_startup_dialogs as 1: the operator had
  turned automatic trust off and it stayed on. Now AUTO_TRUST defaults to
  the environment value and an explicit --auto-trust/--no-auto-trust wins.
- _mam_dialog_pick exited on the first dialog inside its resend cooldown
  instead of looking further down the screen, so a skipped (--no-auto-trust)
  or just-answered trust prompt hid the codex update notice below it, which
  the docs say is always answered. It continues to the next dialog now; S5
  and S5b (one send per cooldown, one retry after it) still hold.

Regressions: S6c, S5f (startup path), R3b and C3 (resume/create, env only,
flag over env). Each fails without the fix. The create skill doc names the
environment form and its precedence.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VCgu9Fa9iT7TbJUSzdq4Ff
2026-09-24 06:47:57 +09:00

167 lines
7.8 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"]
@pytest.mark.parametrize("env_value,flag,expected", [
("0", (), "0"), # the documented environment form alone
("0", ("--auto-trust",), "1"), # an explicit flag wins over the environment
("1", ("--no-auto-trust",), "0"),
])
def test_r3b_the_environment_form_of_the_switch_reaches_the_handler_on_resume(
mam_sandbox, mock_herdr, mock_agents, monkeypatch, env_value, flag, expected):
"""R3b (merge review, codex probe): MAM_AUTO_TRUST=0 without a flag used to
be overwritten by the script's own export of its default."""
tmp_path = mam_sandbox
ws = _create_and_stop(tmp_path, "codex", "test-r3b-codex")
log = _record_dialog_calls(tmp_path)
monkeypatch.setenv("MAM_AUTO_TRUST", env_value)
res = _resume(tmp_path, "codex", "test-r3b-codex", ws, extra=flag)
assert res.returncode == 0, res.stdout + res.stderr
assert _calls(log) == ["sess=test-r3b-codex timeout=20 agent=codex auto_trust=%s" % expected]
@pytest.mark.parametrize("env_value,flag,expected", [
("0", (), "0"),
("0", ("--auto-trust",), "1"),
("1", ("--no-auto-trust",), "0"),
])
def test_c3_the_environment_form_of_the_switch_reaches_the_handler_on_create(
mam_sandbox, mock_herdr, mock_agents, monkeypatch, env_value, flag, expected):
"""C3 (merge review, codex probe): the same contract on create_session."""
tmp_path = mam_sandbox
log = _record_dialog_calls(tmp_path)
ws = str(tmp_path / "ws_c3")
os.makedirs(ws, exist_ok=True)
create = _script(tmp_path, SCRIPTS[0])
monkeypatch.setenv("MAM_AUTO_TRUST", env_value)
res = subprocess.run(["bash", create, "--workspace", ws, "--agent", "codex",
"--role", "creator", "--session", "test-c3-codex", "--no-onboard", *flag],
cwd=str(tmp_path), capture_output=True, text=True)
assert res.returncode == 0, res.stdout + res.stderr
assert _calls(log) == ["sess=test-c3-codex timeout=15 agent=codex auto_trust=%s" % expected]
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