test(conftest): relocate pytest's basetemp when it sits inside the operator's Hermes home

Every per-test sandbox is <basetemp>/.../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
This commit is contained in:
teknium1
2026-09-14 18:45:15 -07:00
committed by Teknium
parent aa0b228c2c
commit 163ecaebff
2 changed files with 83 additions and 0 deletions
+32
View File
@@ -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 ``<basetemp>/.../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 "
+51
View File
@@ -0,0 +1,51 @@
"""pytest's basetemp must never sit inside the operator's platform-native Hermes home.
Every per-test sandbox is ``<basetemp>/.../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)