Files
hermes-webui/tests/_pytest_port.py
T
nesquena-hermes ee7fda019f test-infra: hard-isolate test state from production + resilient teardown (#4289)
* test-infra: hard-isolate test state from production + robust teardown

Two production-safety fixes to tests/conftest.py + tests/_pytest_port.py:

1. TEST_STATE_DIR now defaults under the OS temp dir (/tmp/hermes-webui-tests/)
   instead of HERMES_HOME/webui-test-<hash> — which had been writing test state
   INTO the real ~/.hermes/profiles/webui/ (144 leaked webui-test-* dirs found).
   Added a hard-stop guard: refuse to run if the resolved test state dir is
   inside the production Hermes home, so a misconfigured env can never let tests
   wipe/clobber production profiles/sessions/credentials.

2. _rmtree_retry is now resilient on Linux (was a bare shutil.rmtree): retries
   transient OSError [Errno 39] Directory-not-empty races (a background daemon
   writing into the tree during teardown) + a final ignore_errors sweep, and
   warns instead of raising so a benign cleanup race can't spuriously ERROR a
   test (the failure mode seen near #4283 context-reconciliation tests).

* test-infra: anchor production-proximity guard on literal ~/.hermes (not $HERMES_HOME)

The first guard compared TEST_STATE_DIR against $HERMES_HOME, which is overridden
to TEST_STATE_DIR itself during a run -> false-tripped collection (6 modules) with
'test dir X is inside production home X'. Now anchor on the literal user-home
~/.hermes and explicitly allow any dir under the OS temp root, so the temp default
never trips while a real ~/.hermes target still hard-stops.

* test-infra: close TMPDIR-under-~/.hermes guard hole (Codex)

Codex flagged: the 'temp is always allowed' exception could be exploited if
TMPDIR itself is set under ~/.hermes — then a 'temp' path is a production path and
_under_temp would wrongly suppress the guard. Now the temp exception only holds
when the temp root is itself OUTSIDE the production home. Verified: normal /tmp
default allowed; TMPDIR=~/.hermes/tmp now trips the guard.

* test-infra: mirror production-proximity guard into _pytest_port.py (Opus defense-in-depth)

* test-infra: remove ~/.hermes production-path fallbacks from 4 test files (Codex)

Codex's kickback found the real gap: test_sprint5, test_sprint45,
test_onboarding_existing_config, test_regressions each hardcoded a
~/.hermes/webui-mvp-test fallback for their state dir (used when running
standalone or if the env var is unset). Repointed all to the shared isolated
TEST_STATE_DIR from _pytest_port (temp-rooted). Also refreshed stale
~/.hermes/webui-mvp-test references in test_gateway_sync comment + ARCHITECTURE.md.
Now ZERO production-path fallbacks remain in tests.

---------

Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
2026-06-15 19:04:16 -07:00

72 lines
3.0 KiB
Python

"""
Shared test server constants for use in individual test files.
Instead of hardcoding ``BASE = "http://127.0.0.1:8788"`` in every test file,
import from here so the port and state dir are always consistent with
what conftest.py computed for this worktree.
Usage::
from tests._pytest_port import BASE
conftest.py publishes ``HERMES_WEBUI_TEST_PORT`` and
``HERMES_WEBUI_TEST_STATE_DIR`` to ``os.environ`` at module level
(before any test file is imported), so this module always reads the
correct values. The auto-derivation fallback matches conftest's logic
exactly, so standalone imports also work correctly.
"""
import hashlib
import os
import pathlib
def _auto_test_port(repo_root: pathlib.Path) -> int:
h = int(hashlib.md5(str(repo_root).encode()).hexdigest(), 16)
return 20000 + (h % 10000)
def _auto_state_dir_name(repo_root: pathlib.Path) -> str:
h = hashlib.md5(str(repo_root).encode()).hexdigest()[:8]
return f"webui-test-{h}"
_TESTS_DIR = pathlib.Path(__file__).parent.resolve()
_REPO_ROOT = _TESTS_DIR.parent.resolve()
TEST_PORT = int(os.environ.get('HERMES_WEBUI_TEST_PORT',
str(_auto_test_port(_REPO_ROOT))))
BASE = f"http://127.0.0.1:{TEST_PORT}"
# Test state dir: prefer the value conftest.py published to the environment.
# The standalone fallback anchors under the OS temp dir (NOT ~/.hermes) so test
# state is never created inside the production Hermes home — matching conftest's
# hard-isolation default. (See conftest.py TEST_STATE_DIR.)
import tempfile as _tempfile
_TEST_STATE_ROOT = pathlib.Path(
os.environ.get('HERMES_WEBUI_TEST_STATE_ROOT', _tempfile.gettempdir())
) / 'hermes-webui-tests'
TEST_STATE_DIR = pathlib.Path(os.environ.get(
'HERMES_WEBUI_TEST_STATE_DIR',
str(_TEST_STATE_ROOT / _auto_state_dir_name(_REPO_ROOT))
)).resolve()
# Defense-in-depth: mirror conftest.py's production-proximity guard so a
# standalone import (without conftest) can't resolve a state dir inside the real
# ~/.hermes either. Same logic: trip only if under literal ~/.hermes and the temp
# root isn't itself under production.
_PROD_HERMES_HOME = (pathlib.Path.home() / '.hermes').resolve()
_TEMP_ROOT = pathlib.Path(_tempfile.gettempdir()).resolve()
_temp_root_is_safe = not (
_TEMP_ROOT == _PROD_HERMES_HOME or _PROD_HERMES_HOME in _TEMP_ROOT.parents
)
_under_temp = _temp_root_is_safe and (
TEST_STATE_DIR == _TEMP_ROOT or _TEMP_ROOT in TEST_STATE_DIR.parents
)
_under_prod = TEST_STATE_DIR == _PROD_HERMES_HOME or _PROD_HERMES_HOME in TEST_STATE_DIR.parents
if _under_prod and not _under_temp:
raise RuntimeError(
f"REFUSING TO RUN: test state dir {TEST_STATE_DIR} is inside the production "
f"Hermes home {_PROD_HERMES_HOME}. Tests must never touch production files."
)
# Default model injected by conftest — tests that mutate the default model
# must restore to this value so later tests see a consistent baseline.
TEST_DEFAULT_MODEL = os.environ.get('HERMES_WEBUI_DEFAULT_MODEL', 'openai/gpt-5.4-mini')