diff --git a/tests/conftest.py b/tests/conftest.py index 8984442969..32f69a1b46 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,11 +5,10 @@ Hermetic-test invariants enforced here (see AGENTS.md for rationale): 1. **No credential env vars.** All provider/credential-shaped env vars (ending in _API_KEY, _TOKEN, _SECRET, _PASSWORD, _CREDENTIALS, etc.) are unset before every test. Local developer keys cannot leak in. -2. **Isolated HERMES_HOME.** HERMES_HOME points to a per-test tempdir so - code reading ``~/.hermes/*`` via ``get_hermes_home()`` can't see the - real one. (We do NOT also redirect HOME — that broke subprocesses in - CI. Code using ``Path.home() / ".hermes"`` instead of the canonical - ``get_hermes_home()`` is a bug to fix at the callsite.) +2. **Isolated Hermes homes.** HERMES_HOME and the platform-default root + resolve inside a per-test tempdir. Profile/root resolution can inspect + both without probing production state. HOME and Path.home() stay intact + for subprocesses and non-Hermes paths. Explicit test overrides still win. 3. **Deterministic runtime.** TZ=UTC, LANG=C.UTF-8, PYTHONHASHSEED=0. 4. **No HERMES_SESSION_* inheritance** — the agent's current gateway session must not leak into tests. @@ -60,6 +59,11 @@ if str(PROJECT_ROOT) not in sys.path: _PRE_SANDBOX_KANBAN_OVERRIDE = os.environ.get("HERMES_KANBAN_HOME", "").strip() _PRE_SANDBOX_HERMES_HOME = os.environ.get("HERMES_HOME", "") +# Capture before any test fixture can override Path.home()/LOCALAPPDATA. +from hermes_constants import _get_platform_default_hermes_home + +_NATIVE_HERMES_PARENT = _get_platform_default_hermes_home().parent + def _hermes_home_points_at_production(value: str) -> bool: """True when a pre-set HERMES_HOME resolves to the real production root. @@ -489,16 +493,24 @@ def _hermetic_environment(tmp_path, monkeypatch): # custom host resolution override/delete this explicitly. monkeypatch.setenv("HERMES_HONCHO_HOST", "hermes") - # 3. Redirect HERMES_HOME to a per-test tempdir. Code that reads - # ``~/.hermes/*`` via ``get_hermes_home()`` now gets the tempdir. - # - # NOTE: We do NOT also redirect HOME. Doing so broke CI because - # some tests (and their transitive deps) spawn subprocesses that - # inherit HOME and expect it to be stable. If a test genuinely - # needs HOME isolated, it should set it explicitly in its own - # fixture. Any code in the codebase reading ``~/.hermes/*`` via - # ``Path.home() / ".hermes"`` instead of ``get_hermes_home()`` - # is a bug to fix at the callsite. + # 3. Isolate both inputs to profile/root resolution. HERMES_HOME alone + # is insufficient: get_default_hermes_root() resolves the native root + # too, to distinguish standard profiles from custom deployments. + # Patch only the Hermes default, not HOME/Path.home(). Subprocesses need + # a stable HOME. Hardcoded real-home I/O must still trip the guard. + import hermes_constants + + platform_default = hermes_constants._get_platform_default_hermes_home + + def isolated_platform_default() -> Path: + root = platform_default() + # Explicit Path.home()/LOCALAPPDATA overrides in individual tests + # still select their own layout. Suffix changes retain their name. + return tmp_path / root.name if root.parent == _NATIVE_HERMES_PARENT else root + + monkeypatch.setattr( + hermes_constants, "_get_platform_default_hermes_home", isolated_platform_default + ) fake_hermes_home = tmp_path / "hermes_test" fake_hermes_home.mkdir() (fake_hermes_home / "sessions").mkdir()