From 163ecaebffed845bc3e1143a1bfae8355fbc9425 Mon Sep 17 00:00:00 2001 From: teknium1 <127238744+teknium1@users.noreply.github.com> Date: Mon, 14 Sep 2026 18:45:15 -0700 Subject: [PATCH] test(conftest): relocate pytest's basetemp when it sits inside the operator's Hermes home MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every per-test sandbox is /.../hermes_test, and get_default_hermes_root() prefers the platform-native home whenever HERMES_HOME sits under it. A basetemp inside ~/.hermes (pytest --basetemp, or TMPDIR/TEMP pointing there — the default on Windows, where the home is %LOCALAPPDATA%\hermes) therefore turned every sandbox back into the live install, and any test that resolves the default profile wrote fixtures over the operator's config.yaml, .env and MEMORY.md. Hook into pytest_configure after _pytest.tmpdir has built the TempPathFactory and move a basetemp that resolves under the native home to a fresh tempdir outside it (falling back to the repo's ignored .pytest_cache when the system temp dir is itself inside the home). The fix lives at the basetemp seam so it covers every test, not only the two writers in test_profiles.py, and needs no per-test fixture or opt-out marker. Fixes #111101 --- tests/conftest.py | 32 ++++++++++++++++++++ tests/test_basetemp_isolation.py | 51 ++++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+) create mode 100644 tests/test_basetemp_isolation.py diff --git a/tests/conftest.py b/tests/conftest.py index 23448b80a9..e12d56ad0b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1227,8 +1227,40 @@ _OS_MARKS = { } +def _relocate_basetemp_outside_operator_home(config) -> None: + """Move pytest's basetemp out of the operator's platform-native Hermes home. + + Every per-test sandbox is ``/.../hermes_test``. ``get_default_hermes_root()`` + prefers the platform-native home whenever ``HERMES_HOME`` sits *under* it, so a basetemp + inside ``~/.hermes`` (or ``%LOCALAPPDATA%\\hermes``, where ``TEMP`` commonly lives on + Windows) turns the sandbox back into the live install and ``get_profile_dir("default")`` + writes fixtures over the operator's config.yaml / .env / MEMORY.md (#111101). + """ + from hermes_constants import _get_platform_default_hermes_home + + native = _get_platform_default_hermes_home().resolve() + factory = config._tmp_path_factory + given = factory._given_basetemp + candidate = given if given is not None else Path( + os.environ.get("PYTEST_DEBUG_TEMPROOT") or tempfile.gettempdir() + ) + if not candidate.resolve().is_relative_to(native): + return + # The system temp dir may itself be inside the home (Windows TEMP under the + # Hermes home); the repo's ignored cache dir is always outside it. + fallback = PROJECT_ROOT / ".pytest_cache" + safe_root = None if not Path(tempfile.gettempdir()).resolve().is_relative_to(native) else fallback + if safe_root is not None: + safe_root.mkdir(exist_ok=True) + safe = Path(tempfile.mkdtemp(prefix="hermes-pytest-basetemp-", dir=safe_root)) + factory._given_basetemp = safe + config.option.basetemp = str(safe) + + +@pytest.hookimpl(trylast=True) # after _pytest.tmpdir has built config._tmp_path_factory def pytest_configure(config): # noqa: D401 — pytest hook """Register markers used by hermetic conftest.""" + _relocate_basetemp_outside_operator_home(config) config.addinivalue_line( "markers", f"{_LIVE_SYSTEM_GUARD_BYPASS_MARK}: bypass the live-system guard " diff --git a/tests/test_basetemp_isolation.py b/tests/test_basetemp_isolation.py new file mode 100644 index 0000000000..3819b4bf8a --- /dev/null +++ b/tests/test_basetemp_isolation.py @@ -0,0 +1,51 @@ +"""pytest's basetemp must never sit inside the operator's platform-native Hermes home. + +Every per-test sandbox is ``/.../hermes_test`` and ``get_default_hermes_root()`` +prefers the platform-native home whenever ``HERMES_HOME`` sits *under* it — so a basetemp +inside the home silently turns the sandbox back into the live install (#111101). +""" +from __future__ import annotations + +from pathlib import Path +from types import SimpleNamespace + +import pytest + +import hermes_constants +from tests import conftest as suite_conftest + + +def _config_with_basetemp(given: Path | None) -> SimpleNamespace: + return SimpleNamespace( + _tmp_path_factory=SimpleNamespace(_given_basetemp=given), + option=SimpleNamespace(basetemp=str(given) if given else None), + ) + + +def test_basetemp_inside_the_native_home_is_relocated_outside_it(tmp_path, monkeypatch): + native = tmp_path / "native-home" + native.mkdir() + monkeypatch.setattr(hermes_constants, "_get_platform_default_hermes_home", lambda: native) + config = _config_with_basetemp(native / ".repro") + + suite_conftest._relocate_basetemp_outside_operator_home(config) + + relocated = config._tmp_path_factory._given_basetemp + assert relocated is not None and not relocated.resolve().is_relative_to(native.resolve()) + assert config.option.basetemp == str(relocated) + # The sandbox derived from it no longer resolves to the native root. + monkeypatch.setenv("HERMES_HOME", str(relocated / "t0" / "hermes_test")) + assert hermes_constants.get_default_hermes_root() == relocated / "t0" / "hermes_test" + + +def test_basetemp_outside_the_native_home_is_left_alone(tmp_path, monkeypatch): + native = tmp_path / "native-home" + native.mkdir() + monkeypatch.setattr(hermes_constants, "_get_platform_default_hermes_home", lambda: native) + given = tmp_path / "elsewhere" + config = _config_with_basetemp(given) + + suite_conftest._relocate_basetemp_outside_operator_home(config) + + assert config._tmp_path_factory._given_basetemp == given + assert config.option.basetemp == str(given)