Files
multi-agent-mux/tests/test_c1_tui_readiness.py
T
Godopu 17edf90676 fix(tui): establish 2-tier readiness model, modal/hint token separation, and adapter modal contracts
- Separate TUI dialog detection into blocking gate layer (_MAM_MODAL_TOKENS) and recovery layer (_MAM_HINT_TOKENS) to eliminate idle dialog starvation
- Add modal_tokens contract across agent adapters (claude, cline) with dynamic scope facts resolution in send_keys_safe
- Implement fail-closed resolution for ambiguous same-kind panes (R-1) and enforce WORKSPACE_ROOT export (R-2)
- Preserve session on TUI readiness timeout when PID is alive for diagnostics
- Add comprehensive test suite in tests/test_c1_tui_readiness.py and adapter contract assertions in test_a4
2026-08-28 15:21:10 +09:00

610 lines
21 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# test_c1_tui_readiness.py — Unit and integration tests for TUI readiness,
# 2-tier readiness model, modal/hint token separation, and session diagnostics/preservation.
import os, subprocess, time, json
from pathlib import Path
import pytest
_LIB_SH = str(Path(__file__).resolve().parent.parent / ".agents" / "skills" / "lib.sh")
_CREATE_SH = str(Path(__file__).resolve().parent.parent / ".agents" / "skills" / "multi-agent-mux-create" / "scripts" / "create_session.sh")
def _run_lib_helpers(script_body, env_extra=None):
import tempfile, shutil
tmp_dir = tempfile.mkdtemp(prefix="test_tui_ready_")
try:
env = dict(os.environ)
if env_extra:
env.update(env_extra)
env["WORKSPACE_ROOT"] = tmp_dir
wrapper = f"""
set -euo pipefail
cd "{tmp_dir}"
source "{_LIB_SH}"
_init_herdr_isolation() {{ :; }}
{script_body}
"""
return subprocess.run(["bash", "-c", wrapper], capture_output=True, text=True, env=env, cwd=tmp_dir)
finally:
shutil.rmtree(tmp_dir, ignore_errors=True)
def _lib_helper_src():
content = open(_LIB_SH, encoding="utf-8").read()
start = content.find("_herdr_ws_id_file() {")
end = content.find('cmd="${1:-}"', start)
assert start != -1 and end != -1
return content[start:end]
def test_c1_hint_tokens_only_dialog_closed():
"""C-1: Hint tokens (Use arrow keys / Esc to cancel / Press Enter) alone must return dialog CLOSED."""
screen = """Select an option:
1. Option A
2. Option B
Use arrow keys to navigate · Esc to cancel
"""
script = f"""
_pane_capture() {{ printf '%s' '{screen}'; }}
if _pane_dialog_open dummy; then echo "DIALOG_OPEN"; else echo "DIALOG_CLOSED"; fi
"""
res = _run_lib_helpers(script)
assert res.returncode == 0, res.stderr + res.stdout
assert "DIALOG_CLOSED" in res.stdout
def test_c1b_hint_press_enter_sends_enter():
"""C-1b (B-3): 'Press Enter to continue' alone must return dialog CLOSED, and wait_for_tui_ready must send Enter exactly once per iteration (30 total across 30 iterations, not 60)."""
screen = """Some startup banner
Press Enter to continue
"""
script = f"""
_pane_capture() {{ printf '%s' '{screen}'; }}
KEYS=/tmp/mam-press-enter-keys.$$
: > "$KEYS"
_sks_herdr() {{
if [ "${{1:-}}" = "send-keys" ]; then echo "$*" >> "$KEYS"; fi
return 0
}}
sleep() {{ :; }}
if _pane_dialog_open dummy; then echo "DIALOG_OPEN"; else echo "DIALOG_CLOSED"; fi
export MAM_STRONG_READY_TOKENS='NonExistentStrong'
export MAM_WEAK_READY_TOKENS=''
export MAM_INPUT_PLACEHOLDER=''
rc=0
wait_for_tui_ready dummy-sess custom || rc=$?
echo "RC=$rc"
echo "KEYS_COUNT=$(grep -c "send-keys -t dummy-sess Enter" "$KEYS" || echo 0)"
rm -f "$KEYS"
"""
res = _run_lib_helpers(script)
assert res.returncode == 0, res.stderr + res.stdout
assert "DIALOG_CLOSED" in res.stdout
assert "KEYS_COUNT=30" in res.stdout
def test_c1c_transition_to_upsell_modal_sends_escape_not_enter():
"""C-1c: Screen transition from Press Enter to Upsell Modal (Yes, try it) sends Escape, not duplicate Enter."""
state_file = f"/tmp/mam-screen-state.{os.getpid()}"
script = f"""
echo "1" > "{state_file}"
_pane_capture() {{
local s
s=$(cat "{state_file}" 2>/dev/null || echo 1)
if [ "$s" = "1" ]; then
printf 'Some banner\\nPress Enter to continue\\n'
else
printf 'Claude Code\\nYes, try it\\n'
fi
}}
KEYS=/tmp/mam-transition-keys.{os.getpid()}
: > "$KEYS"
_sks_herdr() {{
if [ "${{1:-}}" = "send-keys" ]; then
echo "$*" >> "$KEYS"
# Transition to screen 2 on first Enter
if [ "${{4:-}}" = "Enter" ]; then
echo "2" > "{state_file}"
fi
fi
return 0
}}
sleep() {{ :; }}
export MAM_STRONG_READY_TOKENS='AnthropicReady'
export MAM_WEAK_READY_TOKENS=''
export MAM_INPUT_PLACEHOLDER=''
export MAM_TUI_TIMEOUT=2
rc=0
wait_for_tui_ready dummy-sess claude 2 || rc=$?
echo "RC=$rc"
echo "KEYS_CONTENT=$(tr '\\n' '|' < "$KEYS")"
rm -f "$KEYS" "{state_file}"
"""
res = _run_lib_helpers(script)
assert res.returncode == 0, res.stderr + res.stdout
assert "send-keys -t dummy-sess Enter" in res.stdout
assert "send-keys -t dummy-sess Escape" in res.stdout
assert "send-keys -t dummy-sess Enter|send-keys -t dummy-sess Escape|" in res.stdout
def test_c2_modal_tokens_dialog_open():
"""C-2: High-confidence modal tokens (e.g. Do you trust the files) must return dialog OPEN."""
screen = """Security Alert
Do you trust the files in this repository?
"""
script = f"""
_pane_capture() {{ printf '%s' '{screen}'; }}
if _pane_dialog_open dummy; then echo "DIALOG_OPEN"; else echo "DIALOG_CLOSED"; fi
"""
res = _run_lib_helpers(script)
assert res.returncode == 0, res.stderr + res.stdout
assert "DIALOG_OPEN" in res.stdout
def test_c3_strong_token_and_hint_token_readiness_succeeds():
"""C-3: Strong ready token + hint token together must detect readiness immediately (rc 0)."""
screen = """Cline v3.0.0
Use arrow keys to select a tool
"""
script = f"""
_pane_capture() {{ printf '%s' '{screen}'; }}
sleep() {{ :; }}
wait_for_tui_ready dummy-sess cline
"""
res = _run_lib_helpers(script)
assert res.returncode == 0, res.stderr + res.stdout
assert "TUI detected ready" in res.stdout
def test_c4_placeholder_only_readiness_succeeds():
"""C-4: Long placeholder (Ask anything..., len>=8) alone satisfies S-tier readiness."""
screen = """Welcome to the AI Assistant
Ask anything...
"""
script = f"""
_pane_capture() {{ printf '%s' '{screen}'; }}
sleep() {{ :; }}
# Override ready tokens so strong tokens do not match
export MAM_STRONG_READY_TOKENS='NonExistentStrongToken'
export MAM_WEAK_READY_TOKENS=''
export MAM_INPUT_PLACEHOLDER='Ask anything...'
wait_for_tui_ready dummy-sess cline
"""
res = _run_lib_helpers(script)
assert res.returncode == 0, res.stderr + res.stdout
assert "TUI detected ready" in res.stdout
def test_c4b_box_modal_does_not_succeed():
"""C-4b: Box modal frame (────── + solo ) with no strong token or placeholder must NOT satisfy readiness."""
screen = """┌───────────────────────────────────┐
│ Please select authentication mode │
│ ───────────────────────────────── │
│ 1. API Key │
│ 2. OAuth │
└───────────────────────────────────┘
"""
script = f"""
_pane_capture() {{ printf '%s' '{screen}'; }}
sleep() {{ :; }}
export MAM_STRONG_READY_TOKENS='NonExistentStrongToken'
export MAM_WEAK_READY_TOKENS='Chat|history'
export MAM_INPUT_PLACEHOLDER='Ask anything...'
export MAM_INPUT_PROMPT=''
export MAM_INPUT_RULE_PATTERN='─{{10,}}'
_MAM_MODAL_TOKENS='NON_MATCHING_MODAL'
rc=0
wait_for_tui_ready dummy-sess cline || rc=$?
echo "RC=$rc"
"""
res = _run_lib_helpers(script)
assert res.returncode == 0, res.stderr + res.stdout
assert "RC=1" in res.stdout
def test_c4c_prompt_and_rule_corroboration_alone_cannot_satisfy_readiness():
"""C-4c: Neither prompt alone, rule alone, nor prompt+rule alone can satisfy readiness (C-tier cannot promote alone)."""
screen = """────────────────────────────
"""
script = f"""
_pane_capture() {{ printf '%s' '{screen}'; }}
sleep() {{ :; }}
export MAM_STRONG_READY_TOKENS='NonExistentToken'
export MAM_WEAK_READY_TOKENS='NonExistentWeak'
export MAM_INPUT_PLACEHOLDER='NonExistentPlaceholder'
export MAM_INPUT_PROMPT=''
export MAM_INPUT_RULE_PATTERN='─{{10,}}'
rc=0
wait_for_tui_ready dummy-sess cline || rc=$?
echo "RC=$rc"
"""
res = _run_lib_helpers(script)
assert res.returncode == 0, res.stderr + res.stdout
assert "RC=1" in res.stdout
def test_c4d_weak_token_requires_corroboration():
"""C-4d: Weak token (Chat) alone fails; Weak token + prompt in last 5 lines succeeds."""
# 1. Weak token alone without prompt in last 5 lines
screen_weak_alone = """Chat history from earlier session:
line 1
line 2
line 3
line 4
line 5
line 6
"""
script1 = f"""
_pane_capture() {{ printf '%s' '{screen_weak_alone}'; }}
sleep() {{ :; }}
export MAM_STRONG_READY_TOKENS='NonExistentStrong'
export MAM_WEAK_READY_TOKENS='Chat|history'
export MAM_INPUT_PLACEHOLDER=''
export MAM_INPUT_PROMPT=''
export MAM_INPUT_RULE_PATTERN=''
rc=0
wait_for_tui_ready dummy-sess cline || rc=$?
echo "RC=$rc"
"""
res1 = _run_lib_helpers(script1)
assert res1.returncode == 0, res1.stderr + res1.stdout
assert "RC=1" in res1.stdout
# 2. Weak token + prompt in last 5 lines
screen_weak_corroborated = """Chat Session Active
Ready for input
"""
script2 = f"""
_pane_capture() {{ printf '%s' '{screen_weak_corroborated}'; }}
sleep() {{ :; }}
export MAM_STRONG_READY_TOKENS='NonExistentStrong'
export MAM_WEAK_READY_TOKENS='Chat|history'
export MAM_INPUT_PLACEHOLDER=''
export MAM_INPUT_PROMPT=''
export MAM_INPUT_RULE_PATTERN=''
rc=0
wait_for_tui_ready dummy-sess cline || rc=$?
echo "RC=$rc"
"""
res2 = _run_lib_helpers(script2)
assert res2.returncode == 0, res2.stderr + res2.stdout
assert "RC=0" in res2.stdout
def test_c4e_short_placeholder_not_promoted_to_strong():
"""C-4e: Placeholder with length < 8 chars must NOT be promoted to strong S-tier."""
screen = """Ask
"""
script = f"""
_pane_capture() {{ printf '%s' '{screen}'; }}
sleep() {{ :; }}
export MAM_STRONG_READY_TOKENS='NonExistentStrong'
export MAM_WEAK_READY_TOKENS=''
export MAM_INPUT_PLACEHOLDER='Ask'
rc=0
wait_for_tui_ready dummy-sess custom || rc=$?
echo "RC=$rc"
"""
res = _run_lib_helpers(script)
assert res.returncode == 0, res.stderr + res.stdout
assert "RC=1" in res.stdout
def test_c5_hermes_fallback_to_strong_only():
"""C-5: When input_* are all empty (Hermes), strong_ready_tokens alone satisfies readiness."""
screen = """Hermes Agent v1.0
System initialized.
"""
script = f"""
_pane_capture() {{ printf '%s' '{screen}'; }}
sleep() {{ :; }}
wait_for_tui_ready dummy-sess hermes
"""
res = _run_lib_helpers(script)
assert res.returncode == 0, res.stderr + res.stdout
assert "TUI detected ready" in res.stdout
def test_c6_consecutive_empty_captures_unobservable_rc2():
"""C-6: Consecutive empty captures give up as unobservable/headless (rc 2) without burning 30s."""
script = """
_pane_capture() { echo ""; }
sleep() { :; }
export MAM_READY_EMPTY_GIVEUP=3
rc=0
wait_for_tui_ready dummy-sess claude || rc=$?
echo "RC=$rc"
"""
res = _run_lib_helpers(script)
assert res.returncode == 0, res.stderr + res.stdout
assert "RC=2" in res.stdout
assert "unobservable/headless pane" in res.stderr
def test_c7_readiness_checks_decoded_text_not_raw_json():
"""C-7: Raw JSON field names (e.g. Chat in JSON envelope) do not cause false positives."""
raw_json = json.dumps({"result": {"read": {"text": "Just raw text with no ready tokens", "Chat": 1}}})
script = f"""
_sks_herdr() {{
if [ "${{1:-}}" = "capture-pane" ]; then
printf '%s' '{raw_json}'
fi
return 0
}}
sleep() {{ :; }}
export MAM_STRONG_READY_TOKENS='NonExistentStrong'
export MAM_WEAK_READY_TOKENS='Chat'
export MAM_INPUT_PLACEHOLDER=''
export MAM_INPUT_PROMPT=''
export MAM_INPUT_RULE_PATTERN=''
rc=0
wait_for_tui_ready dummy-sess cline || rc=$?
echo "RC=$rc"
"""
res = _run_lib_helpers(script)
assert res.returncode == 0, res.stderr + res.stdout
assert "RC=1" in res.stdout
def test_c8_mam_dialog_tokens_is_union_of_modal_and_hint():
"""C-8: _MAM_DIALOG_TOKENS must remain the union of _MAM_MODAL_TOKENS and _MAM_HINT_TOKENS."""
script = """
echo "MODAL=$_MAM_MODAL_TOKENS"
echo "HINT=$_MAM_HINT_TOKENS"
echo "DIALOG=$_MAM_DIALOG_TOKENS"
"""
res = _run_lib_helpers(script)
assert res.returncode == 0, res.stderr + res.stdout
lines = dict(line.split("=", 1) for line in res.stdout.strip().splitlines())
expected_dialog = f"{lines['MODAL']}|{lines['HINT']}"
assert lines["DIALOG"] == expected_dialog
def test_c8b_agent_modal_tokens_merged_into_dialog_open():
"""C-8b (T-2d / F-8): Per-agent modal_tokens from adapter are merged into _pane_dialog_open and block send_keys_safe."""
# 1. Direct _pane_dialog_open test
screen = """Please enter your Cline API key to continue:
"""
script1 = f"""
_pane_capture() {{ printf '%s' '{screen}'; }}
export MAM_MODAL_TOKENS='Cline API key'
if _pane_dialog_open dummy; then echo "DIALOG_OPEN"; else echo "DIALOG_CLOSED"; fi
"""
res1 = _run_lib_helpers(script1)
assert res1.returncode == 0, res1.stderr + res1.stdout
assert "DIALOG_OPEN" in res1.stdout
# 2. Dynamic facts resolution via send_keys_safe on cline session
script2 = f"""
_pane_quiescent() {{ return 0; }}
_pane_capture() {{ printf '%s' 'Select an API provider to get started:'; }}
_sks_herdr() {{ return 0; }}
rc=0
SKS_DIALOG_TIMEOUT=1 send_keys_safe "my-project-worker-cline-01" "some text" || rc=$?
echo "RC=$rc"
"""
res2 = _run_lib_helpers(script2)
assert res2.returncode == 0, res2.stderr + res2.stdout
assert "RC=2" in res2.stdout, f"Expected dialog blocking (RC=2) for cline modal: {res2.stdout}"
def test_c9_create_session_timeout_alive_pid_preserves_session(mam_sandbox, mock_herdr, mock_agents):
"""C-9 & C-11: create_session with readiness timeout + alive PID preserves session with timeout status (exit 0) and dumps diagnostic."""
script_path = mam_sandbox / ".agents" / "skills" / "multi-agent-mux-create" / "scripts" / "create_session.sh"
lib_path = mam_sandbox / ".agents" / "skills" / "lib.sh"
diag_dir = mam_sandbox / ".mam" / "diagnostics"
yaml_file = mam_sandbox / ".mam" / "agent-sessions.yaml"
# Make wait_for_tui_ready immediately return 1 in sandboxed lib.sh
lib_content = lib_path.read_text()
lib_path.write_text(lib_content.replace("wait_for_tui_ready() {", "wait_for_tui_ready() { return 1; #"))
# In sandbox create_session.sh, simulate alive process using $$
create_content = script_path.read_text()
script_path.write_text(create_content.replace(
'PANE_PID_CHECK=$(_herdr list-panes',
'PANE_PID_CHECK=$$ # $(_herdr list-panes'
))
cmd = [
"bash", str(script_path),
"--workspace", str(mam_sandbox),
"--agent", "cline",
"--role", "creator",
"--session", "test-timeout-sess",
"--onboard"
]
res = subprocess.run(cmd, capture_output=True, text=True, cwd=str(mam_sandbox))
assert res.returncode == 0, res.stderr + res.stdout
assert "WARNING: agent TUI never became ready within timeout, but process" in res.stderr
# Check diagnostic dump file created (C-11)
dump_files = list(diag_dir.glob("test-timeout-sess-*.txt"))
assert len(dump_files) == 1
dump_content = dump_files[0].read_text()
assert "ReturnCode: 1" in dump_content
# Check YAML registration with tui-ready-timeout and NO delegate job (N-C)
assert yaml_file.exists()
yaml_content = yaml_file.read_text()
assert "tui-ready-timeout" in yaml_content
assert "delegate_job_id: null" in yaml_content or "delegate_job_id: None" in yaml_content or "delegate_job_id:" not in yaml_content or "delegate_job_id: ''" in yaml_content
def test_c10_create_session_timeout_dead_pid_kills_session(mam_sandbox, mock_herdr, mock_agents):
"""C-10: create_session with readiness timeout + dead PID triggers rollback kill and exits with code 42."""
script_path = mam_sandbox / ".agents" / "skills" / "multi-agent-mux-create" / "scripts" / "create_session.sh"
lib_path = mam_sandbox / ".agents" / "skills" / "lib.sh"
diag_dir = mam_sandbox / ".mam" / "diagnostics"
# Make wait_for_tui_ready return 1 and mock list-panes to return dead PID
lib_content = lib_path.read_text()
create_content = script_path.read_text()
script_path.write_text(create_content.replace(
'PANE_PID_CHECK=$(_herdr list-panes',
'PANE_PID_CHECK=99999999 # $(_herdr list-panes'
))
lib_path.write_text(lib_content.replace("wait_for_tui_ready() {", "wait_for_tui_ready() { return 1; #"))
cmd = [
"bash", str(script_path),
"--workspace", str(mam_sandbox),
"--agent", "cline",
"--role", "creator",
"--session", "test-dead-sess"
]
res = subprocess.run(cmd, capture_output=True, text=True, cwd=str(mam_sandbox))
assert res.returncode == 42, res.stderr + res.stdout
assert "ERROR: agent TUI never became ready" in res.stderr
# Check diagnostic dump file created
dump_files = list(diag_dir.glob("test-dead-sess-*.txt"))
assert len(dump_files) == 1
assert "ReturnCode: 1" in dump_files[0].read_text()
def test_adapter_strong_weak_partition():
"""N-D: Verify strong_ready_tokens and weak_ready_tokens partition ready_tokens for all adapters."""
from lib_py.agents.registry import get_adapter
for agent in ('claude', 'agy', 'hermes', 'cline', 'grok'):
adapter = get_adapter(agent)
assert adapter is not None
ready_set = set(t for t in adapter.ready_tokens.split('|') if t)
strong_set = set(t for t in adapter.strong_ready_tokens.split('|') if t)
weak_set = set(t for t in adapter.weak_ready_tokens.split('|') if t)
assert strong_set | weak_set == ready_set, f"Mismatch in {agent}: {strong_set} | {weak_set} != {ready_set}"
assert strong_set.isdisjoint(weak_set), f"Overlap in {agent}: {strong_set & weak_set}"
def test_sks_agent_suffix_resolution_not_substring():
"""Verify send_keys_safe resolves agent by strict suffix without substring false positives."""
script = """
_pane_quiescent() { return 0; }
_pane_capture() { echo "prompt > "; }
_sks_herdr() { return 0; }
RESOLVED=""
mock_py() {
if [ "$1" = "-m" ] && [ "$2" = "lib_py.agents" ] && [ "$3" = "facts" ]; then
echo "echo RESOLVED_AGENT=$4"
fi
}
_delegate_py_bin() { echo "mock_py"; }
check_sks() {
local sess="$1"
unset MAM_MODAL_TOKENS
# Call send_keys_safe and capture output
send_keys_safe "$sess" "hello" 2>&1 || true
}
echo "1: $(check_sks 'claude-reviewer-cline-01')"
echo "2: $(check_sks 'my-claude-creator-cline')"
echo "3: $(check_sks 'reviewer-creator-grok-01')"
echo "4: $(check_sks 'planner-reviewer-claude-01')"
echo "5: $(check_sks 'test-worker-hermes-02')"
echo "6: $(check_sks 'workspace-creator-agy-05')"
echo "7: $(check_sks 'claude')"
echo "8: $(check_sks 'cline-01')"
echo "9: $(check_sks 'grok-02')"
"""
res = _run_lib_helpers(script)
assert res.returncode == 0, res.stderr + res.stdout
assert "1: RESOLVED_AGENT=cline" in res.stdout
assert "2: RESOLVED_AGENT=cline" in res.stdout
assert "3: RESOLVED_AGENT=grok" in res.stdout
assert "4: RESOLVED_AGENT=claude" in res.stdout
assert "5: RESOLVED_AGENT=hermes" in res.stdout
assert "6: RESOLVED_AGENT=agy" in res.stdout
assert "7: RESOLVED_AGENT=claude" in res.stdout
assert "8: RESOLVED_AGENT=cline" in res.stdout
assert "9: RESOLVED_AGENT=grok" in res.stdout
def test_f2_upsell_modal_defense_with_real_facts_and_banner():
"""F-2/F-6: Real Claude Code banner with upsell modal must trigger modal defense, not return ready."""
script = """
SENT_KEYS=""
_sks_herdr() {
if [ "$1" = "send-keys" ]; then
SENT_KEYS="$SENT_KEYS $*"
fi
return 0
}
COUNT_FILE=$(mktemp)
echo "0" > "$COUNT_FILE"
_pane_capture() {
local c
c=$(cat "$COUNT_FILE")
c=$((c + 1))
echo "$c" > "$COUNT_FILE"
if [ "$c" -le 2 ]; then
# First 2 captures (loop iter 1 capture + tail check): Claude Code banner with Fullscreen Modal
cat << 'EOF'
Claude Code (Sonnet 3.5)
Try the new fullscreen renderer?
Flicker-free output
Selected text auto-copies to your clipboard
Yes, try it
EOF
else
# Subsequent captures (after Escape sent): modal dismissed, normal prompt
cat << 'EOF'
Claude Code (Sonnet 3.5)
EOF
fi
}
rc=0
wait_for_tui_ready "my-claude-sess" "claude" || rc=$?
rm -f "$COUNT_FILE"
echo "RC=$rc"
echo "KEYS=$SENT_KEYS"
"""
res = _run_lib_helpers(script)
assert res.returncode == 0, res.stderr + res.stdout
assert "Escape" in res.stdout, f"Expected Escape to be sent for upsell modal: {res.stdout}"
assert "RC=0" in res.stdout
def test_r1_resolve_pane_id_fail_closed_on_multiple_same_kind_panes():
"""R-1: If fallback finds multiple panes of same agent kind, fail closed (exit 1)."""
helper = _lib_helper_src()
script = helper + """
# Mock real herdr pane list returning 2 claude panes
_real_herdr() {
cat << 'EOF'
{"result": {"panes": [
{"pane_id": "w1:p1", "label": "unrelated-1", "agent": "claude"},
{"pane_id": "w1:p2", "label": "unrelated-2", "agent": "claude"}
]}}
EOF
}
# Create a dummy yaml with unknown session mapped to claude cmd
mkdir -p .mam
cat << 'EOF' > .mam/agent-sessions.yaml
herdr_sessions:
- name: my-unmatched-session
pane:
cmd: claude
EOF
rc=0
pid=$(_resolve_herdr_pane_id "my-unmatched-session") || rc=$?
echo "RC=$rc|PID=$pid"
"""
res = _run_lib_helpers(script)
assert res.returncode == 0, res.stderr + res.stdout
assert "RC=1|PID=" in res.stdout, f"Expected fail-closed RC=1 and empty PID: {res.stdout}"