hermes-agent/tests/test_log_isolation.py
joelbrilliant ef1b06d853 fix(tests): stop the suite writing into the operator's real Hermes logs
hermes_cli/main.py calls setup_logging() at module scope. That resolves
get_hermes_home() and attaches rotating file handlers to the ROOT logger via
a QueueHandler. So merely importing it - which many test modules do, directly
or transitively - points the whole pytest session's logging at
<HERMES_HOME>/logs/agent.log and errors.log.

The _isolate_env fixture already sandboxes HERMES_HOME, but fixtures run
after collection has imported the test modules, and by then the handler holds
an absolute path to the real file. Verified by importing hermes_cli.main in a
clean interpreter and walking the queue listener: both handlers pointed at the
developer's own ~/.hermes/logs/.

Measured on a live install: 126 warnings in a personal agent.log came from
test runs rather than the running gateway - phantom 'FakeTree' Discord
registration failures and 'rejected invalid API key' entries whose paths only
exist in tests/gateway/test_api_server_runs.py. That noise makes genuine
warnings hard to find exactly when someone is debugging.

conftest is imported before any test module, so sandboxing HERMES_HOME there
closes the window. The per-test fixture still applies afterwards.

Also fixes 4 pre-existing failures: tests/gateway/test_channel_directory.py
TestBuildFromSessions was reading the operator's real sessions data for the
same reason.

Full gateway+tools suites: 66 failures on clean origin/main, 62 with this
change, 0 new. The regression guard asserts the value captured AT conftest
import - reading os.environ inside a test passes even with the fix removed,
because the per-test fixture has sandboxed it by then.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 19:53:17 -07:00

88 lines
3.4 KiB
Python

"""The test suite must never write into the operator's real Hermes logs.
`hermes_cli/main.py` calls `setup_logging()` at module scope, which resolves
`get_hermes_home()` and attaches rotating file handlers to the ROOT logger.
Importing it - which many test modules do, directly or transitively - wires
the whole pytest session's logging to `<HERMES_HOME>/logs/agent.log`.
If HERMES_HOME is not already sandboxed at that moment, that is the
operator's real log. Measured on a live install, 126 warnings in a personal
`agent.log` came from test runs rather than the running gateway: phantom
`FakeTree` Discord failures and `rejected invalid API key` entries from
`test_api_server_runs.py`. Noise like that makes genuine warnings hard to
find precisely when someone is debugging.
The per-test env fixture cannot close this: fixtures run after collection has
imported the test modules, and by then the handler holds an absolute path.
`tests/conftest.py` sets HERMES_HOME at module scope for that reason - this
guards the property so a refactor cannot quietly undo it.
"""
import logging
import os
from pathlib import Path
import pytest
def _real_hermes_home() -> Path:
"""Where the operator's logs live, ignoring any test sandboxing."""
return Path.home() / ".hermes"
def _all_file_destinations() -> list[str]:
"""Every file path the root logger can reach, including via a QueueHandler.
Logging is routed through a queue, so the file handlers hang off the
listener rather than the root logger - checking `root.handlers` alone
reports nothing and looks falsely clean.
"""
seen: list[str] = []
def collect(handlers) -> None:
for handler in handlers or ():
path = getattr(handler, "baseFilename", None)
if path:
seen.append(str(path))
listener = getattr(handler, "listener", None)
if listener is not None:
collect(getattr(listener, "handlers", ()))
collect(logging.getLogger().handlers)
try:
import hermes_logging
listener = getattr(hermes_logging, "_queue_listener", None)
if listener is not None:
collect(getattr(listener, "handlers", ()))
except Exception:
pass
return seen
class TestLogIsolation:
def test_hermes_home_is_sandboxed_before_imports(self):
# Deliberately NOT os.environ: by test time the per-test `_isolate_env`
# fixture has sandboxed HERMES_HOME, so reading it here would pass even
# with the conftest block deleted. Assert the value captured at conftest
# import, which is the moment that actually matters.
from tests.conftest import HERMES_HOME_AT_CONFTEST_IMPORT as home
assert home, "conftest must set HERMES_HOME before test modules import"
assert Path(home).resolve() != _real_hermes_home().resolve(), (
f"HERMES_HOME pointed at the operator's real home ({home}) when "
"conftest loaded; import-time setup_logging() writes to their agent.log"
)
def test_importing_the_cli_does_not_target_the_real_logs(self):
pytest.importorskip("hermes_cli.main")
real_logs = str(_real_hermes_home() / "logs")
offenders = [p for p in _all_file_destinations() if p.startswith(real_logs)]
assert offenders == [], (
"the test session is writing into the operator's real Hermes logs:\n "
+ "\n ".join(offenders)
)