mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-18 04:41:56 +00:00
Problem:
After `hermes profile use NAME`, the gateway (started via systemd with
HERMES_HOME=/root/.hermes hardcoded) ignores the active profile and
always runs as the Default profile. WebUI, Telegram, and all non-CLI
platforms are affected.
Root cause:
_apply_profile_override() contained an early-return guard:
if profile_name is None and os.environ.get("HERMES_HOME"):
return # trust the inherited value
The intent was to let child processes inherit their parent's profile via
HERMES_HOME without redundantly re-reading active_profile. But
systemd also sets HERMES_HOME — to the hermes root (/root/.hermes),
not a profile directory — so the guard fired and silently skipped the
active_profile check. The user's `hermes profile use NAME` write to
~/.hermes/active_profile was never seen by the gateway process.
Fix:
Only skip the active_profile check when HERMES_HOME is already a
profile directory, identified by its immediate parent directory being
named "profiles" (e.g. ~/.hermes/profiles/coder or
/opt/data/profiles/coder). When HERMES_HOME points to a root
directory (parent name != "profiles"), continue to read active_profile.
Tests:
- test_hermes_home_at_root_with_active_profile_is_redirected: the
bug scenario — HERMES_HOME=/root/.hermes + active_profile=coder →
HERMES_HOME must be redirected to .../profiles/coder.
Stash-verified: FAILS without fix, PASSES with fix.
- test_hermes_home_already_profile_dir_is_trusted: child-process
inheritance contract unchanged — .../profiles/coder is trusted as-is.
- test_hermes_home_unset_reads_active_profile: classic path unchanged.
- test_hermes_home_unset_default_profile_no_redirect: "default" still
produces no redirect.
4/4 tests green.
Closes #22502.
141 lines
5.1 KiB
Python
141 lines
5.1 KiB
Python
"""Regression tests for _apply_profile_override HERMES_HOME guard (issue #22502).
|
|
|
|
When HERMES_HOME is set to the hermes root (e.g. systemd hardcodes
|
|
HERMES_HOME=/root/.hermes), _apply_profile_override must still read
|
|
active_profile and update HERMES_HOME to the profile directory.
|
|
|
|
When HERMES_HOME is already a profile directory (.../profiles/<name>),
|
|
_apply_profile_override must trust it and return without re-reading
|
|
active_profile (child-process inheritance contract).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
|
|
def _run_apply_profile_override(
|
|
tmp_path, monkeypatch, *, hermes_home: str | None, active_profile: str | None,
|
|
argv: list[str] | None = None,
|
|
):
|
|
"""Run _apply_profile_override in isolation.
|
|
|
|
Returns the value of os.environ["HERMES_HOME"] after the call,
|
|
or None if unset.
|
|
"""
|
|
hermes_root = tmp_path / ".hermes"
|
|
hermes_root.mkdir(parents=True, exist_ok=True)
|
|
|
|
if active_profile is not None:
|
|
(hermes_root / "active_profile").write_text(active_profile)
|
|
|
|
if active_profile and active_profile != "default":
|
|
(hermes_root / "profiles" / active_profile).mkdir(parents=True, exist_ok=True)
|
|
|
|
monkeypatch.setattr(Path, "home", lambda: tmp_path)
|
|
if hermes_home is not None:
|
|
monkeypatch.setenv("HERMES_HOME", hermes_home)
|
|
else:
|
|
monkeypatch.delenv("HERMES_HOME", raising=False)
|
|
|
|
monkeypatch.setattr(sys, "argv", argv or ["hermes", "gateway", "start"])
|
|
|
|
from hermes_cli.main import _apply_profile_override
|
|
_apply_profile_override()
|
|
|
|
return os.environ.get("HERMES_HOME")
|
|
|
|
|
|
class TestApplyProfileOverrideHermesHomeGuard:
|
|
"""Regression guard for issue #22502.
|
|
|
|
Verifies that HERMES_HOME pointing to the hermes root does NOT suppress
|
|
the active_profile check, while HERMES_HOME already pointing to a
|
|
profile directory IS trusted as-is.
|
|
"""
|
|
|
|
def test_hermes_home_at_root_with_active_profile_is_redirected(
|
|
self, tmp_path, monkeypatch
|
|
):
|
|
"""HERMES_HOME=/root/.hermes + active_profile=coder must redirect
|
|
HERMES_HOME to .../profiles/coder.
|
|
|
|
Bug scenario from #22502: systemd sets HERMES_HOME to the hermes root
|
|
and the user switches to a profile via `hermes profile use`.
|
|
Before the fix, the guard returned early and active_profile was ignored.
|
|
"""
|
|
hermes_root = tmp_path / ".hermes"
|
|
hermes_root.mkdir(parents=True, exist_ok=True)
|
|
|
|
result = _run_apply_profile_override(
|
|
tmp_path,
|
|
monkeypatch,
|
|
hermes_home=str(hermes_root),
|
|
active_profile="coder",
|
|
)
|
|
|
|
assert result is not None, "HERMES_HOME must be set after profile redirect"
|
|
assert "profiles" in result, (
|
|
f"Expected HERMES_HOME to point into profiles/ dir, got: {result!r}"
|
|
)
|
|
assert result.endswith("coder"), (
|
|
f"Expected HERMES_HOME to end with 'coder', got: {result!r}"
|
|
)
|
|
|
|
def test_hermes_home_already_profile_dir_is_trusted(self, tmp_path, monkeypatch):
|
|
"""HERMES_HOME=.../profiles/coder must not be overridden even when
|
|
active_profile says something different.
|
|
|
|
Preserves the child-process inheritance contract: a subprocess spawned
|
|
with HERMES_HOME already set to a specific profile must stay in that
|
|
profile.
|
|
"""
|
|
hermes_root = tmp_path / ".hermes"
|
|
profile_dir = hermes_root / "profiles" / "coder"
|
|
profile_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
(hermes_root / "active_profile").write_text("other")
|
|
|
|
monkeypatch.setattr(Path, "home", lambda: tmp_path)
|
|
monkeypatch.setenv("HERMES_HOME", str(profile_dir))
|
|
monkeypatch.setattr(sys, "argv", ["hermes", "gateway", "start"])
|
|
|
|
from hermes_cli.main import _apply_profile_override
|
|
_apply_profile_override()
|
|
|
|
assert os.environ.get("HERMES_HOME") == str(profile_dir), (
|
|
"HERMES_HOME must remain unchanged when already pointing to a profile dir"
|
|
)
|
|
|
|
def test_hermes_home_unset_reads_active_profile(self, tmp_path, monkeypatch):
|
|
"""Classic case: HERMES_HOME unset + active_profile=coder must set
|
|
HERMES_HOME to the profile directory (existing behaviour must not regress).
|
|
"""
|
|
result = _run_apply_profile_override(
|
|
tmp_path,
|
|
monkeypatch,
|
|
hermes_home=None,
|
|
active_profile="coder",
|
|
)
|
|
|
|
assert result is not None
|
|
assert "coder" in result
|
|
|
|
def test_hermes_home_unset_default_profile_no_redirect(self, tmp_path, monkeypatch):
|
|
"""active_profile=default must not redirect HERMES_HOME."""
|
|
hermes_root = tmp_path / ".hermes"
|
|
hermes_root.mkdir(parents=True, exist_ok=True)
|
|
|
|
monkeypatch.setattr(Path, "home", lambda: tmp_path)
|
|
monkeypatch.delenv("HERMES_HOME", raising=False)
|
|
monkeypatch.setattr(sys, "argv", ["hermes", "gateway", "start"])
|
|
(hermes_root / "active_profile").write_text("default")
|
|
|
|
from hermes_cli.main import _apply_profile_override
|
|
_apply_profile_override()
|
|
|
|
assert os.environ.get("HERMES_HOME") is None
|