fix(herdr): resolve shim routing defects, add workspace scoping, and bump to v3.0.1
- 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
This commit is contained in:
@@ -165,8 +165,92 @@ rm -f "$KEYS"
|
||||
assert "Enter" not in res.stdout.split("KEYS_CONTENT=")[-1]
|
||||
|
||||
|
||||
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_resolve_pane_id_fails_cleanly_under_set_e():
|
||||
"""D-4: _resolve_herdr_pane_id must return 1 with empty stdout under set -e."""
|
||||
helper = _lib_helper_src()
|
||||
script = """
|
||||
set -euo pipefail
|
||||
unset HERDR_WORKSPACE_ID
|
||||
source "LIB_SH_PLACEHOLDER"
|
||||
_init_herdr_isolation() { :; }
|
||||
_real_herdr() { return 1; }
|
||||
HELPER_PLACEHOLDER
|
||||
_resolve_herdr_pane_id nonexistent >/dev/null 2>&1 || true
|
||||
echo SURVIVED
|
||||
rc=0
|
||||
out=$(_resolve_herdr_pane_id nonexistent 2>/dev/null) || rc=$?
|
||||
printf 'OUT=%s\\n' "$out"
|
||||
echo "RC=$rc"
|
||||
""".replace("LIB_SH_PLACEHOLDER", _LIB_SH).replace("HELPER_PLACEHOLDER", helper)
|
||||
res = subprocess.run(["bash", "-c", script], capture_output=True, text=True)
|
||||
assert res.returncode == 0, res.stderr + res.stdout
|
||||
assert "SURVIVED" in res.stdout
|
||||
assert "RC=1" in res.stdout
|
||||
out_line = [l for l in res.stdout.splitlines() if l.startswith("OUT=")][0]
|
||||
assert out_line == "OUT="
|
||||
|
||||
|
||||
def test_send_keys_safe_returns_3_when_paste_buffer_fails():
|
||||
"""D-5 (ISSUE-1): paste-buffer failure returns 3 and still deletes the buffer."""
|
||||
script = """
|
||||
_pane_quiescent() { return 0; }
|
||||
_pane_dialog_open() { return 1; }
|
||||
LOG=/tmp/mam-d5-sks.$$
|
||||
: > "$LOG"
|
||||
_sks_herdr() {
|
||||
echo "$*" >> "$LOG"
|
||||
if [ "${1:-}" = "agent" ] && [ "${2:-}" = "prompt" ]; then return 1; fi
|
||||
if [ "${1:-}" = "paste-buffer" ]; then return 1; fi
|
||||
return 0
|
||||
}
|
||||
rc=0
|
||||
send_keys_safe "d5-sess" "hello" "job-d5" || rc=$?
|
||||
echo "RC=$rc"
|
||||
echo "LOG_CONTENT=$(tr '\\n' '|' < "$LOG")"
|
||||
rm -f "$LOG"
|
||||
"""
|
||||
res = _run_lib_helpers(script)
|
||||
assert res.returncode == 0, res.stderr + res.stdout
|
||||
assert "RC=3" in res.stdout
|
||||
log = res.stdout.split("LOG_CONTENT=")[-1]
|
||||
assert "C-m" not in log
|
||||
assert "delete-buffer" in log
|
||||
|
||||
|
||||
def test_no_substring_matching_remains_in_lib_sh():
|
||||
"""D-6 (ISSUE-2): no `in tn` substring matching remains in lib.sh."""
|
||||
import re
|
||||
content = open(_LIB_SH, encoding="utf-8").read()
|
||||
assert re.search(r"\bin tn\b", content) is None
|
||||
assert re.search(r'agent"\) in tn', content) is None
|
||||
start = content.find("_resolve_herdr_pane_id()")
|
||||
assert start != -1
|
||||
body = content[start:content.find('cmd="${1:-}"', start)]
|
||||
assert r"^w[A-Za-z0-9]+:p[A-Za-z0-9]+$" in body
|
||||
|
||||
|
||||
def test_paste_buffer_branch_never_submits():
|
||||
"""D-7 (ISSUE-1): paste-buffer arm must not submit or invoke prompt/run."""
|
||||
content = open(_LIB_SH, encoding="utf-8").read()
|
||||
start = content.find(" paste-buffer)")
|
||||
assert start != -1
|
||||
end = content.find("\n delete-buffer)", start)
|
||||
assert end != -1
|
||||
body = content[start:end]
|
||||
for token in ("Enter", "C-m", "pane run", "agent prompt"):
|
||||
assert token not in body, token
|
||||
|
||||
|
||||
def test_fullscreen_modal_is_rejected_not_accepted():
|
||||
"""D-3: Yes, try it modal is a dialog; dismiss with Escape, never Enter."""
|
||||
"""D-3: fullscreen modal is dismissed with Escape, never Enter."""
|
||||
script = f"""
|
||||
_pane_capture() {{ printf '%s' '{_FULLSCREEN_MODAL}'; }}
|
||||
KEYS=/tmp/mam-fs-modal-keys.$$
|
||||
@@ -175,19 +259,36 @@ _sks_herdr() {{
|
||||
if [ "${{1:-}}" = "send-keys" ]; then echo "$*" >> "$KEYS"; fi
|
||||
return 0
|
||||
}}
|
||||
if _pane_dialog_open dummy; then echo "DIALOG_OPEN"; else echo "DIALOG_CLOSED"; fi
|
||||
handle_startup_dialogs dummy 1
|
||||
echo "KEYS_CONTENT=$(tr '\\n' '|' < "$KEYS")"
|
||||
rm -f "$KEYS"
|
||||
"""
|
||||
res = _run_lib_helpers(script)
|
||||
assert res.returncode == 0, res.stderr + res.stdout
|
||||
assert "DIALOG_OPEN" in res.stdout
|
||||
keys = res.stdout.split("KEYS_CONTENT=")[-1]
|
||||
assert "Escape" in keys
|
||||
assert "Enter" not in keys
|
||||
|
||||
|
||||
def test_prose_yes_try_it_is_not_a_dialog():
|
||||
"""N-1 / F-1: conversational 'Yes, try it' must not trip _pane_dialog_open."""
|
||||
script = r"""
|
||||
_pane_capture() { printf '%s' 'Sure — if the build fails again, Yes, try it with the --clean flag.'; }
|
||||
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_mam_dialog_tokens_exclude_yes_try_it():
|
||||
"""N-1 / F-1: token list must not contain Yes, try it; Escape branch stays."""
|
||||
content = open(_LIB_SH, encoding="utf-8").read()
|
||||
token_line = next(l for l in content.splitlines() if l.startswith("_MAM_DIALOG_TOKENS="))
|
||||
assert "Yes, try it" not in token_line
|
||||
assert "grep -q 'Yes, try it'" in content
|
||||
|
||||
|
||||
def test_wait_for_tui_ready_succeeds_on_fullscreen_tip():
|
||||
"""D-2c: tip + ready banner must not deadlock wait_for_tui_ready."""
|
||||
script = f"""
|
||||
|
||||
Reference in New Issue
Block a user