test(issue-17): every test that sources lib.sh does it under its own WORKSPACE_ROOT (review grok, codex)
d198dd5fixed one helper; the same defect was in eleven more modules (reviews grok and codex R1/R2): their shells inherited the process environment, so each `source lib.sh` rewrote <WORKSPACE_ROOT>/.mam/shim/herdr - the checkout's own when the variable is unset or names this checkout (which pytest_configure keeps), another workspace's under --noconftest. - conftest: own_workspace_root, an opt-in fixture setting WORKSPACE_ROOT to <tmp_path>/.mam-test-state/workspace. Not autouse: runner.py and mqtt_common read WORKSPACE_ROOT in-process, and some tests unset it on purpose. mam_sandbox and explicit envs run later and still win. - pytestmark usefixtures("own_workspace_root") in test_version_consistency, test_b19_headless_reconcile_fixes, test_b8_send_keys_verification, test_issue6_fail_closed, test_loop_cli, test_issue5_phase3_loop, test_issue5_phase4_runtime, test_o3_scoped_guard, test_issue25_plan_reuse, test_orc_onboard (their shells inherit os.environ, many with no env at all). - test_orc_onboard (R2): SCRUBBED_ENV is taken at import and no longer carries WORKSPACE_ROOT; every call builds _scrubbed_env(root) with the root of the call (ws / cwd / mam_sandbox, else this test's). - test_workspace_scope (unittest) and test_issue21_worktree_prereqs._mutate pin the root they already create. Tests (test_issue17_workspace_root_isolation.py): W1 copies the checkout and runs one test of each of those modules there with WORKSPACE_ROOT naming the copy - the copy gets no .mam; W2 imports test_orc_onboard under a sentinel root, then calls it under another - the sentinel stays empty. Atd198dd5both fail (the copy gets .mam/shim/herdr; the sentinel gets it). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HCGpJXhSZasJLU38ksDghe
This commit is contained in:
1 parent
d198dd50aa
commit
8b47cc76ee
14 files changed
+176
-16
No files matched your search
@@ -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"
|
||||
Reference in new issue
Block a user