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:
1 parent
42e51735ba
commit
40293841e7
3 files changed
+381
No files matched your search
@@ -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
|
||||
|
||||
Reference in new issue
Block a user