diff --git a/tests/conftest.py b/tests/conftest.py index c4f47e3..4c75631 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -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): """ diff --git a/tests/test_b19_headless_reconcile_fixes.py b/tests/test_b19_headless_reconcile_fixes.py index 36c56c1..d916209 100644 --- a/tests/test_b19_headless_reconcile_fixes.py +++ b/tests/test_b19_headless_reconcile_fixes.py @@ -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.""" diff --git a/tests/test_b8_send_keys_verification.py b/tests/test_b8_send_keys_verification.py index 43af495..1c9afe2 100644 --- a/tests/test_b8_send_keys_verification.py +++ b/tests/test_b8_send_keys_verification.py @@ -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 diff --git a/tests/test_issue17_workspace_root_isolation.py b/tests/test_issue17_workspace_root_isolation.py new file mode 100644 index 0000000..e5f1d93 --- /dev/null +++ b/tests/test_issue17_workspace_root_isolation.py @@ -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 /.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" diff --git a/tests/test_issue21_worktree_prereqs.py b/tests/test_issue21_worktree_prereqs.py index b091832..c81e975 100644 --- a/tests/test_issue21_worktree_prereqs.py +++ b/tests/test_issue21_worktree_prereqs.py @@ -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 diff --git a/tests/test_issue25_plan_reuse.py b/tests/test_issue25_plan_reuse.py index 5296967..c950185 100644 --- a/tests/test_issue25_plan_reuse.py +++ b/tests/test_issue25_plan_reuse.py @@ -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" diff --git a/tests/test_issue5_phase3_loop.py b/tests/test_issue5_phase3_loop.py index a12549d..12ade47 100644 --- a/tests/test_issue5_phase3_loop.py +++ b/tests/test_issue5_phase3_loop.py @@ -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.""" diff --git a/tests/test_issue5_phase4_runtime.py b/tests/test_issue5_phase4_runtime.py index 6b114d7..5d61b88 100644 --- a/tests/test_issue5_phase4_runtime.py +++ b/tests/test_issue5_phase4_runtime.py @@ -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.""" diff --git a/tests/test_issue6_fail_closed.py b/tests/test_issue6_fail_closed.py index 308b51d..dab0456 100644 --- a/tests/test_issue6_fail_closed.py +++ b/tests/test_issue6_fail_closed.py @@ -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" diff --git a/tests/test_loop_cli.py b/tests/test_loop_cli.py index 41225b6..a45bec5 100644 --- a/tests/test_loop_cli.py +++ b/tests/test_loop_cli.py @@ -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" diff --git a/tests/test_o3_scoped_guard.py b/tests/test_o3_scoped_guard.py index 1a0b4d0..8618351 100644 --- a/tests/test_o3_scoped_guard.py +++ b/tests/test_o3_scoped_guard.py @@ -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" diff --git a/tests/test_orc_onboard.py b/tests/test_orc_onboard.py index ada9d33..3d5cbd9 100644 --- a/tests/test_orc_onboard.py +++ b/tests/test_orc_onboard.py @@ -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 /.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)) diff --git a/tests/test_version_consistency.py b/tests/test_version_consistency.py index f155224..00332a8 100644 --- a/tests/test_version_consistency.py +++ b/tests/test_version_consistency.py @@ -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" diff --git a/tests/test_workspace_scope.py b/tests/test_workspace_scope.py index 6688ccf..2084861 100644 --- a/tests/test_workspace_scope.py +++ b/tests/test_workspace_scope.py @@ -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('_', '-')