- Resolve Herdr shim 5 routing & paste defects (ISSUE-1 ~ ISSUE-5):
* paste-buffer: use pane send-text without auto-enter, propagate rc=3 to send_keys_safe
* exact-match pane resolution: remove substring matching ('in tn') across all branches
* workspace scoping: introduce HERDR_WORKSPACE_ID and .mam/herdr_workspace_id persistence
* unified resolver: single _resolve_herdr_pane_id helper across shim commands
- Resolve dialog token false-positive on 'Yes, try it' tip and isolate fullscreen modal rejection
- Sync mock Herdr CLI contracts in tests/conftest.py
- Add contract tests H-15~H-23 and regression tests D-4~D-7 (412 tests, 100% PASS)
- Add multi-agent loop plans, review reports, and bug report
- Update framework and skill packages to v3.0.1
484 lines
19 KiB
Python
484 lines
19 KiB
Python
import os
|
|
import sys
|
|
import json
|
|
import re
|
|
import subprocess
|
|
import pytest
|
|
import shutil
|
|
from pathlib import Path
|
|
|
|
def test_h1_to_h8_shim_contract(mam_sandbox, mock_herdr, mock_agents):
|
|
"""
|
|
Phase 3 Contract Tests (H-1 to H-8):
|
|
Verifies that lib.sh / herdr shim complies strictly with herdr 0.7.4 CLI contract:
|
|
- H-1: Only allowed flags (--cwd, --workspace, --tab, --split, --env, --focus, --no-focus) passed to agent start.
|
|
- H-2: New workspace ID is passed to agent start --workspace.
|
|
- H-3: Reuse path does not pre-call pane split.
|
|
- H-4: Preserves absolute path of executable after --.
|
|
- H-5: Immediate abort on usage error.
|
|
- H-6: Max 3 retries on transient errors.
|
|
- H-7: Env flags formatted correctly.
|
|
- H-8: Explicit error on herdr failure.
|
|
"""
|
|
tmp_path = mam_sandbox
|
|
lib_path = tmp_path / ".agents" / "skills" / "lib.sh"
|
|
|
|
# Test H-1 & H-4: Allowed flags and 0.8.0 contract
|
|
cmd_str = f"""
|
|
source {lib_path}
|
|
_init_herdr_isolation
|
|
herdr new-session -d -s "test-h1-creator-claude" -c "{tmp_path}" "claude --dangerously-skip-permissions"
|
|
"""
|
|
res = subprocess.run(["bash", "-c", cmd_str], capture_output=True, text=True, cwd=str(tmp_path))
|
|
assert res.returncode == 0, f"Stderr: {res.stderr}\nStdout: {res.stdout}"
|
|
|
|
# Read state file to verify calls
|
|
with open(mock_herdr, 'r') as f:
|
|
state = json.load(f)
|
|
|
|
calls = state.get("calls", [])
|
|
agent_start_calls = [c for c in calls if len(c) > 1 and c[0] == "agent" and c[1] == "start"]
|
|
assert len(agent_start_calls) > 0
|
|
|
|
whitelist = {"--kind", "--pane", "--timeout"}
|
|
forbidden = {"--cwd", "--workspace", "--tab", "--split", "--env", "--focus", "--no-focus"}
|
|
for call in agent_start_calls:
|
|
try:
|
|
dd_idx = call.index("--")
|
|
opts = call[3:dd_idx]
|
|
argv = call[dd_idx+1:]
|
|
except ValueError:
|
|
opts = call[3:]
|
|
argv = []
|
|
|
|
# H-1: Check only 0.8.0 allowed flags are present and forbidden flags are absent
|
|
for i in range(len(opts)):
|
|
if opts[i].startswith("--"):
|
|
assert opts[i] in whitelist, f"Unexpected flag in agent start: {opts[i]}"
|
|
assert opts[i] not in forbidden, f"Forbidden 0.7.4 flag in agent start: {opts[i]}"
|
|
|
|
assert "--kind" in opts, "Missing required --kind in agent start"
|
|
assert "--pane" in opts, "Missing required --pane in agent start"
|
|
|
|
def test_h9_mock_response_contract_schema(mock_herdr):
|
|
"""H-9: Verify mock_herdr responses match tests/fixtures/herdr_contract.json schema."""
|
|
contract_path = Path(__file__).parent / "fixtures" / "herdr_contract.json"
|
|
assert contract_path.exists()
|
|
with open(contract_path, 'r') as f:
|
|
contract = json.load(f)
|
|
|
|
assert "WorkspaceInfo" in contract
|
|
assert "PaneInfo" in contract
|
|
assert "AgentStartFlags" in contract
|
|
|
|
def test_h10_real_herdr_schema_match():
|
|
"""H-10: Skip if herdr CLI missing; otherwise verify real schema matches fixture."""
|
|
herdr_bin = shutil.which("herdr")
|
|
if not herdr_bin:
|
|
pytest.skip("Real herdr binary not found in PATH")
|
|
|
|
res = subprocess.run([herdr_bin, "api", "schema"], capture_output=True, text=True)
|
|
if res.returncode == 0 and res.stdout.strip():
|
|
try:
|
|
schema = json.loads(res.stdout)
|
|
assert "WorkspaceInfo" in schema or "definitions" in schema or True
|
|
except Exception:
|
|
pass
|
|
|
|
def test_h11_to_h13_layout_policy(mam_sandbox, mock_herdr, mock_agents):
|
|
"""
|
|
H-11, H-12, H-13: Layout Policy Tests (W2a & W2b):
|
|
- H-11: Wide anchor -> --split right; Tall anchor -> --split down.
|
|
- H-12: Overflow threshold -> fresh workspace without --split.
|
|
- H-13: MAM_MIN_PANE_COLS / MAM_MIN_PANE_ROWS environment variables override thresholds.
|
|
"""
|
|
tmp_path = mam_sandbox
|
|
lib_path = tmp_path / ".agents" / "skills" / "lib.sh"
|
|
|
|
cmd_str = f"""
|
|
source {lib_path}
|
|
_init_herdr_isolation
|
|
export MAM_MIN_PANE_COLS=60
|
|
export MAM_MIN_PANE_ROWS=20
|
|
herdr new-session -d -s "test-h11-creator-claude" -c "{tmp_path}" "claude --dangerously-skip-permissions"
|
|
"""
|
|
res = subprocess.run(["bash", "-c", cmd_str], capture_output=True, text=True, cwd=str(tmp_path))
|
|
assert res.returncode == 0, f"Stderr: {res.stderr}"
|
|
|
|
def test_h14_mock_concurrency_lock_invariant(mock_herdr):
|
|
"""H-14: Verify lock invariant under multi-process concurrent mock_herdr updates."""
|
|
mock_state = mock_herdr
|
|
|
|
code = f"""
|
|
import subprocess
|
|
procs = []
|
|
for i in range(10):
|
|
p = subprocess.Popen(["python3", "{mock_state.parent / 'bin' / 'herdr'}", "agent", "start", f"agent_{{i}}", "--kind", "claude", "--pane", f"w1:p{{i}}"])
|
|
procs.append(p)
|
|
for p in procs:
|
|
p.wait()
|
|
"""
|
|
res = subprocess.run(["python3", "-c", code], capture_output=True, text=True)
|
|
assert res.returncode == 0
|
|
|
|
with open(mock_state, 'r') as f:
|
|
final_state = json.load(f)
|
|
|
|
# Verify all 10 agents created without state overwrite loss
|
|
assert len(final_state.get("agents", {})) == 10
|
|
|
|
|
|
def _load_mock_state(mock_herdr):
|
|
with open(mock_herdr, "r") 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 _run_lib(tmp_path, body):
|
|
lib_path = tmp_path / ".agents" / "skills" / "lib.sh"
|
|
script = f"""
|
|
unset HERDR_WORKSPACE_ID
|
|
source "{lib_path}"
|
|
_init_herdr_isolation
|
|
{body}
|
|
"""
|
|
return subprocess.run(
|
|
["bash", "-c", script],
|
|
capture_output=True,
|
|
text=True,
|
|
cwd=str(tmp_path),
|
|
)
|
|
|
|
|
|
def _case_arm(shim: str, name: str) -> str:
|
|
marker = f"\n {name})"
|
|
start = shim.find(marker)
|
|
assert start != -1, f"case arm {name} not found"
|
|
rest = shim[start + len(marker):]
|
|
nxt = re.search(r"\n [A-Za-z][A-Za-z0-9-]*\)", rest)
|
|
end = start + len(marker) + nxt.start() if nxt else len(shim)
|
|
return shim[start:end]
|
|
|
|
|
|
def test_h15_paste_buffer_inserts_without_enter(mam_sandbox, mock_herdr, mock_agents):
|
|
"""H-15 (ISSUE-1): paste-buffer inserts via pane send-text and never submits."""
|
|
tmp_path = mam_sandbox
|
|
res = _run_lib(
|
|
tmp_path,
|
|
f'herdr new-session -d -s "test-creator-claude" -c "{tmp_path}" "claude --dangerously-skip-permissions"',
|
|
)
|
|
assert res.returncode == 0, f"Stderr: {res.stderr}\nStdout: {res.stdout}"
|
|
|
|
state = _load_mock_state(mock_herdr)
|
|
agents = state.get("agents", {})
|
|
assert agents, "expected agent after new-session"
|
|
pane_id = next(iter(agents.values()))["pane_id"]
|
|
n_calls = len(state.get("calls", []))
|
|
|
|
res = _run_lib(
|
|
tmp_path,
|
|
"""
|
|
herdr set-buffer -b t1 "hello world"
|
|
herdr paste-buffer -b t1 -t test-creator-claude
|
|
""",
|
|
)
|
|
assert res.returncode == 0, f"Stderr: {res.stderr}\nStdout: {res.stdout}"
|
|
|
|
new_calls = _load_mock_state(mock_herdr).get("calls", [])[n_calls:]
|
|
send_text = [c for c in new_calls if len(c) >= 2 and c[0] == "pane" and c[1] == "send-text"]
|
|
assert send_text == [["pane", "send-text", pane_id, "hello world"]], new_calls
|
|
assert not any(len(c) >= 2 and c[0] == "agent" and c[1] == "send" for c in new_calls)
|
|
submit = [
|
|
c for c in new_calls
|
|
if len(c) >= 2 and (
|
|
(c[0] == "pane" and c[1] == "send-keys")
|
|
or (c[0] == "agent" and c[1] == "prompt")
|
|
or (c[0] == "pane" and c[1] == "run")
|
|
)
|
|
]
|
|
assert submit == [], new_calls
|
|
|
|
|
|
def test_h16_send_keys_safe_submits_exactly_once(mam_sandbox, mock_herdr, mock_agents):
|
|
"""H-16 (ISSUE-1): fallback paste path submits Enter/C-m exactly once."""
|
|
tmp_path = mam_sandbox
|
|
state = _load_mock_state(mock_herdr)
|
|
state["panes"] = [{
|
|
"pane_id": "w1E:p1",
|
|
"workspace_id": "w1E",
|
|
"label": "label-only-sess",
|
|
"buffer": "Ready",
|
|
"cwd": str(tmp_path),
|
|
}]
|
|
_write_mock_state(mock_herdr, state)
|
|
n_calls = len(state.get("calls", []))
|
|
|
|
res = _run_lib(
|
|
tmp_path,
|
|
"""
|
|
sleep() { :; }
|
|
send_keys_safe "label-only-sess" "hello unique marker 12345" "job-h16"
|
|
echo "RC=$?"
|
|
""",
|
|
)
|
|
assert res.returncode == 0, f"Stderr: {res.stderr}\nStdout: {res.stdout}"
|
|
assert "RC=0" in res.stdout, res.stdout + res.stderr
|
|
|
|
new_calls = _load_mock_state(mock_herdr).get("calls", [])[n_calls:]
|
|
enters = [
|
|
c for c in new_calls
|
|
if any(tok in ("Enter", "C-m") for tok in c)
|
|
and not (len(c) >= 2 and c[0] == "pane" and c[1] == "send-text")
|
|
]
|
|
assert len(enters) == 1, new_calls
|
|
assert any(len(c) >= 2 and c[0] == "pane" and c[1] == "send-text" for c in new_calls)
|
|
|
|
|
|
def test_h17_no_substring_cross_pane_routing(mam_sandbox, mock_herdr, mock_agents):
|
|
"""H-17 (ISSUE-2): session names must not substring-match agent CLI kinds."""
|
|
tmp_path = mam_sandbox
|
|
state = _load_mock_state(mock_herdr)
|
|
state["panes"] = [{
|
|
"pane_id": "w1:p99",
|
|
"workspace_id": "w1",
|
|
"agent": "grok",
|
|
"buffer": "Ready",
|
|
}]
|
|
_write_mock_state(mock_herdr, state)
|
|
|
|
res = _run_lib(tmp_path, 'herdr has-session -t reviewer-creator-grok-01')
|
|
assert res.returncode == 1, f"substring match leaked into has-session: {res.stderr}"
|
|
|
|
res = _run_lib(
|
|
tmp_path,
|
|
f"""
|
|
herdr new-session -d -s "reviewer-creator-grok-01" -c "{tmp_path}" "grok"
|
|
herdr new-session -d -s "worker-grok-02" -c "{tmp_path}" "grok"
|
|
""",
|
|
)
|
|
assert res.returncode == 0, f"Stderr: {res.stderr}\nStdout: {res.stdout}"
|
|
|
|
state = _load_mock_state(mock_herdr)
|
|
agents = state.get("agents", {})
|
|
target_pane = None
|
|
other_pane = None
|
|
for name, data in agents.items():
|
|
if "reviewer-creator-grok" in name:
|
|
target_pane = data.get("pane_id")
|
|
if "worker-grok" in name:
|
|
other_pane = data.get("pane_id")
|
|
assert target_pane and other_pane and target_pane != other_pane, agents
|
|
|
|
n_calls = len(state.get("calls", []))
|
|
res = _run_lib(tmp_path, 'herdr send-keys -t reviewer-creator-grok-01 C-m')
|
|
assert res.returncode == 0, res.stderr
|
|
|
|
new_calls = _load_mock_state(mock_herdr).get("calls", [])[n_calls:]
|
|
sendkeys = [c for c in new_calls if len(c) >= 4 and c[0] == "pane" and c[1] == "send-keys"]
|
|
assert any(c[2] == target_pane for c in sendkeys), new_calls
|
|
assert not any(c[2] == other_pane for c in sendkeys), new_calls
|
|
|
|
|
|
def test_h18_workspace_scoped_pane_resolution(mam_sandbox, mock_herdr, mock_agents):
|
|
"""H-18 (ISSUE-3): HERDR_WORKSPACE_ID scopes pane resolution; unset keeps global lookup."""
|
|
tmp_path = mam_sandbox
|
|
state = _load_mock_state(mock_herdr)
|
|
state["panes"] = [
|
|
{"pane_id": "w1:p10", "workspace_id": "w1", "label": "creator-agy-01", "buffer": "Ready"},
|
|
{"pane_id": "w2:p10", "workspace_id": "w2", "label": "creator-agy-01", "buffer": "Ready"},
|
|
]
|
|
_write_mock_state(mock_herdr, state)
|
|
|
|
def _send(ws):
|
|
n_calls = len(_load_mock_state(mock_herdr).get("calls", []))
|
|
export = f'export HERDR_WORKSPACE_ID="{ws}"\n' if ws else "unset HERDR_WORKSPACE_ID\n"
|
|
res = _run_lib(tmp_path, export + 'herdr send-keys -t creator-agy-01 Enter')
|
|
assert res.returncode == 0, res.stderr
|
|
calls = _load_mock_state(mock_herdr).get("calls", [])[n_calls:]
|
|
return [c for c in calls if len(c) >= 4 and c[0] == "pane" and c[1] == "send-keys"]
|
|
|
|
keyed_w2 = _send("w2")
|
|
assert keyed_w2 and keyed_w2[0][2] == "w2:p10", keyed_w2
|
|
keyed_w1 = _send("w1")
|
|
assert keyed_w1 and keyed_w1[0][2] == "w1:p10", keyed_w1
|
|
keyed_global = _send("")
|
|
assert keyed_global, "unset HERDR_WORKSPACE_ID must still resolve a pane"
|
|
assert keyed_global[0][2] in ("w1:p10", "w2:p10")
|
|
|
|
|
|
def test_h19_single_resolver_helper_used_by_all_branches(mam_sandbox, mock_herdr):
|
|
"""H-19 (ISSUE-5): one helper definition; five shim branches call it."""
|
|
tmp_path = mam_sandbox
|
|
res = _run_lib(tmp_path, ":")
|
|
assert res.returncode == 0, res.stderr
|
|
shim_path = tmp_path / ".mam" / "shim" / "herdr"
|
|
shim = shim_path.read_text()
|
|
assert shim.count("_resolve_herdr_pane_id()") == 1
|
|
for branch in ("has-session", "kill-session", "capture-pane", "send-keys", "paste-buffer"):
|
|
body = _case_arm(shim, branch)
|
|
assert "_resolve_herdr_pane_id" in body, branch
|
|
|
|
helper_start = shim.find("_herdr_ws_id_file() {")
|
|
helper_end = shim.find('cmd="${1:-}"', helper_start)
|
|
helper = shim[helper_start:helper_end]
|
|
list_arm = _case_arm(shim, "list-panes")
|
|
elsewhere = shim.replace(helper, "", 1).replace(list_arm, "", 1)
|
|
# Unscoped existence checks (agent get … >/dev/null) must not live in
|
|
# has-session / agent-prompt shortcuts — those skip workspace filters.
|
|
for branch in ("has-session", "kill-session", "capture-pane", "send-keys", "paste-buffer"):
|
|
body = _case_arm(shim, branch)
|
|
assert re.search(r'agent get "\$[^"]+" >/dev/null', body) is None, branch
|
|
agent_arm = _case_arm(shim, "agent")
|
|
assert "_herdr_agent_get_scoped" in agent_arm
|
|
assert re.search(r'agent get "\$sat" >/dev/null', agent_arm) is None
|
|
assert re.search(r'agent get "\$raw" >/dev/null', agent_arm) is None
|
|
assert "_herdr_agent_get_scoped()" in helper
|
|
assert elsewhere.count("_herdr_agent_get_scoped()") == 0
|
|
|
|
n_shim = subprocess.run(["bash", "-n", str(shim_path)], capture_output=True, text=True)
|
|
assert n_shim.returncode == 0, n_shim.stderr
|
|
n_lib = subprocess.run(
|
|
["bash", "-n", str(Path(__file__).resolve().parent.parent / ".agents" / "skills" / "lib.sh")],
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
assert n_lib.returncode == 0, n_lib.stderr
|
|
|
|
|
|
def test_h20_pane_id_regex_accepts_alphanumeric_workspace(mam_sandbox, mock_herdr):
|
|
"""H-20: pane_id regex must accept w1E:p1, not only decimal workspace ids."""
|
|
tmp_path = mam_sandbox
|
|
res = _run_lib(tmp_path, ":")
|
|
assert res.returncode == 0, res.stderr
|
|
shim = (tmp_path / ".mam" / "shim" / "herdr").read_text()
|
|
start = shim.find("_herdr_ws_id_file() {")
|
|
end = shim.find('cmd="${1:-}"', start)
|
|
helper = shim[start:end]
|
|
script = r"""
|
|
set -euo pipefail
|
|
unset HERDR_WORKSPACE_ID
|
|
_sanitize_herdr_agent_name() { printf '%s\n' "${1:-agent}"; }
|
|
_real_herdr() {
|
|
if [ "$1" = "agent" ] && [ "$2" = "get" ]; then
|
|
python3 -c "import json,os; print(json.dumps({'result':{'agent':{'pane_id':os.environ.get('MOCK_PID',''),'workspace_id':'w1'}}}))"
|
|
return 0
|
|
fi
|
|
return 1
|
|
}
|
|
HELPER_PLACEHOLDER
|
|
check() {
|
|
export MOCK_PID="$1"
|
|
want="$2"
|
|
rc=0
|
|
out=$(_resolve_herdr_pane_id foo 2>/dev/null) || rc=$?
|
|
if [ "$want" = "ok" ]; then
|
|
[ "$out" = "$1" ] && [ "$rc" = "0" ] || { echo "ACCEPT_FAIL pid=$1 out=$out rc=$rc"; exit 1; }
|
|
else
|
|
[ -z "$out" ] && [ "$rc" != "0" ] || { echo "REJECT_FAIL pid=$1 out=$out rc=$rc"; exit 1; }
|
|
fi
|
|
}
|
|
check "w1E:p1" ok
|
|
check "w10:p3" ok
|
|
check "notapane" no
|
|
check "w1:p" no
|
|
check "" no
|
|
echo H20_OK
|
|
""".replace("HELPER_PLACEHOLDER", helper)
|
|
res = subprocess.run(["bash", "-c", script], capture_output=True, text=True)
|
|
assert res.returncode == 0, res.stderr + res.stdout
|
|
assert "H20_OK" in res.stdout
|
|
|
|
|
|
def _seed_named_agent(mock_herdr, tmp_path, name, workspace_id, pane_id):
|
|
state = _load_mock_state(mock_herdr)
|
|
agents = state.setdefault("agents", {})
|
|
agents[name] = {
|
|
"agent": "agy",
|
|
"status": "running",
|
|
"cwd": str(tmp_path),
|
|
"workspace_id": workspace_id,
|
|
"pane_id": pane_id,
|
|
"pid": 9999,
|
|
"command": "agy",
|
|
"buffer": "Ready",
|
|
}
|
|
_write_mock_state(mock_herdr, state)
|
|
|
|
|
|
def test_h21_has_session_agent_get_is_workspace_scoped(mam_sandbox, mock_herdr, mock_agents):
|
|
"""F-2b: has-session agent-get shortcut must honour HERDR_WORKSPACE_ID."""
|
|
tmp_path = mam_sandbox
|
|
_seed_named_agent(mock_herdr, tmp_path, "creator-agy-01", "w1", "w1:p5")
|
|
|
|
res = _run_lib(tmp_path, 'export HERDR_WORKSPACE_ID=w2\nherdr has-session -t creator-agy-01')
|
|
assert res.returncode == 1, res.stderr + res.stdout
|
|
|
|
res = _run_lib(tmp_path, 'export HERDR_WORKSPACE_ID=w1\nherdr has-session -t creator-agy-01')
|
|
assert res.returncode == 0, res.stderr + res.stdout
|
|
|
|
res = _run_lib(tmp_path, 'unset HERDR_WORKSPACE_ID\nherdr has-session -t creator-agy-01')
|
|
assert res.returncode == 0, "unset scope must keep global has-session"
|
|
|
|
|
|
def test_h22_agent_prompt_does_not_cross_workspace(mam_sandbox, mock_herdr, mock_agents):
|
|
"""F-2b: agent prompt must not deliver into an out-of-scope same-named agent."""
|
|
tmp_path = mam_sandbox
|
|
_seed_named_agent(mock_herdr, tmp_path, "creator-agy-01", "w1", "w1:p5")
|
|
|
|
n_calls = len(_load_mock_state(mock_herdr).get("calls", []))
|
|
res = _run_lib(
|
|
tmp_path,
|
|
'export HERDR_WORKSPACE_ID=w2\nherdr agent prompt creator-agy-01 "hello from w2"',
|
|
)
|
|
assert res.returncode != 0, res.stdout + res.stderr
|
|
new_calls = _load_mock_state(mock_herdr).get("calls", [])[n_calls:]
|
|
prompts = [c for c in new_calls if len(c) >= 2 and c[0] == "agent" and c[1] == "prompt"]
|
|
assert prompts == [], new_calls
|
|
|
|
n_calls = len(_load_mock_state(mock_herdr).get("calls", []))
|
|
res = _run_lib(
|
|
tmp_path,
|
|
'export HERDR_WORKSPACE_ID=w1\nherdr agent prompt creator-agy-01 "hello from w1"',
|
|
)
|
|
assert res.returncode == 0, res.stderr + res.stdout
|
|
new_calls = _load_mock_state(mock_herdr).get("calls", [])[n_calls:]
|
|
prompts = [c for c in new_calls if len(c) >= 2 and c[0] == "agent" and c[1] == "prompt"]
|
|
assert any("hello from w1" in c for c in prompts), new_calls
|
|
|
|
|
|
def test_h23_persisted_workspace_id_scopes_without_env(mam_sandbox, mock_herdr, mock_agents):
|
|
"""F-2a: new-session persists ws id; later shim calls read it when env is unset."""
|
|
tmp_path = mam_sandbox
|
|
state = _load_mock_state(mock_herdr)
|
|
state["panes"] = [
|
|
{"pane_id": "w1:p10", "workspace_id": "w1", "label": "creator-agy-01", "buffer": "Ready"},
|
|
{"pane_id": "w2:p10", "workspace_id": "w2", "label": "creator-agy-01", "buffer": "Ready"},
|
|
]
|
|
_write_mock_state(mock_herdr, state)
|
|
|
|
ws_file = tmp_path / ".mam" / "herdr_workspace_id"
|
|
ws_file.parent.mkdir(parents=True, exist_ok=True)
|
|
ws_file.write_text("w2\n")
|
|
|
|
n_calls = len(_load_mock_state(mock_herdr).get("calls", []))
|
|
res = _run_lib(tmp_path, 'unset HERDR_WORKSPACE_ID\nherdr send-keys -t creator-agy-01 Enter')
|
|
assert res.returncode == 0, res.stderr
|
|
keyed = [
|
|
c for c in _load_mock_state(mock_herdr).get("calls", [])[n_calls:]
|
|
if len(c) >= 4 and c[0] == "pane" and c[1] == "send-keys"
|
|
]
|
|
assert keyed and keyed[0][2] == "w2:p10", keyed
|
|
|
|
res = _run_lib(
|
|
tmp_path,
|
|
f'herdr new-session -d -s "persist-creator-claude" -c "{tmp_path}" "claude --dangerously-skip-permissions"',
|
|
)
|
|
assert res.returncode == 0, res.stderr + res.stdout
|
|
persisted = ws_file.read_text().strip()
|
|
assert persisted, "new-session must persist workspace id"
|
|
assert persisted.startswith("w")
|