fix: harden test server isolation — HERMES_BASE_HOME + strip provider keys + mock _get_active_hermes_home in unit tests (#620)

Fixes the root cause of OPENROUTER_API_KEY being overwritten with test-key-fresh on every pytest run.

Three-layer fix:
1. Unit tests: mock _get_active_hermes_home in TestApplyOnboardingSetupGuard so .env writes land in /tmp, never ~/.hermes
2. Test server subprocess: add HERMES_BASE_HOME=TEST_STATE_DIR to hard-lock profile resolution inside the server process
3. Test server subprocess: strip real provider keys (OPENROUTER_API_KEY etc.) from the inherited env before server starts

Reviewed and approved by @nesquena. 1373 passed, 0 skipped.
This commit is contained in:
nesquena-hermes
2026-04-16 23:03:32 -07:00
committed by GitHub
parent 79428f93c6
commit e7b8ab4d70
2 changed files with 47 additions and 17 deletions

View File

@@ -274,6 +274,14 @@ def test_server():
# os.environ already set at module level above; no-op here.
env = os.environ.copy()
# Strip real provider keys so test subprocess never inherits production credentials.
# The test server uses a mock/isolated config — no real API calls are made.
for _k in list(env):
if any(_k.startswith(p) for p in (
'OPENROUTER_API_KEY', 'OPENAI_API_KEY', 'ANTHROPIC_API_KEY',
'GOOGLE_API_KEY', 'DEEPSEEK_API_KEY',
)):
del env[_k]
env.update({
"HERMES_WEBUI_PORT": str(TEST_PORT),
"HERMES_WEBUI_HOST": "127.0.0.1",
@@ -281,6 +289,14 @@ def test_server():
"HERMES_WEBUI_DEFAULT_WORKSPACE": str(TEST_WORKSPACE),
"HERMES_WEBUI_DEFAULT_MODEL": "openai/gpt-5.4-mini",
"HERMES_HOME": str(TEST_STATE_DIR),
# Belt-and-suspenders: HERMES_BASE_HOME hard-locks _DEFAULT_HERMES_HOME
# in api/profiles.py to the test state dir regardless of profile switching
# or any os.environ mutation that happens inside the server process.
# Without this, a profile switch or active_profile file in the real
# ~/.hermes can redirect _get_active_hermes_home() out of the sandbox,
# causing onboarding writes (config.yaml, .env) to land in the production
# ~/.hermes/profiles/webui/ and overwrite real API keys.
"HERMES_BASE_HOME": str(TEST_STATE_DIR),
})
# Pass agent dir if discovered so server.py doesn't have to re-discover

View File

@@ -151,11 +151,17 @@ class TestApplyOnboardingSetupGuard:
def test_setup_allowed_with_confirm_overwrite(self):
"""With confirm_overwrite=True, setup may proceed (will hit real logic)."""
import api.onboarding as mod
import tempfile
fake_config_path = pathlib.Path("/tmp/_test_config_confirm.yaml")
fake_config_path.unlink(missing_ok=True) # start clean
try:
# Without patching Path.exists, use a non-existent path so it won't block
with tempfile.TemporaryDirectory() as tmp_home:
tmp_home_path = pathlib.Path(tmp_home)
# Without patching Path.exists, use a non-existent path so it won't block.
# Also redirect _get_active_hermes_home so .env writes go to the temp dir,
# never to the real ~/.hermes/.env.
with mock.patch.object(mod, "_get_active_hermes_home", return_value=tmp_home_path):
result = mod.apply_onboarding_setup(
{
"provider": "openrouter",
@@ -176,11 +182,19 @@ class TestApplyOnboardingSetupGuard:
def test_setup_allowed_when_no_config_exists(self):
"""Fresh install: no config.yaml → setup proceeds normally (no blocking error)."""
import api.onboarding as mod
import tempfile
fake_config_path = pathlib.Path("/tmp/_test_config_fresh.yaml")
fake_config_path.unlink(missing_ok=True)
try:
with mock.patch.object(mod, "_get_config_path", return_value=fake_config_path):
with tempfile.TemporaryDirectory() as tmp_home:
tmp_home_path = pathlib.Path(tmp_home)
# Redirect both config path and hermes home so writes stay in /tmp,
# never touching the real ~/.hermes/.env.
with (
mock.patch.object(mod, "_get_config_path", return_value=fake_config_path),
mock.patch.object(mod, "_get_active_hermes_home", return_value=tmp_home_path),
):
result = mod.apply_onboarding_setup(
{
"provider": "openrouter",