Files
aiturk-hermes-ide/tests/hermes_state/test_live_db_guard_ancestry.py

161 lines
5.7 KiB
Python

"""The live-DB guard must survive a scrubbed child environment (#82770).
Forensic background: a read-only sweep of production ``state.db`` files found
hundreds of zero-message "open" gateway session rows carrying test-fixture
identities (``chat-1`` / ``user-1`` / ``wx-chat``), with matching
``gateway_routing`` scopes pointing at ``pytest-of-*`` temp directories.
The escape is structural, not a one-off test bug. Hermetic isolation rides
entirely on the process environment: ``HERMES_HOME`` says *where* to write and
``PYTEST_CURRENT_TEST`` / ``PYTEST_VERSION`` say *whether the guard is armed*.
Both live in the same carrier, so a child spawned with a rebuilt environment
loses them together — it aims at the developer's real ``state.db`` and
silences the only check that would have stopped it, in one step.
Process ancestry is the signal that survives an env rebuild, so these tests
pin that the guard is armed by ancestry when the environment no longer says
"pytest".
These tests drive ``_ensure_test_isolation`` rather than constructing a real
``SessionDB``: if the guard regresses, the assertion must fail *without* the
test itself writing to the developer's live database.
"""
import os
import subprocess
import sys
from pathlib import Path
import pytest
import hermes_state
REPO_ROOT = Path(__file__).resolve().parents[2]
# Probe run in the child: resolve the REAL platform state root (not a
# hardcoded ~/.hermes — that root is %LOCALAPPDATA%\hermes on Windows) and
# report whether the guard refuses it.
_CHILD_PROBE = """
import sys
sys.path.insert(0, {repo!r})
import hermes_state
root = hermes_state._real_platform_state_root()
if root is None:
print("NO-ROOT")
else:
try:
hermes_state._ensure_test_isolation(root / "state.db")
except RuntimeError:
print("REFUSED")
else:
print("ALLOWED")
"""
def _scrubbed_env(**overrides):
"""The environment a rebuilt-from-scratch child spawn ends up with.
Also strips ``HERMES_TEST_ISOLATION`` — the conftest-exported marker
layer would otherwise arm the guard first and these tests would no
longer prove anything about the ancestry fallback they exist to pin.
"""
env = {
k: v
for k, v in os.environ.items()
if not k.startswith("PYTEST_")
and k not in ("HERMES_HOME", "HERMES_TEST_ISOLATION")
}
env.update(overrides)
return env
def _run_probe(env):
result = subprocess.run(
[sys.executable, "-c", _CHILD_PROBE.format(repo=str(REPO_ROOT))],
env=env,
capture_output=True,
text=True,
cwd=str(REPO_ROOT),
timeout=120,
)
verdict = result.stdout.strip().splitlines()[-1] if result.stdout.strip() else ""
if verdict == "NO-ROOT":
pytest.skip("no real platform state root resolvable on this machine")
assert verdict in ("REFUSED", "ALLOWED"), (
f"probe produced no verdict.\nstdout={result.stdout!r}\n"
f"stderr={result.stderr!r}"
)
return verdict
class TestScrubbedChildEnvironment:
def test_child_without_pytest_env_still_refuses_production_db(self):
"""The #82770 escape: no PYTEST_* and no HERMES_HOME, yet still a test.
This is the exact shape of the leak — the child resolves the real
``state.db`` because ``HERMES_HOME`` is gone, and the env-only guard
sees a "normal user run" because ``PYTEST_*`` is gone with it.
"""
assert _run_probe(_scrubbed_env()) == "REFUSED"
def test_child_inheriting_pytest_env_still_refuses_production_db(self):
"""The pre-existing env path must keep working unchanged."""
env = dict(os.environ)
env.pop("HERMES_HOME", None)
env.setdefault("PYTEST_CURRENT_TEST", "tests/x.py::test_x (call)")
assert _run_probe(env) == "REFUSED"
def test_env_bypass_lets_a_deliberate_child_through(self):
"""A test that genuinely needs the live DB in a child can opt out.
``_STATE_DB_GUARD_BYPASS`` is a module global and cannot cross a
process boundary, so ancestry-armed children need an env-carried
escape hatch or they would have no way to opt out at all.
"""
env = _scrubbed_env(**{hermes_state._STATE_DB_GUARD_BYPASS_ENV: "1"})
assert _run_probe(env) == "ALLOWED"
class TestPytestProcessRecognition:
"""Unit-level checks for the ancestry predicate's matching rules."""
class _FakeProc:
def __init__(self, cmdline):
self._cmdline = cmdline
def cmdline(self):
return self._cmdline
@pytest.mark.parametrize(
"cmdline",
[
["/usr/bin/python", "-m", "pytest", "tests/"],
["/venv/bin/pytest", "-q"],
[r"C:\venv\Scripts\pytest.exe", "-q"],
["/usr/bin/py.test", "tests/"],
],
)
def test_recognises_pytest_invocations(self, cmdline):
assert hermes_state._process_looks_like_pytest(self._FakeProc(cmdline))
@pytest.mark.parametrize(
"cmdline",
[
["hermes", "gateway", "start"],
["/usr/bin/python", "-m", "hermes_cli.main", "sessions", "list"],
# A path that merely *contains* "pytest" is not a pytest process:
# tmp paths like /tmp/pytest-of-dev/... show up in real argv.
["hermes", "run", "--file", "/tmp/pytest-of-dev/test0/input.txt"],
],
)
def test_ignores_non_pytest_invocations(self, cmdline):
assert not hermes_state._process_looks_like_pytest(self._FakeProc(cmdline))
def test_unreadable_process_is_not_pytest(self):
class _Denied:
def cmdline(self):
raise PermissionError("access denied")
assert not hermes_state._process_looks_like_pytest(_Denied())