test(issue-17): tests that source lib.sh no longer write into an inherited WORKSPACE_ROOT (v4.8.8) #53

Merged
Godopu merged 4 commits from test/issue17-workspace-root into main 2026-10-08 20:01:25 +00:00
14 changed files with 176 additions and 16 deletions
Showing only changes of commit 8b47cc76ee - Show all commits

No files matched your search

+15
View File
@@ -177,6 +177,21 @@ def _isolate_mam_state_dirs(_ambient_control_plane, tmp_path, monkeypatch):
monkeypatch.setenv("MAM_ENV_FILE", str(state / ".mam.env.absent"))
@pytest.fixture
def own_workspace_root(tmp_path, monkeypatch):
"""Issue #17: every `source lib.sh` rewrites $WORKSPACE_ROOT/.mam/shim/herdr,
and lib.sh derives this checkout when the variable is unset (a value naming
this checkout survives pytest_configure). A module whose shells inherit the
process environment opts in with `pytestmark = pytest.mark.usefixtures(
"own_workspace_root")`: they then write under this test's tmp_path instead.
mam_sandbox and a test's own WORKSPACE_ROOT run later and still win. Not
autouse: in-process code reads WORKSPACE_ROOT too (runner.py, mqtt_common)."""
root = tmp_path / ".mam-test-state" / "workspace"
root.mkdir(parents=True, exist_ok=True)
monkeypatch.setenv("WORKSPACE_ROOT", str(root))
return root
@pytest.fixture
def mam_sandbox(tmp_path, monkeypatch, request):
"""
@@ -8,6 +8,10 @@ import pytest
from lib_py.layout import compute_2xk_layout
# Issue #17: shells started here inherit a WORKSPACE_ROOT of this test's own
# (lib.sh would otherwise rewrite the checkout's .mam/shim/herdr).
pytestmark = pytest.mark.usefixtures("own_workspace_root")
def test_bug2_headless_layout_does_not_overflow():
"""Verify Bug 2: w=0, h=0 in headless mode does not trigger overflow."""
+4
View File
@@ -2,6 +2,10 @@ import os
import subprocess
import pytest
# Issue #17: shells started here inherit a WORKSPACE_ROOT of this test's own
# (lib.sh would otherwise rewrite the checkout's .mam/shim/herdr).
pytestmark = pytest.mark.usefixtures("own_workspace_root")
def test_send_keys_safe_b8_verification(tmp_path):
"""Verify that send_keys_safe for agy sessions goes through submission verification and does not return 0 blindly on failure."""
# Test script that mocks herdr commands and tests send_keys_safe behavior for agy
@@ -0,0 +1,92 @@
"""Issue #17 follow-up (v4.8.7 review): no test writes into the workspace it
inherited. Every `source lib.sh` rewrites $WORKSPACE_ROOT/.mam/shim/herdr, and
lib.sh derives the checkout when the variable is unset; conftest keeps a
WORKSPACE_ROOT that names this checkout. So the danger is the checkout itself -
the live workspace when the suite runs in the main checkout from an agent pane.
- W1: a copy of this checkout runs one test of every module whose shells used
to inherit the root, with WORKSPACE_ROOT naming the copy: the copy gets no
.mam at all (the modules now use own_workspace_root or pin a root).
- W2: test_orc_onboard's environment, taken at import, does not carry the
import-time WORKSPACE_ROOT into a later call.
"""
from __future__ import annotations
import os
import re
import shutil
import subprocess
import sys
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parent.parent
TESTS = REPO_ROOT / "tests"
# one test per module that sourced lib.sh (or ran the wrapper / run_loop.sh)
# with the inherited root; each wrote <checkout>/.mam/shim/herdr at 6b00b6e
NODES = [
"tests/test_version_consistency.py::test_mam_version_is_not_env_overridable",
"tests/test_b19_headless_reconcile_fixes.py::test_fullscreen_tip_is_not_a_blocking_dialog",
"tests/test_b19_headless_reconcile_fixes.py::test_bug4_slow_settling_pane_success",
"tests/test_b8_send_keys_verification.py::test_send_keys_safe_b8_verification",
"tests/test_issue6_fail_closed.py::test_t1_25_stop_session_cancels_redispatched_job_with_pinned_token",
"tests/test_loop_cli.py::test_help_lists_creator_not_target_agent",
"tests/test_issue5_phase3_loop.py::test_p3_02_run_loop_wrapper_help_lists_same_as_bash",
"tests/test_issue5_phase4_runtime.py::test_p4_03_freeze_bypass_by_default",
"tests/test_o3_scoped_guard.py::test_b13_no_freeze_switch_disables_reexec",
"tests/test_issue25_plan_reuse.py::test_p7b_the_legacy_shell_loop_refuses_the_option",
"tests/test_orc_onboard.py::test_o12_onboard_add_uuid",
"tests/test_workspace_scope.py::TestWorkspaceScope::test_derived_herdr_session_name",
"tests/test_issue17_item4_decisions.py::test_c_cmd_wait",
]
def _copy_checkout(dst: Path) -> None:
files = subprocess.run(["git", "-C", str(REPO_ROOT), "ls-files", "-co", "--exclude-standard"],
capture_output=True, text=True, check=True).stdout.splitlines()
for rel in files:
src = REPO_ROOT / rel
if src.is_file() and not rel.startswith(("nats-docker/", ".mam/")):
(dst / rel).parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(src, dst / rel)
def test_w1_no_test_writes_into_the_checkout_it_runs_in(tmp_path):
copy = tmp_path / "checkout"
_copy_checkout(copy)
assert not (copy / ".mam").exists()
env = {k: v for k, v in os.environ.items() if not k.startswith(("HERDR_", "MAM_", "MQTT_", "SKS_"))}
env.update(WORKSPACE_ROOT=str(copy), AGENT_PYTHON_BIN=sys.executable, PYTHONDONTWRITEBYTECODE="1")
env.pop("PYTHONPATH", None)
res = subprocess.run([sys.executable, "-m", "pytest", "-p", "no:cacheprovider", "-q", "-x",
f"--basetemp={tmp_path / 'bt'}", *NODES],
capture_output=True, text=True, cwd=str(copy), env=env, timeout=900)
assert res.returncode == 0, res.stdout[-3000:] + res.stderr[-2000:]
summary = res.stdout.strip().splitlines()[-1] # some nodes are parametrized: at least one test each
assert int(re.search(r"(\d+) passed", summary).group(1)) >= len(NODES) and "skipped" not in summary, summary
written = sorted(str(p.relative_to(copy)) for p in (copy / ".mam").rglob("*")) if (copy / ".mam").exists() else []
assert written == [], f"tests wrote into the checkout they ran in: {written}"
_ONBOARD_PROBE = r"""
import os, sys
from pathlib import Path
sys.path.insert(0, sys.argv[1])
import test_orc_onboard as m # imported while WORKSPACE_ROOT is the sentinel
ws = Path(sys.argv[2])
os.environ["WORKSPACE_ROOT"] = str(ws) # what mam_sandbox sets later
m.run_find_uuid(str(ws), "claude")
"""
def test_w2_onboard_does_not_carry_its_import_time_root(tmp_path):
sentinel, ws = tmp_path / "sentinel", tmp_path / "fixture-ws"
sentinel.mkdir()
ws.mkdir()
env = {k: v for k, v in os.environ.items() if not k.startswith(("HERDR_", "MAM_", "MQTT_", "SKS_"))}
env.update(WORKSPACE_ROOT=str(sentinel), HOME=str(tmp_path), PYTHONDONTWRITEBYTECODE="1")
res = subprocess.run([sys.executable, "-c", _ONBOARD_PROBE, str(TESTS), str(ws)], capture_output=True,
text=True, env=env, cwd=str(tmp_path), timeout=120)
assert res.returncode == 0, res.stderr[-2000:]
assert sorted(str(p.relative_to(sentinel)) for p in sentinel.rglob("*")) == [], "written into the import-time root"
assert (ws / ".mam" / "shim" / "herdr").is_file(), "the call's own root got lib.sh's shim"
+2 -1
View File
@@ -543,7 +543,8 @@ def _create(tmp: Path, ws: Path, env=None) -> subprocess.CompletedProcess:
def _mutate(tmp: Path, code: str) -> None:
res = subprocess.run(["bash", "-c", f"source {_skill(tmp, 'lib.sh')} && atomic_dump_yaml {tmp / '.mam' / 'agent-sessions.yaml'}"],
input=code, capture_output=True, text=True)
input=code, capture_output=True, text=True,
env=dict(os.environ, WORKSPACE_ROOT=str(tmp))) # Issue #17: not an inherited root
assert res.returncode == 0, res.stderr
+4
View File
@@ -24,6 +24,10 @@ from pathlib import Path
import pytest
# Issue #17: shells started here inherit a WORKSPACE_ROOT of this test's own
# (lib.sh would otherwise rewrite the checkout's .mam/shim/herdr).
pytestmark = pytest.mark.usefixtures("own_workspace_root")
REPO_ROOT = Path(__file__).resolve().parent.parent
RUNNER_PY = REPO_ROOT / ".agents" / "skills" / "lib_py" / "loop" / "runner.py"
RUN_LOOP_SH = REPO_ROOT / ".agents" / "skills" / "multi-agent-mux-loop" / "scripts" / "run_loop.sh"
+4
View File
@@ -29,6 +29,10 @@ if str(SKILLS) not in sys.path:
from lib_py.loop import parser, checkpoint, runner
# Issue #17: shells started here inherit a WORKSPACE_ROOT of this test's own
# (lib.sh would otherwise rewrite the checkout's .mam/shim/herdr).
pytestmark = pytest.mark.usefixtures("own_workspace_root")
def test_p3_01_strict_verdict_ignores_fenced_code():
"""P3-01: A verdict token inside markdown code fences must NOT be matched."""
+4
View File
@@ -24,6 +24,10 @@ if str(SKILLS) not in sys.path:
from lib_py.loop import eager, parser, checkpoint, runner
# Issue #17: shells started here inherit a WORKSPACE_ROOT of this test's own
# (lib.sh would otherwise rewrite the checkout's .mam/shim/herdr).
pytestmark = pytest.mark.usefixtures("own_workspace_root")
def test_p4_01_eager_import_completeness_and_bytecode_inhibition(monkeypatch):
"""P4-01: Verify all core framework modules are eagerly loaded and bytecode writes disabled."""
+4
View File
@@ -18,6 +18,10 @@ import pytest
from _local_mqtt import local_broker_env
# Issue #17: shells started here inherit a WORKSPACE_ROOT of this test's own
# (lib.sh would otherwise rewrite the checkout's .mam/shim/herdr).
pytestmark = pytest.mark.usefixtures("own_workspace_root")
REPO_ROOT = Path(__file__).resolve().parent.parent
SKILLS = REPO_ROOT / ".agents" / "skills"
WRAPPER = SKILLS / "multi-agent-mux-delegate-job" / "multi-agent-mux-delegate-job"
+5
View File
@@ -4,6 +4,11 @@
import os
import subprocess
from pathlib import Path
import pytest
# Issue #17: shells started here inherit a WORKSPACE_ROOT of this test's own
# (lib.sh would otherwise rewrite the checkout's .mam/shim/herdr).
pytestmark = pytest.mark.usefixtures("own_workspace_root")
REPO_ROOT = Path(__file__).resolve().parent.parent
RUN_LOOP_SH = REPO_ROOT / ".agents" / "skills" / "multi-agent-mux-loop" / "scripts" / "run_loop.sh"
+4
View File
@@ -24,6 +24,10 @@ import time
from pathlib import Path
import pytest
# Issue #17: shells started here inherit a WORKSPACE_ROOT of this test's own
# (lib.sh would otherwise rewrite the checkout's .mam/shim/herdr).
pytestmark = pytest.mark.usefixtures("own_workspace_root")
REPO_ROOT = Path(__file__).resolve().parent.parent
GUARD_SCRIPT = REPO_ROOT / ".agents" / "hooks" / "loop_delegation_guard.sh"
+26 -14
View File
@@ -7,19 +7,31 @@ import shutil
import pytest
from pathlib import Path
# Issue #17: shells started here inherit a WORKSPACE_ROOT of this test's own
# (lib.sh would otherwise rewrite the checkout's .mam/shim/herdr).
pytestmark = pytest.mark.usefixtures("own_workspace_root")
REPO_ROOT = Path(__file__).parent.parent
LIB_SH = REPO_ROOT / ".agents" / "skills" / "lib.sh"
ONBOARD_SH = REPO_ROOT / ".agents" / "skills" / "multi-agent-mux-orc-onboard" / "scripts" / "orc_onboard.sh"
RECONCILE_SH = REPO_ROOT / ".agents" / "skills" / "multi-agent-mux-monitor" / "scripts" / "reconcile.sh"
SCRUBBED_ENV = dict(os.environ)
for k in ["CLAUDE_CODE_SESSION_ID", "ANTIGRAVITY_CONVERSATION_ID", "HERMES_SESSION_ID", "MAM_ORCHESTRATOR_UUIDS"]:
for k in ["CLAUDE_CODE_SESSION_ID", "ANTIGRAVITY_CONVERSATION_ID", "HERMES_SESSION_ID", "MAM_ORCHESTRATOR_UUIDS",
"WORKSPACE_ROOT"]:
SCRUBBED_ENV.pop(k, None)
SCRUBBED_ENV["PYTHONPATH"] = f"{LIB_SH.parent}:{SCRUBBED_ENV.get('PYTHONPATH', '')}"
def _scrubbed_env(root=None) -> dict:
"""SCRUBBED_ENV (taken at import) with the WORKSPACE_ROOT of now: `root`,
else this test's (own_workspace_root / mam_sandbox) - never the one the
module was imported under (Issue #17: lib.sh rewrites <it>/.mam/shim/herdr)."""
return dict(SCRUBBED_ENV, WORKSPACE_ROOT=str(root or os.environ["WORKSPACE_ROOT"]))
def run_onboard(args, env=None, cwd=None):
run_env = dict(SCRUBBED_ENV)
run_env = _scrubbed_env(cwd)
if cwd:
run_env["AGENT_SESSIONS_YAML"] = f"{cwd}/.mam/agent-sessions.yaml"
run_env["YAML_PATH"] = f"{cwd}/.mam/agent-sessions.yaml"
@@ -31,7 +43,7 @@ def run_onboard(args, env=None, cwd=None):
def run_find_uuid(ws, agent, target="", env=None):
run_env = dict(SCRUBBED_ENV)
run_env = _scrubbed_env(ws)
run_env["AGENT_SESSIONS_YAML"] = f"{ws}/.mam/agent-sessions.yaml"
run_env["YAML_PATH"] = f"{ws}/.mam/agent-sessions.yaml"
run_env["HOME_DIR"] = ws
@@ -44,7 +56,7 @@ def run_find_uuid(ws, agent, target="", env=None):
def run_verify_uuid(ws, agent, uuid, row=None, mode="discover", env=None):
run_env = dict(SCRUBBED_ENV)
run_env = _scrubbed_env(ws)
run_env["AGENT_SESSIONS_YAML"] = f"{ws}/.mam/agent-sessions.yaml"
run_env["YAML_PATH"] = f"{ws}/.mam/agent-sessions.yaml"
run_env["HOME_DIR"] = ws
@@ -62,7 +74,7 @@ def dump_state(mam_sandbox, state):
env = {"AGENT_SESSIONS_YAML": yaml_path, "YAML_PATH": yaml_path, "HOME_DIR": str(mam_sandbox)}
state_str = json.dumps(state)
cmd = f"source '{LIB_SH}' && atomic_dump_yaml '{yaml_path}' <<'PYEOF'\nd.update({state_str})\nPYEOF"
subprocess.run(["bash", "-c", cmd], env={**SCRUBBED_ENV, **env}, check=True)
subprocess.run(["bash", "-c", cmd], env={**_scrubbed_env(mam_sandbox), **env}, check=True)
return env
@@ -307,7 +319,7 @@ def test_o18_autodetect_via_env(mam_sandbox):
}
# Run inside subshell where command line matches claude
cmd = f"export CLAUDE_CODE_SESSION_ID='{orc_uuid}' && exec -a claude bash '{ONBOARD_SH}'"
res = subprocess.run(["bash", "-c", cmd], capture_output=True, text=True, env={**SCRUBBED_ENV, **env}, cwd=str(mam_sandbox))
res = subprocess.run(["bash", "-c", cmd], capture_output=True, text=True, env={**_scrubbed_env(), **env}, cwd=str(mam_sandbox))
assert res.returncode == 0
res_list = run_onboard(["--list"], env=env, cwd=str(mam_sandbox))
@@ -327,7 +339,7 @@ def test_o20_validate_fails_on_non_list_orchestrator_uuids(mam_sandbox):
env = {"AGENT_SESSIONS_YAML": str(mam_sandbox / ".mam" / "agent-sessions.yaml"), "YAML_PATH": str(mam_sandbox / ".mam" / "agent-sessions.yaml"), "HOME_DIR": str(mam_sandbox)}
state_str = json.dumps(state)
cmd = f"source '{LIB_SH}' && atomic_dump_yaml '{env['YAML_PATH']}' <<'PYEOF'\nd.update({state_str})\nPYEOF"
res = subprocess.run(["bash", "-c", cmd], env={**SCRUBBED_ENV, **env}, capture_output=True, text=True)
res = subprocess.run(["bash", "-c", cmd], env={**_scrubbed_env(), **env}, capture_output=True, text=True)
assert res.returncode != 0
assert "VALIDATE" in res.stderr or "VALIDATE" in res.stdout
@@ -339,7 +351,7 @@ def test_o21_validate_fails_on_duplicate_orchestrator_uuids(mam_sandbox):
env = {"AGENT_SESSIONS_YAML": str(mam_sandbox / ".mam" / "agent-sessions.yaml"), "YAML_PATH": str(mam_sandbox / ".mam" / "agent-sessions.yaml"), "HOME_DIR": str(mam_sandbox)}
state_str = json.dumps(state)
cmd = f"source '{LIB_SH}' && atomic_dump_yaml '{env['YAML_PATH']}' <<'PYEOF'\nd.update({state_str})\nPYEOF"
res = subprocess.run(["bash", "-c", cmd], env={**SCRUBBED_ENV, **env}, capture_output=True, text=True)
res = subprocess.run(["bash", "-c", cmd], env={**_scrubbed_env(), **env}, capture_output=True, text=True)
assert res.returncode != 0
@@ -349,7 +361,7 @@ def test_o22_validate_fails_on_empty_string_orchestrator_uuid(mam_sandbox):
env = {"AGENT_SESSIONS_YAML": str(mam_sandbox / ".mam" / "agent-sessions.yaml"), "YAML_PATH": str(mam_sandbox / ".mam" / "agent-sessions.yaml"), "HOME_DIR": str(mam_sandbox)}
state_str = json.dumps(state)
cmd = f"source '{LIB_SH}' && atomic_dump_yaml '{env['YAML_PATH']}' <<'PYEOF'\nd.update({state_str})\nPYEOF"
res = subprocess.run(["bash", "-c", cmd], env={**SCRUBBED_ENV, **env}, capture_output=True, text=True)
res = subprocess.run(["bash", "-c", cmd], env={**_scrubbed_env(), **env}, capture_output=True, text=True)
assert res.returncode != 0
@@ -367,7 +379,7 @@ if h1 == h2:
sys.exit(1)
PYEOF
"""
res = subprocess.run(["bash", "-c", cmd], env={**SCRUBBED_ENV, "AGENT_SESSIONS_YAML": yaml_path, "YAML_PATH": yaml_path}, capture_output=True, text=True)
res = subprocess.run(["bash", "-c", cmd], env={**_scrubbed_env(), "AGENT_SESSIONS_YAML": yaml_path, "YAML_PATH": yaml_path}, capture_output=True, text=True)
assert res.returncode == 0
@@ -438,7 +450,7 @@ def test_o27_ignore_different_family_env_variable(mam_sandbox):
"ANTIGRAVITY_CONVERSATION_ID": "72d2d251-5a06-486b-92e5-7e46a7a80d2e"
}
cmd = f"export ANTIGRAVITY_CONVERSATION_ID='72d2d251-5a06-486b-92e5-7e46a7a80d2e' && exec -a claude bash '{ONBOARD_SH}'"
res = subprocess.run(["bash", "-c", cmd], capture_output=True, text=True, env={**SCRUBBED_ENV, **env}, cwd=str(mam_sandbox))
res = subprocess.run(["bash", "-c", cmd], capture_output=True, text=True, env={**_scrubbed_env(), **env}, cwd=str(mam_sandbox))
assert res.returncode == 3
@@ -451,7 +463,7 @@ def test_o28_use_family_matched_env_variable(mam_sandbox):
"ANTIGRAVITY_CONVERSATION_ID": orc_uuid
}
cmd = f"export ANTIGRAVITY_CONVERSATION_ID='{orc_uuid}' && exec -a agy bash '{ONBOARD_SH}'"
res = subprocess.run(["bash", "-c", cmd], capture_output=True, text=True, env={**SCRUBBED_ENV, **env}, cwd=str(mam_sandbox))
res = subprocess.run(["bash", "-c", cmd], capture_output=True, text=True, env={**_scrubbed_env(), **env}, cwd=str(mam_sandbox))
assert res.returncode == 0
res_list = run_onboard(["--list"], env=env, cwd=str(mam_sandbox))
@@ -467,7 +479,7 @@ def test_o29_fresh_claude_session_detection(mam_sandbox):
"CLAUDE_CODE_SESSION_ID": orc_uuid
}
cmd = f"export CLAUDE_CODE_SESSION_ID='{orc_uuid}' && exec -a claude bash '{ONBOARD_SH}'"
res = subprocess.run(["bash", "-c", cmd], capture_output=True, text=True, env={**SCRUBBED_ENV, **env}, cwd=str(mam_sandbox))
res = subprocess.run(["bash", "-c", cmd], capture_output=True, text=True, env={**_scrubbed_env(), **env}, cwd=str(mam_sandbox))
assert res.returncode == 0
res_list = run_onboard(["--list"], env=env, cwd=str(mam_sandbox))
@@ -484,7 +496,7 @@ def test_o30_argv_precedes_env(mam_sandbox):
"CLAUDE_CODE_SESSION_ID": env_uuid
}
cmd = f"export CLAUDE_CODE_SESSION_ID='{env_uuid}' && exec -a claude bash '{ONBOARD_SH}' --uuid {argv_uuid}"
res = subprocess.run(["bash", "-c", cmd], capture_output=True, text=True, env={**SCRUBBED_ENV, **env}, cwd=str(mam_sandbox))
res = subprocess.run(["bash", "-c", cmd], capture_output=True, text=True, env={**_scrubbed_env(), **env}, cwd=str(mam_sandbox))
assert res.returncode == 0
res_list = run_onboard(["--list"], env=env, cwd=str(mam_sandbox))
+5
View File
@@ -6,6 +6,11 @@ import re
import subprocess
from pathlib import Path
import yaml
import pytest
# Issue #17: shells started here inherit a WORKSPACE_ROOT of this test's own
# (lib.sh would otherwise rewrite the checkout's .mam/shim/herdr).
pytestmark = pytest.mark.usefixtures("own_workspace_root")
REPO_ROOT = Path(__file__).resolve().parent.parent
LIB_SH = REPO_ROOT / ".agents" / "skills" / "lib.sh"
+3 -1
View File
@@ -27,7 +27,9 @@ class TestWorkspaceScope(unittest.TestCase):
cmd = f"source {lib_sh} && env -u HERDR_SESSION_NAME -u HERDR_SERVER_NAME bash -c 'source {lib_sh} && resolve_herdr_session \"test-session\" \"{self.ws_dir}\"'"
# `source` is a bash builtin; on hosts where /bin/sh is dash `shell=True`
# fails with "source: not found" (Issue #28), so run the string under bash.
res = subprocess.run(["bash", "-c", cmd], capture_output=True, text=True)
# Issue #17: lib.sh rewrites $WORKSPACE_ROOT/.mam/shim/herdr - this test's own, not an inherited one
res = subprocess.run(["bash", "-c", cmd], capture_output=True, text=True,
env=dict(os.environ, WORKSPACE_ROOT=self.ws_dir))
self.assertEqual(res.returncode, 0, f"Command failed: {res.stderr}")
parent = os.path.basename(os.path.dirname(os.path.abspath(self.ws_dir))).lower().replace('_', '-')
work = os.path.basename(os.path.abspath(self.ws_dir)).lower().replace('_', '-')