tests: keep the home-io guard off the checkout and Hermes' scratch TMPDIR
The default install checks the repo out INSIDE the Hermes home (install.sh INSTALL_DIR=$HERMES_HOME/hermes-agent), so the guard tripped on the checkout's own test data and on traceback source reads from any default-location run. Exempt the project root like the interpreter prefixes: the checkout is not Hermes state. A Hermes-launched shell also hands pytest TMPDIR=<home>/cache/scratch (tagged HERMES_SCRATCH_DIR), and importing hermes_bootstrap re-applies that redirect for a custom HERMES_HOME. Both put the session sandbox, basetemp and every tempfile default inside a guarded root (16 errors in test_update_completion_routing alone under a bare pytest). conftest now strips Hermes' own export before allocating temp space and pins the system default so the import-time hook is a no-op; user-set temp vars are untouched and the parallel runner's own TMPDIR is unaffected.
This commit is contained in:
@@ -97,6 +97,28 @@ def _hermes_home_points_at_production(value: str) -> bool:
|
||||
return resolved.parent.name == "profiles" and resolved.parent.parent == real_root
|
||||
|
||||
|
||||
# ``import hermes_bootstrap`` (transitively: any entry-point module) runs
|
||||
# ``export_scratch_tmp_env()``, which points TMPDIR/TMP/TEMP at
|
||||
# ``<HERMES_HOME>/cache/scratch`` unless a temp var is already set — and a
|
||||
# Hermes-launched shell (agent terminal, ``hermes`` child) arrives with that
|
||||
# redirect already applied, tagged by HERMES_SCRATCH_DIR. Either way the tmp
|
||||
# root ends up INSIDE a guarded real home (the operator's, or a custom one
|
||||
# honored below), so the session sandbox, pytest's basetemp and every
|
||||
# ``tempfile`` default in the code under test trip the real-home guard. Strip
|
||||
# Hermes' own export (the marker tells it apart from a user-set var, which is
|
||||
# left alone) and pin the system default so the import-time hook stays a
|
||||
# no-op. The parallel runner exports its own disk-backed TMPDIR anyway.
|
||||
from hermes_constants import SCRATCH_DIR_MARKER_ENV, SCRATCH_TMP_ENV_VARS
|
||||
|
||||
_HERMES_EXPORTED_TMP = os.environ.get(SCRATCH_DIR_MARKER_ENV, "")
|
||||
if _HERMES_EXPORTED_TMP:
|
||||
for _key in SCRATCH_TMP_ENV_VARS:
|
||||
if os.environ.get(_key, "").strip() == _HERMES_EXPORTED_TMP:
|
||||
del os.environ[_key]
|
||||
del os.environ[SCRATCH_DIR_MARKER_ENV]
|
||||
tempfile.tempdir = None # drop the cached redirect so gettempdir() re-resolves
|
||||
os.environ.setdefault("TMPDIR", tempfile.gettempdir())
|
||||
|
||||
if _hermes_home_points_at_production(os.environ.get("HERMES_HOME", "")):
|
||||
_SESSION_HERMES_HOME = tempfile.mkdtemp(prefix="hermes-test-home-")
|
||||
os.environ["HERMES_HOME"] = _SESSION_HERMES_HOME
|
||||
|
||||
@@ -6,9 +6,14 @@ import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import sqlite3
|
||||
import subprocess
|
||||
import sys
|
||||
import textwrap
|
||||
|
||||
import pytest
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def protected_home(tmp_path, monkeypatch):
|
||||
@@ -125,3 +130,47 @@ def test_close_keeps_a_reused_descriptors_new_owner(tmp_path, monkeypatch):
|
||||
finally:
|
||||
for fd in reopened:
|
||||
original_close(fd)
|
||||
|
||||
|
||||
def test_checkout_inside_a_guarded_root_is_not_hermes_state():
|
||||
"""The default install checks the repo out INSIDE the home (install.sh:
|
||||
INSTALL_DIR=$HERMES_HOME/hermes-agent): the checkout, its .venv and test
|
||||
data are exempt even when the guarded root contains them; siblings under
|
||||
that root are still refused."""
|
||||
from tests.home_io_guard import HomeIOGuard
|
||||
|
||||
guard = HomeIOGuard(lambda: [PROJECT_ROOT.parent])
|
||||
guard.check(PROJECT_ROOT / "tests" / "home_io_guard.py")
|
||||
guard.check(PROJECT_ROOT / ".venv" / "bin" / "python", metadata=True)
|
||||
with pytest.raises(AssertionError, match="REAL hermes home"):
|
||||
guard.check(PROJECT_ROOT.parent / "config.yaml")
|
||||
|
||||
|
||||
def test_hermes_exported_scratch_tmp_is_not_the_test_temp_root(tmp_path):
|
||||
"""A Hermes-launched shell hands pytest TMPDIR=<home>/cache/scratch (tagged by
|
||||
HERMES_SCRATCH_DIR). With that home guarded, honoring it would put the session
|
||||
sandbox, basetemp and every tempfile default inside the guarded root; the
|
||||
conftest must drop Hermes' own export before anything allocates temp space."""
|
||||
home = tmp_path / "home"
|
||||
scratch = home / "cache" / "scratch"
|
||||
scratch.mkdir(parents=True)
|
||||
probe = tmp_path / "test_probe.py"
|
||||
probe.write_text(textwrap.dedent(f"""
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
def test_temp_root_is_outside_the_honored_home():
|
||||
home = Path({str(home)!r}).resolve()
|
||||
assert not Path(tempfile.gettempdir()).resolve().is_relative_to(home)
|
||||
with tempfile.TemporaryDirectory() as made:
|
||||
assert not Path(made).resolve().is_relative_to(home)
|
||||
"""), encoding="utf-8")
|
||||
env = {k: v for k, v in os.environ.items()
|
||||
if k not in ("TMPDIR", "TMP", "TEMP", "HERMES_SCRATCH_DIR", "HERMES_TEST_SANDBOX_HOME")}
|
||||
env.update(HERMES_HOME=str(home), TMPDIR=str(scratch), HERMES_SCRATCH_DIR=str(scratch))
|
||||
result = subprocess.run(
|
||||
[sys.executable, "-m", "pytest", "-p", "tests.conftest", "-p", "no:cacheprovider", "-q", str(probe)],
|
||||
cwd=PROJECT_ROOT, env=env, capture_output=True, text=True, timeout=120,
|
||||
)
|
||||
assert result.returncode == 0, result.stdout + result.stderr
|
||||
assert "1 passed" in result.stdout
|
||||
|
||||
@@ -18,6 +18,12 @@ _INTERPRETER_PREFIXES = tuple({
|
||||
# whose site-packages sits under the (real) Hermes home; third-party imports from it are the
|
||||
# interpreter's installation, not Hermes state.
|
||||
Path(p).resolve() for p in sys.path if p and Path(p).name in ("site-packages", "dist-packages")
|
||||
} | {
|
||||
# The default install checks the repo out INSIDE the home (install.sh:
|
||||
# INSTALL_DIR=$HERMES_HOME/hermes-agent). Reading test data, sources for tracebacks, or the
|
||||
# checkout's own .venv is not Hermes state; without this every run from a default install
|
||||
# trips on its first traceback.
|
||||
Path(__file__).resolve().parent.parent,
|
||||
})
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user