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)