"""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:-} ' 'auto_trust=${MAM_AUTO_TRUST:-}" >> "%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 ` 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