mirror of
https://github.com/nesquena/hermes-webui.git
synced 2026-07-19 22:20:30 +00:00
ee7fda019f
* 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>
72 lines
3.0 KiB
Python
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')
|