handle_startup_dialogs was a claude-only string ladder called only when AGENT=claude, and wait_for_tui_ready does not treat a dialog screen as ready. So agy, grok and codex sat on their directory-trust prompt for the full readiness timeout, the session was recorded as tui-ready-timeout, and onboarding injection was disabled (create_session.sh:313-314) - a human had to accept each dialog by hand. The signatures and accept keys move to the adapters as `startup_dialogs` (pattern, keys, kind), published through the existing `facts` eval contract as MAM_STARTUP_DIALOGS. claude's ladder moves over verbatim; agy takes Enter, grok y, codex Enter, and codex's update notice takes 2 (Skip), which blocks a start just as hard while changing nothing. kind is `trust` or `notice`: --no-auto-trust leaves trust dialogs to a human and still answers notices, since trusting a directory makes codex load its project-local config, hooks and exec policy. Re-sending is gated by time (MAM_DIALOG_RESEND_S, default 5s), not by frames. #24's comment reports that `--source visible` keeps showing an accepted dialog for a while, and _pane_capture hardcodes that source, so "still on screen" is not evidence the keys were missed; acceptance is judged by the ready tokens alone. Two existing tests asserted the old shape and are updated, not weakened: b19's "Escape branch stays" now looks for it in claude's adapter, and c1b's "Enter once per iteration" becomes "never twice for one frame", which the cooldown satisfies strictly more tightly. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
447 lines
20 KiB
Python
447 lines
20 KiB
Python
"""Issue #23 + #24 — the create_session start path.
|
|
|
|
Four regressions live here, in the order of the issues:
|
|
|
|
1. a herdr call aimed at another server must not carry the caller pane's own
|
|
workspace/pane/tab ids (#23 comments: reproduced on Linux and macOS);
|
|
2. blocking start-up modals are agent-adapter knowledge and are answered while
|
|
waiting for the TUI (#24);
|
|
3. onboarding delivery is judged by the agent's own `started` event (#23);
|
|
4. a rollback verifies the kill and leaves no `running` row or dangling job (#23).
|
|
|
|
What is mocked and what is real is stated per test. The dialog tests fix OUR
|
|
contract - which pane string gets which keys - not that the keys drive a real
|
|
agy/grok/codex/opencode TUI; no such TUI can be started here.
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
import subprocess
|
|
import textwrap
|
|
|
|
import pytest
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# helpers
|
|
# --------------------------------------------------------------------------
|
|
|
|
def _run_lib(tmp_path, body, env=None):
|
|
"""Source the sandbox lib.sh and run `body`. Unlike the shim-contract
|
|
helper this keeps HERDR_* exactly as `env` sets it: these tests are about
|
|
what the caller's pane leaks in."""
|
|
lib_path = tmp_path / ".agents" / "skills" / "lib.sh"
|
|
script = 'source "%s"\n_init_herdr_isolation\n%s\n' % (lib_path, body)
|
|
run_env = dict(os.environ)
|
|
for var in ("HERDR_SESSION", "HERDR_SESSION_NAME", "HERDR_WORKSPACE_ID",
|
|
"HERDR_PANE_ID", "HERDR_TAB_ID"):
|
|
run_env.pop(var, None)
|
|
run_env.update(env or {})
|
|
return subprocess.run(["bash", "-c", script], capture_output=True,
|
|
text=True, cwd=str(tmp_path), env=run_env)
|
|
|
|
|
|
def _mock_state(mock_herdr):
|
|
with open(mock_herdr) as f:
|
|
return json.load(f)
|
|
|
|
|
|
def _write_mock_state(mock_herdr, state):
|
|
with open(mock_herdr, "w") as f:
|
|
json.dump(state, f, indent=2)
|
|
|
|
|
|
def _seed_agent(mock_herdr, tmp_path, name, workspace_id, pane_id):
|
|
state = _mock_state(mock_herdr)
|
|
state.setdefault("workspaces", [])
|
|
if not any(w.get("workspace_id") == workspace_id for w in state["workspaces"]):
|
|
state["workspaces"].append(
|
|
{"workspace_id": workspace_id, "label": workspace_id, "cwd": str(tmp_path)})
|
|
state.setdefault("agents", {})[name] = {
|
|
"name": name, "agent": "agy", "status": "running", "cwd": str(tmp_path),
|
|
"workspace_id": workspace_id, "pane_id": pane_id, "pid": 9999,
|
|
"command": "agy", "buffer": "Ready",
|
|
}
|
|
state.setdefault("panes", []).append(
|
|
{"pane_id": pane_id, "workspace_id": workspace_id, "label": name, "buffer": "Ready"})
|
|
_write_mock_state(mock_herdr, state)
|
|
|
|
|
|
def _install_env_recorder(tmp_path, monkeypatch):
|
|
"""A herdr earlier in PATH than the mock that dumps the env it was called
|
|
with, then execs the mock. The shim resolves the first non-shim herdr on
|
|
PATH, so this observes exactly what reached the real binary."""
|
|
rec_dir = tmp_path / "recorder"
|
|
rec_dir.mkdir(parents=True, exist_ok=True)
|
|
env_log = tmp_path / "herdr_env.ndjson"
|
|
real = tmp_path / "bin" / "herdr"
|
|
script = rec_dir / "herdr"
|
|
script.write_text(textwrap.dedent('''\
|
|
#!/usr/bin/env python3
|
|
import json, os, sys
|
|
with open(%r, "a") as f:
|
|
f.write(json.dumps({
|
|
"argv": sys.argv[1:],
|
|
"HERDR_WORKSPACE_ID": os.environ.get("HERDR_WORKSPACE_ID"),
|
|
"HERDR_PANE_ID": os.environ.get("HERDR_PANE_ID"),
|
|
"HERDR_TAB_ID": os.environ.get("HERDR_TAB_ID"),
|
|
"HERDR_SESSION": os.environ.get("HERDR_SESSION"),
|
|
"HERDR_SESSION_NAME": os.environ.get("HERDR_SESSION_NAME"),
|
|
}) + "\\n")
|
|
os.execv(%r, [%r] + sys.argv[1:])
|
|
''') % (str(env_log), str(real), str(real)))
|
|
script.chmod(0o755)
|
|
monkeypatch.setenv("PATH", "%s:%s" % (rec_dir, os.environ["PATH"]))
|
|
return env_log
|
|
|
|
|
|
def _recorded(env_log):
|
|
if not os.path.exists(env_log):
|
|
return []
|
|
return [json.loads(l) for l in open(env_log) if l.strip()]
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# item 1 — caller pane scope must not leak across servers
|
|
# --------------------------------------------------------------------------
|
|
|
|
CROSS = {"HERDR_SESSION": "default", "HERDR_SESSION_NAME": "mam-isolated",
|
|
"HERDR_WORKSPACE_ID": "w5", "HERDR_PANE_ID": "w5:p2",
|
|
"HERDR_TAB_ID": "w5:t1"}
|
|
|
|
|
|
def test_s1_cross_server_call_drops_the_caller_panes_scope_ids(
|
|
mam_sandbox, mock_herdr, mock_agents, monkeypatch):
|
|
"""S1 (mock herdr): the caller sits in another server's pane. has-session,
|
|
kill-session and agent prompt must find the agent, and the three ids must
|
|
not reach herdr."""
|
|
tmp_path = mam_sandbox
|
|
env_log = _install_env_recorder(tmp_path, monkeypatch)
|
|
_seed_agent(mock_herdr, tmp_path, "creator-agy-01", "w1", "w1:p5")
|
|
|
|
res = _run_lib(tmp_path, 'herdr has-session -t creator-agy-01', env=CROSS)
|
|
assert res.returncode == 0, res.stdout + res.stderr
|
|
|
|
res = _run_lib(tmp_path, 'herdr agent prompt creator-agy-01 "hello"', env=CROSS)
|
|
assert res.returncode == 0, res.stdout + res.stderr
|
|
|
|
res = _run_lib(tmp_path, 'herdr kill-session -t creator-agy-01', env=CROSS)
|
|
assert res.returncode == 0, res.stdout + res.stderr
|
|
|
|
calls = _recorded(env_log)
|
|
assert calls, "the recorder saw no herdr call"
|
|
for call in calls:
|
|
assert call["HERDR_WORKSPACE_ID"] is None, call
|
|
assert call["HERDR_PANE_ID"] is None, call
|
|
assert call["HERDR_TAB_ID"] is None, call
|
|
|
|
# the kill really happened, i.e. this is not a silent no-op
|
|
assert "creator-agy-01" not in _mock_state(mock_herdr).get("agents", {})
|
|
|
|
|
|
def test_s1b_the_delegate_job_wrappers_bare_herdr_is_the_lib_sh_function(
|
|
mam_sandbox, mock_herdr, mock_agents, monkeypatch):
|
|
"""S1b (mock herdr): the wrapper sources lib.sh and then calls a bare
|
|
`herdr` (wrapper :918 preflight). A shell function beats PATH, so that call
|
|
is mam_herdr and is fixed by item 1 too - this is why #23's preflight
|
|
failure is cured by the same change."""
|
|
tmp_path = mam_sandbox
|
|
_install_env_recorder(tmp_path, monkeypatch)
|
|
_seed_agent(mock_herdr, tmp_path, "creator-agy-01", "w1", "w1:p5")
|
|
|
|
res = _run_lib(tmp_path, 'echo "kind=$(type -t herdr)"\nherdr has-session -t creator-agy-01',
|
|
env=CROSS)
|
|
assert "kind=function" in res.stdout, res.stdout + res.stderr
|
|
assert res.returncode == 0, res.stdout + res.stderr
|
|
|
|
|
|
def test_s2_same_server_keeps_the_explicit_workspace_scope(
|
|
mam_sandbox, mock_herdr, mock_agents, monkeypatch):
|
|
"""S2 (mock herdr): on the target's own server the exported scope still
|
|
scopes - a same-named agent in another workspace is not addressed (H-22)."""
|
|
tmp_path = mam_sandbox
|
|
_seed_agent(mock_herdr, tmp_path, "creator-agy-01", "w1", "w1:p5")
|
|
same = {"HERDR_SESSION": "mam-isolated", "HERDR_SESSION_NAME": "mam-isolated",
|
|
"HERDR_WORKSPACE_ID": "w2"}
|
|
|
|
before = len(_mock_state(mock_herdr).get("calls", []))
|
|
res = _run_lib(tmp_path, 'herdr agent prompt creator-agy-01 "hello from w2"', env=same)
|
|
assert res.returncode != 0, res.stdout + res.stderr
|
|
new = _mock_state(mock_herdr).get("calls", [])[before:]
|
|
prompts = [c for c in new if len(c) >= 2 and c[0] == "agent" and c[1] == "prompt"]
|
|
assert prompts == [], new
|
|
|
|
|
|
def test_s2b_a_caller_outside_any_pane_keeps_the_explicit_scope(
|
|
mam_sandbox, mock_herdr, mock_agents):
|
|
"""S2b (mock herdr): no HERDR_SESSION means the caller is not in a pane at
|
|
all, so an exported HERDR_WORKSPACE_ID is a deliberate scope, not a leak.
|
|
This is the case H-18/H-21/H-23 fix."""
|
|
tmp_path = mam_sandbox
|
|
_seed_agent(mock_herdr, tmp_path, "creator-agy-01", "w1", "w1:p5")
|
|
|
|
res = _run_lib(tmp_path, 'herdr has-session -t creator-agy-01',
|
|
env={"HERDR_SESSION_NAME": "mam-isolated", "HERDR_WORKSPACE_ID": "w2"})
|
|
assert res.returncode == 1, "an explicit out-of-scope id must still hide the agent"
|
|
|
|
res = _run_lib(tmp_path, 'herdr has-session -t creator-agy-01',
|
|
env={"HERDR_SESSION_NAME": "mam-isolated", "HERDR_WORKSPACE_ID": "w1"})
|
|
assert res.returncode == 0, res.stdout + res.stderr
|
|
|
|
|
|
def test_s3_shim_contract_regressions_still_pass(mam_sandbox):
|
|
"""S3: H-18 / H-21 / H-23 are the tests that fix today's scoping meaning.
|
|
Run them explicitly as this change's regression guard."""
|
|
repo = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
res = subprocess.run(
|
|
[os.path.join(repo, ".venv", "bin", "python"), "-m", "pytest", "-q",
|
|
"-p", "no:cacheprovider",
|
|
"tests/test_herdr_shim_contract.py", "-k",
|
|
"h18 or h21 or h22 or h23"],
|
|
capture_output=True, text=True, cwd=repo,
|
|
env=dict(os.environ, PYTHONDONTWRITEBYTECODE="1"))
|
|
assert res.returncode == 0, res.stdout[-4000:] + res.stderr[-2000:]
|
|
|
|
|
|
def test_s11_this_branch_does_not_change_the_generated_shim(mam_sandbox):
|
|
"""S11: the item-1 fix lives in mam_herdr, never in the heredoc that
|
|
lib.sh materialises into $WORKSPACE_ROOT/.mam/shim/herdr. That file is
|
|
shared by every checkout bound to one control plane, so keeping it
|
|
byte-identical to base is what makes "the control plane is not modified"
|
|
true at runtime."""
|
|
repo = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
base = os.environ.get("MAM_BASE_SHA", "1e78dae")
|
|
show = subprocess.run(["git", "show", "%s:.agents/skills/lib.sh" % base],
|
|
capture_output=True, text=True, cwd=repo)
|
|
if show.returncode != 0:
|
|
pytest.skip("base %s not available in this checkout" % base)
|
|
with open(os.path.join(repo, ".agents", "skills", "lib.sh")) as f:
|
|
head = f.read()
|
|
assert _heredoc_body(head) == _heredoc_body(show.stdout), (
|
|
"the shim heredoc changed: the generated .mam/shim/herdr of this branch "
|
|
"would differ from the control root's, and the two would overwrite "
|
|
"each other on every herdr call")
|
|
|
|
|
|
def _heredoc_body(text):
|
|
lines = text.splitlines(True)
|
|
start = None
|
|
for i, line in enumerate(lines):
|
|
if line.strip() == "cat <<'EOF' > \"$tmp_file\"":
|
|
start = i + 1
|
|
break
|
|
assert start is not None, "shim heredoc not found in lib.sh"
|
|
for j in range(start, len(lines)):
|
|
if lines[j].rstrip("\n") == "EOF":
|
|
return "".join(lines[start:j])
|
|
raise AssertionError("unterminated shim heredoc")
|
|
|
|
|
|
@pytest.mark.parametrize("env,expected_rc", [(CROSS, 1), ({"HERDR_SESSION_NAME": "mam-isolated"}, 1)])
|
|
def test_s12_mam_herdr_never_aborts_its_caller_under_set_e(
|
|
mam_sandbox, mock_herdr, mock_agents, env, expected_rc):
|
|
"""S12 (mock herdr): mam_herdr runs inside scripts using `set -euo
|
|
pipefail`. Neither branch of the cross-server test may abort the shell,
|
|
and the child's rc must be the function's rc."""
|
|
tmp_path = mam_sandbox
|
|
body = ('set -euo pipefail\n'
|
|
'rc=0\n'
|
|
'mam_herdr has-session -t nobody-here || rc=$?\n'
|
|
'echo "missing_rc=$rc"\n'
|
|
'_seen=0\n'
|
|
'mam_herdr has-session -t creator-agy-01 || _seen=$?\n'
|
|
'echo "present_rc=$_seen"\n'
|
|
'echo REACHED_END\n')
|
|
_seed_agent(mock_herdr, tmp_path, "creator-agy-01", "w1", "w1:p5")
|
|
lib_path = tmp_path / ".agents" / "skills" / "lib.sh"
|
|
script = 'source "%s"\n%s' % (lib_path, body)
|
|
run_env = dict(os.environ)
|
|
for var in ("HERDR_SESSION", "HERDR_SESSION_NAME", "HERDR_WORKSPACE_ID",
|
|
"HERDR_PANE_ID", "HERDR_TAB_ID"):
|
|
run_env.pop(var, None)
|
|
run_env.update(env)
|
|
res = subprocess.run(["bash", "-c", script], capture_output=True, text=True,
|
|
cwd=str(tmp_path), env=run_env)
|
|
assert "REACHED_END" in res.stdout, res.stdout + res.stderr
|
|
assert "missing_rc=%d" % expected_rc in res.stdout, res.stdout + res.stderr
|
|
assert "present_rc=0" in res.stdout, res.stdout + res.stderr
|
|
|
|
|
|
def test_s13_the_loop_watcher_applies_the_same_rule(mam_sandbox):
|
|
"""S13 (pure unit): watcher.py builds its own herdr argv and never goes
|
|
through mam_herdr, so it needs the same rule."""
|
|
import sys
|
|
repo = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
sys.path.insert(0, os.path.join(repo, ".agents", "skills"))
|
|
from lib_py.loop import watcher
|
|
|
|
caller_pane = {"HERDR_SESSION": "default", "HERDR_WORKSPACE_ID": "w5",
|
|
"HERDR_PANE_ID": "w5:p2", "HERDR_TAB_ID": "w5:t1", "PATH": "/usr/bin"}
|
|
|
|
cross = watcher.scoped_env("mam-isolated", environ=caller_pane)
|
|
assert "HERDR_WORKSPACE_ID" not in cross and "HERDR_PANE_ID" not in cross
|
|
assert "HERDR_TAB_ID" not in cross and cross["PATH"] == "/usr/bin"
|
|
|
|
same = watcher.scoped_env("default", environ=caller_pane)
|
|
assert same["HERDR_WORKSPACE_ID"] == "w5"
|
|
|
|
outside = watcher.scoped_env("mam-isolated", environ={"HERDR_WORKSPACE_ID": "w5"})
|
|
assert outside["HERDR_WORKSPACE_ID"] == "w5"
|
|
|
|
none_target = watcher.scoped_env(None, environ=caller_pane)
|
|
assert none_target["HERDR_WORKSPACE_ID"] == "w5"
|
|
|
|
seen = {}
|
|
|
|
def _run(argv, **kw):
|
|
seen["env"] = kw.get("env")
|
|
|
|
class P:
|
|
returncode, stdout, stderr = 0, "{}", ""
|
|
return P()
|
|
|
|
watcher.wait_once("agt", session="iso-x", run=_run)
|
|
assert seen["env"] is not None and "HERDR_SESSION" not in ("",)
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# item 2 — start-up dialogs come from the adapter (#24)
|
|
#
|
|
# MOCKED, and this is the limit of it: the pane text is a fixture string and
|
|
# send-keys is a shell stub, so these fix OUR contract - which screen signature
|
|
# gets which keys, once. They do NOT show that the keys drive a real agy, grok,
|
|
# codex or opencode TUI; no such TUI can be started in this environment. That
|
|
# check is a live herdr run after the loop.
|
|
# --------------------------------------------------------------------------
|
|
|
|
def _run_dialogs(tmp_path, agent, pane_text, timeout=1, env=None, prelude=""):
|
|
lib = tmp_path / ".agents" / "skills" / "lib.sh"
|
|
pane_file = tmp_path / "pane.txt"
|
|
pane_file.write_text(pane_text)
|
|
keys_log = tmp_path / "sent_keys.txt"
|
|
if keys_log.exists():
|
|
keys_log.unlink()
|
|
script = '''
|
|
source "%s"
|
|
_pane_tail() { cat "%s"; }
|
|
_sks_herdr() { printf '%%s\\n' "$*" >> "%s"; }
|
|
%s
|
|
handle_startup_dialogs test-sess %d %s
|
|
echo "rc=$?"
|
|
''' % (lib, pane_file, keys_log, prelude, timeout, agent)
|
|
run_env = dict(os.environ)
|
|
run_env.pop("MAM_AUTO_TRUST", None)
|
|
run_env.update(env or {})
|
|
res = subprocess.run(["bash", "-c", script], capture_output=True, text=True,
|
|
cwd=str(tmp_path), env=run_env)
|
|
sent = keys_log.read_text().splitlines() if keys_log.exists() else []
|
|
return res, sent
|
|
|
|
|
|
AGENT_DIALOGS = [
|
|
("agy", "Do you trust the contents of this project?\n > Yes, I trust this folder",
|
|
["send-keys -t test-sess Enter"]),
|
|
("grok", "Do you trust the contents of this directory?",
|
|
["send-keys -t test-sess y"]),
|
|
("codex", "Do you trust the contents of this directory?\n > 1. Yes, continue",
|
|
["send-keys -t test-sess Enter"]),
|
|
("codex", "Update available\n 1. Update now\n 2. Skip\n 3. Skip until next version",
|
|
["send-keys -t test-sess 2"]),
|
|
("claude", "Do you trust the files in this folder?\n Yes, I trust this folder",
|
|
["send-keys -t test-sess Enter"]),
|
|
("claude", "Try the new fullscreen renderer?\n Yes, try it",
|
|
["send-keys -t test-sess Escape"]),
|
|
]
|
|
|
|
|
|
@pytest.mark.parametrize("agent,pane,expected", AGENT_DIALOGS)
|
|
def test_s4_each_agents_dialog_signature_gets_its_own_keys(
|
|
mam_sandbox, agent, pane, expected):
|
|
"""S4 (stubbed pane + stubbed send-keys): the adapter's signature is
|
|
matched and exactly its keys are sent."""
|
|
res, sent = _run_dialogs(mam_sandbox, agent, pane)
|
|
assert sent == expected, (res.stdout, res.stderr, sent)
|
|
|
|
|
|
def test_s4b_every_adapter_publishes_the_fact(mam_sandbox):
|
|
"""S4b (real `lib_py.agents facts`): MAM_STARTUP_DIALOGS is part of the
|
|
facts contract for every agent, so the shell never has to know an agent
|
|
name. An agent with no known blocking modal publishes an empty list."""
|
|
repo = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
skills = os.path.join(repo, ".agents", "skills")
|
|
seen = {}
|
|
for agent in ("claude", "agy", "grok", "codex", "opencode", "hermes"):
|
|
out = subprocess.run(
|
|
[os.path.join(repo, ".venv", "bin", "python"), "-m", "lib_py.agents",
|
|
"facts", agent],
|
|
capture_output=True, text=True, cwd=repo,
|
|
env=dict(os.environ, PYTHONPATH=skills)).stdout
|
|
line = [l for l in out.splitlines() if l.startswith("MAM_STARTUP_DIALOGS=")]
|
|
assert line, "no MAM_STARTUP_DIALOGS for %s" % agent
|
|
seen[agent] = json.loads(line[0].split("=", 1)[1].strip("'"))
|
|
for agent in ("claude", "agy", "grok", "codex"):
|
|
assert seen[agent], "%s must publish its start-up dialogs" % agent
|
|
for pattern, keys, kind in seen[agent]:
|
|
assert kind in ("trust", "notice"), (agent, kind)
|
|
assert keys, (agent, pattern)
|
|
assert seen["opencode"] == [] and seen["hermes"] == []
|
|
|
|
|
|
def test_s5_an_accepted_dialog_still_on_screen_is_not_resent(mam_sandbox):
|
|
"""S5 (stubbed pane + stubbed send-keys): #24's comment reports that
|
|
`--source visible` keeps showing the dialog for a while after it was
|
|
accepted, and _pane_capture hardcodes that source. The pane here never
|
|
changes, so a frame-count rule would send the keys again every poll; the
|
|
time cooldown must keep it at one."""
|
|
res, sent = _run_dialogs(
|
|
mam_sandbox, "grok", "Do you trust the contents of this directory?",
|
|
timeout=3, env={"MAM_DIALOG_RESEND_S": "60"})
|
|
assert sent == ["send-keys -t test-sess y"], (res.stdout, res.stderr, sent)
|
|
|
|
|
|
def test_s5b_the_cooldown_expiring_allows_one_more_try(mam_sandbox):
|
|
"""S5b: the cooldown is a rate limit, not a one-shot - a dialog that really
|
|
did not take the keys is retried once the window passes."""
|
|
res, sent = _run_dialogs(
|
|
mam_sandbox, "grok", "Do you trust the contents of this directory?",
|
|
timeout=4, env={"MAM_DIALOG_RESEND_S": "1"})
|
|
assert len(sent) >= 2, (res.stdout, res.stderr, sent)
|
|
assert set(sent) == {"send-keys -t test-sess y"}, sent
|
|
|
|
|
|
def test_s5c_readiness_is_judged_by_the_ready_tokens_not_by_the_dialog_leaving(
|
|
mam_sandbox):
|
|
"""S5c: a ready screen returns 0 without sending anything."""
|
|
res, sent = _run_dialogs(mam_sandbox, "grok", "Grok ready\n>>> ", timeout=3)
|
|
assert sent == [], sent
|
|
assert "rc=0" in res.stdout, res.stdout + res.stderr
|
|
|
|
|
|
def test_s6_no_auto_trust_leaves_trust_dialogs_alone_but_answers_notices(
|
|
mam_sandbox):
|
|
"""S6 (stubbed): --no-auto-trust. codex trust is left to a human with a
|
|
warning; the update notice still gets answered, since skipping it changes
|
|
nothing and it blocks the session just as hard."""
|
|
res, sent = _run_dialogs(
|
|
mam_sandbox, "codex", "Do you trust the contents of this directory?\n > 1. Yes, continue",
|
|
timeout=1, env={"MAM_AUTO_TRUST": "0"})
|
|
assert sent == [], sent
|
|
assert "no-auto-trust" in res.stderr, res.stderr
|
|
|
|
res, sent = _run_dialogs(
|
|
mam_sandbox, "codex", "Update available\n 1. Update now\n 3. Skip until next version",
|
|
timeout=1, env={"MAM_AUTO_TRUST": "0"})
|
|
assert sent == ["send-keys -t test-sess 2"], (res.stderr, sent)
|
|
|
|
|
|
def test_s6b_create_session_no_longer_gates_dialogs_on_claude(mam_sandbox):
|
|
"""S6b (source read): the #24 root cause was the `AGENT = claude` gate at
|
|
the create_session call site, so the gate's absence is the regression."""
|
|
repo = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
src = open(os.path.join(repo, ".agents", "skills", "multi-agent-mux-create",
|
|
"scripts", "create_session.sh")).read()
|
|
call = [l for l in src.splitlines() if "handle_startup_dialogs" in l]
|
|
assert call, "create_session no longer answers start-up dialogs at all"
|
|
assert all('"$AGENT"' in l for l in call), call
|
|
assert 'if [ "$AGENT" = "claude" ] && [ -z "$LAST_VISIBLE_STATUS_OVERRIDE" ]' not in src
|