Files
multi-agent-mux/tests/test_b3_herdr_preflight.py
T

197 lines
8.9 KiB
Python

"""B-3 — the herdr pre-flight must probe for the real BINARY.
Two independent bypasses make the obvious one-liners useless once lib.sh is
sourced:
* ``command -v herdr`` matches the ``herdr()`` shell FUNCTION (lib.sh:498).
* ``type -P herdr`` matches ``$WORKSPACE_ROOT/.mam/shim/herdr``, because
``_init_herdr_isolation`` runs at source time
(lib.sh bottom) and prepends that dir to PATH.
So a host with no herdr installed at all passes pre-flight, and the failure is
deferred to a confusing runtime error from the shim.
"""
import os
import stat
import subprocess
import pytest
# A PATH with no herdr anywhere. Kept deliberately minimal so a herdr that
# happens to be installed on the developer's box cannot mask a regression.
BARE_PATH = "/usr/bin:/bin:/usr/sbin:/sbin"
def _bash(sandbox, snippet, path=BARE_PATH, extra_env=None):
env = dict(os.environ)
env["PATH"] = path
env["WORKSPACE_ROOT"] = str(sandbox)
if extra_env:
env.update(extra_env)
script = f'source "{sandbox}/.agents/skills/lib.sh" >/dev/null 2>&1\n{snippet}'
return subprocess.run(["bash", "-c", script], capture_output=True,
text=True, cwd=str(sandbox), env=env)
def _make_herdr(directory):
directory.mkdir(parents=True, exist_ok=True)
exe = directory / "herdr"
exe.write_text("#!/bin/sh\necho fake herdr\n")
exe.chmod(exe.stat().st_mode | stat.S_IEXEC | stat.S_IXGRP | stat.S_IXOTH)
return exe
# W-1 — document the two bypasses so a future refactor cannot silently
# "simplify" has_real_herdr back into one of them.
def test_b3_command_v_and_type_p_both_falsely_report_herdr(mam_sandbox):
res = _bash(mam_sandbox, """
command -v herdr >/dev/null && echo "command-v:MATCH" || echo "command-v:miss"
echo "type-t:$(type -t herdr)"
type -P herdr >/dev/null 2>&1 && echo "type-P:MATCH($(type -P herdr))" || echo "type-P:miss"
""")
assert "command-v:MATCH" in res.stdout, res.stdout
assert "type-t:function" in res.stdout, res.stdout
assert "type-P:MATCH" in res.stdout, res.stdout
assert "/.mam/shim/herdr" in res.stdout, \
f"type -P was expected to resolve the shim wrapper:\n{res.stdout}"
# W-2 — has_real_herdr must be FALSE when no binary is installed
def test_b3_has_real_herdr_false_without_binary(mam_sandbox):
res = _bash(mam_sandbox,
'if has_real_herdr; then echo RESULT:TRUE; else echo RESULT:FALSE; fi')
assert "RESULT:FALSE" in res.stdout, \
f"has_real_herdr passed with no herdr binary:\n{res.stdout}\n{res.stderr}"
# W-3 — ... and TRUE when one is, resolving past the shim
def test_b3_has_real_herdr_true_with_binary(mam_sandbox):
real = _make_herdr(mam_sandbox / "realbin")
res = _bash(mam_sandbox,
'if has_real_herdr; then echo RESULT:TRUE; else echo RESULT:FALSE; fi\n'
'echo "RESOLVED:$(_resolve_real_herdr_path)"',
path=f"{mam_sandbox / 'realbin'}:{BARE_PATH}")
assert "RESULT:TRUE" in res.stdout, f"{res.stdout}\n{res.stderr}"
assert f"RESOLVED:{real}" in res.stdout, \
f"resolved the shim instead of the real binary:\n{res.stdout}"
# W-4 — a herdr that only exists inside a wrapper/shim dir does not count
@pytest.mark.parametrize("dirname", ["my-shim", "multi-agent-herdr-shim"])
def test_b3_shim_directories_do_not_satisfy_preflight(mam_sandbox, dirname):
_make_herdr(mam_sandbox / dirname)
res = _bash(mam_sandbox,
'if has_real_herdr; then echo RESULT:TRUE; else echo RESULT:FALSE; fi',
path=f"{mam_sandbox / dirname}:{BARE_PATH}")
assert "RESULT:FALSE" in res.stdout, \
f"a herdr inside wrapper dir '{dirname}' was accepted:\n{res.stdout}"
# W-5 — create_session.sh pre-flight must actually FAIL without a binary.
# This is the defect B-3 names.
def test_b3_create_session_preflight_fails_without_herdr(mam_sandbox):
script = mam_sandbox / ".agents" / "skills" / "multi-agent-mux-create" / "scripts" / "create_session.sh"
env = dict(os.environ)
env["PATH"] = BARE_PATH
env["WORKSPACE_ROOT"] = str(mam_sandbox)
res = subprocess.run(
["bash", str(script), "--workspace", str(mam_sandbox),
"--agent", "claude", "--role", "worker"],
capture_output=True, text=True, cwd=str(mam_sandbox), env=env)
assert res.returncode != 0, \
f"pre-flight passed with no herdr installed:\n{res.stdout}\n{res.stderr}"
assert "herdr not installed" in (res.stdout + res.stderr), \
f"failed, but not on the herdr pre-flight:\n{res.stdout}\n{res.stderr}"
# W-6 — and must still pass the herdr gate when a binary IS present.
# (It may fail later on the agent-CLI gate; that is a different check.)
def test_b3_create_session_preflight_passes_herdr_gate_with_binary(mam_sandbox):
_make_herdr(mam_sandbox / "realbin")
script = mam_sandbox / ".agents" / "skills" / "multi-agent-mux-create" / "scripts" / "create_session.sh"
env = dict(os.environ)
env["PATH"] = f"{mam_sandbox / 'realbin'}:{BARE_PATH}"
env["WORKSPACE_ROOT"] = str(mam_sandbox)
res = subprocess.run(
["bash", str(script), "--workspace", str(mam_sandbox),
"--agent", "claude", "--role", "worker"],
capture_output=True, text=True, cwd=str(mam_sandbox), env=env)
assert "herdr not installed" not in (res.stdout + res.stderr), \
f"herdr gate rejected a real binary:\n{res.stdout}\n{res.stderr}"
# W-7 — the sharpest statement of B-3: with the agent CLI present and ONLY
# herdr missing, pre-flight must stop on the herdr gate. This is the
# test that is genuinely red before the fix without naming any new
# helper, so it measures the defect rather than the implementation.
def test_b3_preflight_stops_on_herdr_gate_when_only_herdr_is_missing(mam_sandbox):
agentbin = mam_sandbox / "agentbin"
agentbin.mkdir(parents=True, exist_ok=True)
for name in ("claude",):
exe = agentbin / name
exe.write_text('#!/bin/sh\necho \'{"loggedIn": true}\'\n')
exe.chmod(exe.stat().st_mode | stat.S_IEXEC | stat.S_IXGRP | stat.S_IXOTH)
script = mam_sandbox / ".agents" / "skills" / "multi-agent-mux-create" / "scripts" / "create_session.sh"
env = dict(os.environ)
env["PATH"] = f"{agentbin}:{BARE_PATH}" # claude yes, herdr no
env["WORKSPACE_ROOT"] = str(mam_sandbox)
res = subprocess.run(
["bash", str(script), "--workspace", str(mam_sandbox),
"--agent", "claude", "--role", "worker"],
capture_output=True, text=True, cwd=str(mam_sandbox), env=env, timeout=120)
combined = res.stdout + res.stderr
assert "herdr not installed" in combined, (
"pre-flight did not stop on the herdr gate even though herdr is absent.\n"
f"rc={res.returncode}\n--- stdout ---\n{res.stdout}\n--- stderr ---\n{res.stderr}")
# W-8 — symlink bypass: a link in an ORDINARY bin dir pointing at the shim.
# No directory-name pattern can reject this; only the resolved target
# reveals it.
def test_b3_symlink_into_shim_is_rejected(mam_sandbox):
_bash(mam_sandbox, "true") # materialise the shim
shim = mam_sandbox / ".mam" / "shim" / "herdr"
assert shim.exists(), "shim wrapper was not created by _init_herdr_isolation"
linkdir = mam_sandbox / "usrlocalbin"
linkdir.mkdir(parents=True, exist_ok=True)
(linkdir / "herdr").symlink_to(shim)
res = _bash(mam_sandbox,
'if has_real_herdr; then echo RESULT:TRUE; else echo RESULT:FALSE; fi',
path=f"{linkdir}:{BARE_PATH}")
assert "RESULT:FALSE" in res.stdout, (
"a symlink pointing into .mam/shim was accepted as a real herdr:\n"
f"{res.stdout}\n{res.stderr}")
# W-9 — ... but a symlink to a REAL binary must still be accepted, so the
# canonicalisation cannot be a blanket "reject all symlinks".
def test_b3_symlink_to_real_binary_is_accepted(mam_sandbox):
real = _make_herdr(mam_sandbox / "realbin")
linkdir = mam_sandbox / "linkbin"
linkdir.mkdir(parents=True, exist_ok=True)
(linkdir / "herdr").symlink_to(real)
res = _bash(mam_sandbox,
'if has_real_herdr; then echo RESULT:TRUE; else echo RESULT:FALSE; fi',
path=f"{linkdir}:{BARE_PATH}")
assert "RESULT:TRUE" in res.stdout, \
f"a symlink to a genuine herdr was rejected:\n{res.stdout}\n{res.stderr}"
# W-10 — a bare relative PATH entry '.mam/shim' (no leading '/' or './') must
# still be rejected. Requires normalising the dir with a leading slash.
def test_b3_bare_relative_shim_entry_is_rejected(mam_sandbox):
_bash(mam_sandbox, "true") # materialise the shim
res = _bash(mam_sandbox,
'if has_real_herdr; then echo RESULT:TRUE; else echo RESULT:FALSE; fi',
path=f".mam/shim:{BARE_PATH}")
assert "RESULT:FALSE" in res.stdout, (
"a bare relative '.mam/shim' PATH entry was accepted:\n"
f"{res.stdout}\n{res.stderr}")