test(issue-55): tests no longer write into the user's real agent homes

An agent pane inherits HOME_DIR, CLAUDE_PROJECT_DIR and LOCAL_BIN from
lib.sh's exports (and the adapters read CODEX_HOME). DiscoveryContext
prefers CLAUDE_PROJECT_DIR over the home_dir a test passes and every
adapter path reads HOME_DIR before HOME, so a suite run by an agent
wrote fake conversation folders into the user's real ~/.claude/projects
(test_adapter_discover, 40 folders on 2026-10-10).

- conftest._isolate_agent_homes (autouse): drops the four pane variables
  for every test, so HOME - which the suite's tests set for an isolated
  home - governs, as in a plain shell. Dropped rather than pointed into
  tmp_path: a fixture-set HOME_DIR would shadow the 11 adapter-contract
  tests that set HOME alone, the same way the pane's value did.
- conftest.agent_home_leak_guard (session autouse) + tests/_agent_home_guard.py:
  snapshots the entry names of the real claude/agy/codex/grok/opencode/
  hermes conversation directories (as the ambient environment names them,
  recorded in pytest_configure) and fails on new ones; read-only. A claude
  folder whose name decodes (against the file system) to a real directory
  outside the temp roots is an operating agent's and is not counted.
- tests/test_issue55_agent_homes.py: the dropped-variable list is pinned
  against the env reads of lib_py/agents, lib_py/paths.py and lib.sh;
  guard unit tests; the issue's reproduction as a subprocess regression
  (on base 42e5173 it leaves <dir>/-…-test-adapter-discover0-ws/u-c1.jsonl).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
GodopuandClaude Fable 5.1 committed 2026-10-10 14:29:36 +00:00
1 parent 42e51735ba
commit 40293841e7
3 files changed
+381

No files matched your search

+53
View File
@@ -130,15 +130,24 @@ _TESTS_DIR = str(Path(__file__).resolve().parent)
if _TESTS_DIR not in sys.path:
sys.path.insert(0, _TESTS_DIR)
import _mam_state_guard # noqa: E402
import _agent_home_guard # noqa: E402
_BINDING_VARS = ("WORKSPACE_ROOT", "AGENT_SESSIONS_YAML", "MAM_JOBS_DIR", "DELEGATE_JOB_LOGS_DIR")
_RECORDED_BINDING = {}
# Issue #55: the agent-home variables as the pane had them, recorded before
# any fixture points them into a tmp_path, so the session guard knows which
# real directories to watch. They are not dropped: every test gets its own
# values from _isolate_agent_homes.
_AGENT_HOME_VARS = ("HOME",) + _agent_home_guard.DROPPED_VARS
_RECORDED_AGENT_HOMES = {}
_DROPPED_BINDING = pytest.StashKey[dict]()
def pytest_configure(config):
_RECORDED_BINDING.clear()
_RECORDED_BINDING.update({k: os.environ[k] for k in _BINDING_VARS if os.environ.get(k)})
_RECORDED_AGENT_HOMES.clear()
_RECORDED_AGENT_HOMES.update({k: os.environ[k] for k in _AGENT_HOME_VARS if os.environ.get(k)})
dropped = {k: os.environ.pop(k) for k in ("AGENT_SESSIONS_YAML", "MAM_JOBS_DIR", "DELEGATE_JOB_LOGS_DIR")
if k in os.environ}
ws = os.environ.get("WORKSPACE_ROOT")
@@ -177,6 +186,30 @@ def _isolate_mam_state_dirs(_ambient_control_plane, tmp_path, monkeypatch):
monkeypatch.setenv("MAM_ENV_FILE", str(state / ".mam.env.absent"))
@pytest.fixture(scope="session")
def _ambient_agent_homes():
"""The agent-home variables pytest_configure recorded, as a copy."""
return dict(_RECORDED_AGENT_HOMES)
@pytest.fixture(autouse=True)
def _isolate_agent_homes(_isolate_mam_state_dirs, monkeypatch):
"""Issue #55: the agent-home variables an agent pane carries (lib.sh
exports HOME_DIR, CLAUDE_PROJECT_DIR and LOCAL_BIN into every pane, and
the adapters read CODEX_HOME) are dropped for every test. DiscoveryContext
prefers CLAUDE_PROJECT_DIR over the home_dir a test passes, and every
adapter path reads HOME_DIR before HOME, so with the pane's values a test
that isolates itself through `home_dir` or HOME still read and wrote the
user's real directories. Dropped rather than pointed into tmp_path: the
suite's tests isolate a home by setting HOME (or HOME_DIR themselves),
and a fixture-set HOME_DIR would shadow every one of them the same way.
HOME stays: the real herdr tests and the shim bootstrap find the server
socket under it. mam_sandbox and a test's own setenv run later and win.
test_issue55_agent_homes pins the variable list against the code."""
for var in _agent_home_guard.DROPPED_VARS:
monkeypatch.delenv(var, raising=False)
@pytest.fixture
def own_workspace_root(tmp_path, monkeypatch):
"""Issue #17: every `source lib.sh` rewrites $WORKSPACE_ROOT/.mam/shim/herdr,
@@ -1512,6 +1545,26 @@ def mam_state_leak_guard(_ambient_control_plane):
assert not found, _mam_state_guard.describe(found)
@pytest.fixture(scope="session", autouse=True)
def agent_home_leak_guard(_ambient_agent_homes, tmp_path_factory):
"""Issue #55: no test may leave an entry in the user's real agent homes -
the claude projects directory and the agy, codex, grok, opencode and
hermes conversation directories the ambient environment names. The
per-test fixture is the fix, this is the regression alarm; it reads the
directories' entry names and never hashes or deletes. A claude folder an
operating agent started meanwhile (its name decodes to a real directory
outside the temp roots) is not counted; a new conversation file an
operating agent writes into one of the other directories would be a
false positive, which is rare (an existing conversation writes to its
existing file)."""
dirs = _agent_home_guard.watch_dirs(_ambient_agent_homes)
before = _agent_home_guard.snapshot(dirs)
yield
found = _agent_home_guard.leaks(dirs, before, _agent_home_guard.real_claude_dir(_ambient_agent_homes),
tmp_roots=[str(tmp_path_factory.getbasetemp())])
assert not found, _agent_home_guard.describe(found)
@pytest.fixture(scope="session", autouse=True)
def subscriber_daemon_leak_guard(tmp_path_factory):
"""Issue #19: no `reconcile.sh --subscribe` daemon anchored in this session's