fix(tests): tests no longer reach a real herdr server - a *-shim ancestor hid the mock, the sandbox kept the pane's socket
A suite run from an agent pane with --basetemp .../pytest-pr56-shim left seven workspaces on that pane's real server (multi-agent-mux): - the real-herdr resolvers (lib.sh _is_shim_path and the generated shim) skipped every PATH directory with "-shim" anywhere in its path, so the basetemp hid the mock's bin/ and the real herdr ran; only a directory that is itself *-shim is a wrapper now; - mam_sandbox kept HERDR_SOCKET_PATH (and the pane/tab ids), which the real herdr follows over the sandbox HOME; it drops them now. tests/test_herdr_real_server_leak.py: L1 (ancestor *-shim, both resolvers) and L2 (pane routing dropped) fail before the fix; L1b keeps a *-shim wrapper directory excluded. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VCgu9Fa9iT7TbJUSzdq4Ff
This commit is contained in:
1 parent
8b0b4a07dc
commit
525df2bb58
3 files changed
+102
-3
No files matched your search
@@ -112,9 +112,15 @@ _canonical_file() {
|
|||||||
|
|
||||||
_is_shim_path() {
|
_is_shim_path() {
|
||||||
case "/$1/" in
|
case "/$1/" in
|
||||||
*"/.mam/shim/"*|*-shim/*|*"$_HERDR_SHIM_DIR_PATTERN"*|*"$_HERDR_SKILLS_BIN_PATTERN"/*)
|
*"/.mam/shim/"*|*"$_HERDR_SHIM_DIR_PATTERN"*|*"$_HERDR_SKILLS_BIN_PATTERN"/*)
|
||||||
return 0 ;;
|
return 0 ;;
|
||||||
esac
|
esac
|
||||||
|
# A wrapper directory named *-shim, the directory itself only: an ancestor
|
||||||
|
# such as a pytest basetemp "pytest-x-shim" above a mock's bin/ made the
|
||||||
|
# resolver skip the mock and run the real herdr.
|
||||||
|
case "${1%/}" in
|
||||||
|
*-shim) return 0 ;;
|
||||||
|
esac
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,7 +133,7 @@ _resolve_real_herdr_path() {
|
|||||||
[ -x "$dir/herdr" ] || continue
|
[ -x "$dir/herdr" ] || continue
|
||||||
cand="$(_canonical_file "$dir/herdr" 2>/dev/null)" || cand="$dir/herdr"
|
cand="$(_canonical_file "$dir/herdr" 2>/dev/null)" || cand="$dir/herdr"
|
||||||
[ -n "$cand" ] || cand="$dir/herdr"
|
[ -n "$cand" ] || cand="$dir/herdr"
|
||||||
_is_shim_path "$cand" && continue
|
_is_shim_path "$(dirname "$cand")" && continue
|
||||||
real_path="$dir/herdr"
|
real_path="$dir/herdr"
|
||||||
break
|
break
|
||||||
done
|
done
|
||||||
@@ -167,7 +173,7 @@ _resolve_real_herdr() {
|
|||||||
local dir save_ifs="$IFS" real_path=""
|
local dir save_ifs="$IFS" real_path=""
|
||||||
IFS=:
|
IFS=:
|
||||||
for dir in $PATH; do
|
for dir in $PATH; do
|
||||||
if [[ "$dir" != *".mam/shim"* ]] && [[ "$dir" != *"-shim"* ]] && [ -x "$dir/herdr" ]; then
|
if [[ "$dir" != *".mam/shim"* ]] && [[ "${dir%/}" != *-shim ]] && [ -x "$dir/herdr" ]; then
|
||||||
real_path="$dir/herdr"
|
real_path="$dir/herdr"
|
||||||
break
|
break
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -290,6 +290,12 @@ def mam_sandbox(tmp_path, monkeypatch, request):
|
|||||||
monkeypatch.delenv("HERDR_SESSION_NAME", raising=False)
|
monkeypatch.delenv("HERDR_SESSION_NAME", raising=False)
|
||||||
monkeypatch.delenv("HERDR_SERVER_NAME", raising=False)
|
monkeypatch.delenv("HERDR_SERVER_NAME", raising=False)
|
||||||
monkeypatch.delenv("HERDR_WORKSPACE_ID", raising=False)
|
monkeypatch.delenv("HERDR_WORKSPACE_ID", raising=False)
|
||||||
|
# A herdr pane exports its server's socket (and pane/tab ids). The real
|
||||||
|
# herdr follows HERDR_SOCKET_PATH over the sandbox HOME, so a call that
|
||||||
|
# missed the mock reached the caller's real server and opened panes there.
|
||||||
|
monkeypatch.delenv("HERDR_SOCKET_PATH", raising=False)
|
||||||
|
monkeypatch.delenv("HERDR_PANE_ID", raising=False)
|
||||||
|
monkeypatch.delenv("HERDR_TAB_ID", raising=False)
|
||||||
# Issue #45: the shim bootstrap writes a herdr server config next to the
|
# Issue #45: the shim bootstrap writes a herdr server config next to the
|
||||||
# user's; with these unset that is $HOME/.config/herdr, inside the sandbox.
|
# user's; with these unset that is $HOME/.config/herdr, inside the sandbox.
|
||||||
# Every pane of a MAM server inherits HERDR_CONFIG_PATH.
|
# Every pane of a MAM server inherits HERDR_CONFIG_PATH.
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
"""Tests must not reach a real herdr server (found 2026-10-12).
|
||||||
|
|
||||||
|
A suite run from an agent pane with --basetemp .../pytest-pr56-shim left seven
|
||||||
|
workspaces on that pane's real server. Two causes, one test each:
|
||||||
|
- L1: the real-herdr resolvers skipped every PATH directory with "-shim"
|
||||||
|
anywhere in its path, so the basetemp hid the mock's bin/ and the real
|
||||||
|
herdr ran;
|
||||||
|
- L2: the sandbox kept the pane's HERDR_SOCKET_PATH, which the real herdr
|
||||||
|
follows over the sandbox HOME.
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
DECOY = "#!/bin/sh\necho \"$@\" >> \"$DECOY_LOG\"\necho '{}'\n"
|
||||||
|
|
||||||
|
|
||||||
|
def test_l1_ancestor_named_shim_does_not_hide_the_mock(mam_sandbox, mock_herdr):
|
||||||
|
tmp = mam_sandbox
|
||||||
|
# The mock's bin/ under an ancestor named *-shim, as under that basetemp.
|
||||||
|
mock_bin = tmp / "pytest-x-shim" / "case0" / "bin"
|
||||||
|
mock_bin.mkdir(parents=True)
|
||||||
|
(mock_bin / "herdr").symlink_to(tmp / "bin" / "herdr")
|
||||||
|
# A decoy next in PATH stands in for the real herdr: with the old match it
|
||||||
|
# is what ran. The real herdr is never reached either way.
|
||||||
|
decoy_bin = tmp / "decoy" / "bin"
|
||||||
|
decoy_bin.mkdir(parents=True)
|
||||||
|
(decoy_bin / "herdr").write_text(DECOY)
|
||||||
|
(decoy_bin / "herdr").chmod(0o755)
|
||||||
|
decoy_log = tmp / "decoy.log"
|
||||||
|
env = dict(os.environ, PATH=f"{mock_bin}:{decoy_bin}:{os.environ['PATH']}",
|
||||||
|
DECOY_LOG=str(decoy_log))
|
||||||
|
lib = tmp / ".agents" / "skills" / "lib.sh"
|
||||||
|
script = f"""
|
||||||
|
source "{lib}"
|
||||||
|
_init_herdr_isolation
|
||||||
|
_resolve_real_herdr_path
|
||||||
|
"$WORKSPACE_ROOT/.mam/shim/herdr" workspace list >/dev/null
|
||||||
|
"""
|
||||||
|
res = subprocess.run(["bash", "-c", script], capture_output=True, text=True,
|
||||||
|
cwd=str(tmp), env=env)
|
||||||
|
assert res.returncode == 0, res.stderr
|
||||||
|
# lib.sh's resolver picks the mock's directory, not the decoy.
|
||||||
|
assert res.stdout.strip().splitlines()[0] == f"{mock_bin}/herdr", res.stdout
|
||||||
|
# The generated shim ran the mock too.
|
||||||
|
assert not decoy_log.exists(), decoy_log.read_text()
|
||||||
|
|
||||||
|
|
||||||
|
def test_l1_a_directory_named_shim_is_still_skipped(mam_sandbox, mock_herdr):
|
||||||
|
"""A wrapper directory that is itself *-shim stays excluded."""
|
||||||
|
tmp = mam_sandbox
|
||||||
|
wrapper = tmp / "herdr-shim"
|
||||||
|
wrapper.mkdir()
|
||||||
|
(wrapper / "herdr").write_text(DECOY)
|
||||||
|
(wrapper / "herdr").chmod(0o755)
|
||||||
|
decoy_log = tmp / "decoy.log"
|
||||||
|
env = dict(os.environ, PATH=f"{wrapper}:{os.environ['PATH']}", DECOY_LOG=str(decoy_log))
|
||||||
|
lib = tmp / ".agents" / "skills" / "lib.sh"
|
||||||
|
script = f"""
|
||||||
|
source "{lib}"
|
||||||
|
_init_herdr_isolation
|
||||||
|
_resolve_real_herdr_path
|
||||||
|
"$WORKSPACE_ROOT/.mam/shim/herdr" workspace list >/dev/null
|
||||||
|
"""
|
||||||
|
res = subprocess.run(["bash", "-c", script], capture_output=True, text=True,
|
||||||
|
cwd=str(tmp), env=env)
|
||||||
|
assert res.returncode == 0, res.stderr
|
||||||
|
assert res.stdout.strip().splitlines()[0] == f"{tmp}/bin/herdr", res.stdout
|
||||||
|
assert not decoy_log.exists(), decoy_log.read_text()
|
||||||
|
|
||||||
|
|
||||||
|
PANE_VARS = {"HERDR_SOCKET_PATH": "/nonexistent/sessions/real/herdr.sock",
|
||||||
|
"HERDR_PANE_ID": "w9:p1", "HERDR_TAB_ID": "w9:t1",
|
||||||
|
"HERDR_WORKSPACE_ID": "w9", "HERDR_SESSION_NAME": "real"}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def _agent_pane_env(monkeypatch):
|
||||||
|
"""What an agent pane on a named server carries; set before mam_sandbox."""
|
||||||
|
for k, v in PANE_VARS.items():
|
||||||
|
monkeypatch.setenv(k, v)
|
||||||
|
|
||||||
|
|
||||||
|
def test_l2_sandbox_drops_the_pane_routing(_agent_pane_env, mam_sandbox):
|
||||||
|
left = sorted(k for k in PANE_VARS if k in os.environ)
|
||||||
|
assert left == [], left
|
||||||
Reference in new issue
Block a user