mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-30 01:41:43 +00:00
fix: profile paths broken in Docker — profiles go to /root/.hermes instead of mounted volume (#7170)
In Docker, HERMES_HOME=/opt/data (set in Dockerfile) and users mount their .hermes directory to /opt/data. However, profile operations used Path.home() / '.hermes' which resolves to /root/.hermes in Docker — an ephemeral container path, not the mounted volume. This caused: - Profiles created at /root/.hermes/profiles/ (lost on container recreate) - active_profile sticky file written to wrong location - profile list looking at wrong directory Fix: Add get_default_hermes_root() to hermes_constants.py that detects Docker/custom deployments (HERMES_HOME outside ~/.hermes) and returns HERMES_HOME as the root. Also handles Docker profiles correctly (<root>/profiles/<name> → root is grandparent). Files changed: - hermes_constants.py: new get_default_hermes_root() - hermes_cli/profiles.py: _get_default_hermes_home() delegates to shared fn - hermes_cli/main.py: _apply_profile_override() + _invalidate_update_cache() - hermes_cli/gateway.py: _profile_suffix() + _profile_arg() - Tests: 12 new tests covering Docker scenarios
This commit is contained in:
parent
916fbf362c
commit
4a65c9cd08
8 changed files with 218 additions and 24 deletions
|
|
@ -755,6 +755,7 @@ class TestProfileArg:
|
|||
hermes_home = tmp_path / ".hermes"
|
||||
hermes_home.mkdir()
|
||||
monkeypatch.setattr(Path, "home", lambda: tmp_path)
|
||||
monkeypatch.setenv("HERMES_HOME", str(hermes_home))
|
||||
result = gateway_cli._profile_arg(str(hermes_home))
|
||||
assert result == ""
|
||||
|
||||
|
|
@ -763,6 +764,7 @@ class TestProfileArg:
|
|||
profile_dir = tmp_path / ".hermes" / "profiles" / "mybot"
|
||||
profile_dir.mkdir(parents=True)
|
||||
monkeypatch.setattr(Path, "home", lambda: tmp_path)
|
||||
monkeypatch.setenv("HERMES_HOME", str(tmp_path / ".hermes"))
|
||||
result = gateway_cli._profile_arg(str(profile_dir))
|
||||
assert result == "--profile mybot"
|
||||
|
||||
|
|
@ -771,6 +773,7 @@ class TestProfileArg:
|
|||
custom_home = tmp_path / "custom" / "hermes"
|
||||
custom_home.mkdir(parents=True)
|
||||
monkeypatch.setattr(Path, "home", lambda: tmp_path)
|
||||
monkeypatch.setenv("HERMES_HOME", str(tmp_path / ".hermes"))
|
||||
result = gateway_cli._profile_arg(str(custom_home))
|
||||
assert result == ""
|
||||
|
||||
|
|
@ -779,6 +782,7 @@ class TestProfileArg:
|
|||
nested = tmp_path / ".hermes" / "profiles" / "mybot" / "subdir"
|
||||
nested.mkdir(parents=True)
|
||||
monkeypatch.setattr(Path, "home", lambda: tmp_path)
|
||||
monkeypatch.setenv("HERMES_HOME", str(tmp_path / ".hermes"))
|
||||
result = gateway_cli._profile_arg(str(nested))
|
||||
assert result == ""
|
||||
|
||||
|
|
@ -787,6 +791,7 @@ class TestProfileArg:
|
|||
bad_profile = tmp_path / ".hermes" / "profiles" / "My Bot!"
|
||||
bad_profile.mkdir(parents=True)
|
||||
monkeypatch.setattr(Path, "home", lambda: tmp_path)
|
||||
monkeypatch.setenv("HERMES_HOME", str(tmp_path / ".hermes"))
|
||||
result = gateway_cli._profile_arg(str(bad_profile))
|
||||
assert result == ""
|
||||
|
||||
|
|
|
|||
|
|
@ -293,12 +293,16 @@ class TestGetActiveProfileName:
|
|||
monkeypatch.setenv("HERMES_HOME", str(profile_dir))
|
||||
assert get_active_profile_name() == "coder"
|
||||
|
||||
def test_custom_path_returns_custom(self, profile_env, monkeypatch):
|
||||
def test_custom_path_returns_default(self, profile_env, monkeypatch):
|
||||
"""A custom HERMES_HOME (Docker, etc.) IS the default root."""
|
||||
tmp_path = profile_env
|
||||
custom = tmp_path / "some" / "other" / "path"
|
||||
custom.mkdir(parents=True)
|
||||
monkeypatch.setenv("HERMES_HOME", str(custom))
|
||||
assert get_active_profile_name() == "custom"
|
||||
# With Docker-aware roots, a custom HERMES_HOME is the default —
|
||||
# not "custom". The user is on the default profile of their
|
||||
# custom deployment.
|
||||
assert get_active_profile_name() == "default"
|
||||
|
||||
|
||||
# ===================================================================
|
||||
|
|
@ -706,6 +710,72 @@ class TestInternalHelpers:
|
|||
home = _get_default_hermes_home()
|
||||
assert home == tmp_path / ".hermes"
|
||||
|
||||
def test_profiles_root_docker_deployment(self, tmp_path, monkeypatch):
|
||||
"""In Docker (HERMES_HOME outside ~/.hermes), profiles go under HERMES_HOME."""
|
||||
docker_home = tmp_path / "opt" / "data"
|
||||
docker_home.mkdir(parents=True)
|
||||
monkeypatch.setattr(Path, "home", lambda: tmp_path)
|
||||
monkeypatch.setenv("HERMES_HOME", str(docker_home))
|
||||
root = _get_profiles_root()
|
||||
assert root == docker_home / "profiles"
|
||||
|
||||
def test_default_hermes_home_docker(self, tmp_path, monkeypatch):
|
||||
"""In Docker, _get_default_hermes_home() returns HERMES_HOME itself."""
|
||||
docker_home = tmp_path / "opt" / "data"
|
||||
docker_home.mkdir(parents=True)
|
||||
monkeypatch.setattr(Path, "home", lambda: tmp_path)
|
||||
monkeypatch.setenv("HERMES_HOME", str(docker_home))
|
||||
home = _get_default_hermes_home()
|
||||
assert home == docker_home
|
||||
|
||||
def test_profiles_root_profile_mode(self, tmp_path, monkeypatch):
|
||||
"""In profile mode (HERMES_HOME under ~/.hermes), profiles root is still ~/.hermes/profiles."""
|
||||
native = tmp_path / ".hermes"
|
||||
profile_dir = native / "profiles" / "coder"
|
||||
profile_dir.mkdir(parents=True)
|
||||
monkeypatch.setattr(Path, "home", lambda: tmp_path)
|
||||
monkeypatch.setenv("HERMES_HOME", str(profile_dir))
|
||||
root = _get_profiles_root()
|
||||
assert root == native / "profiles"
|
||||
|
||||
def test_active_profile_path_docker(self, tmp_path, monkeypatch):
|
||||
"""In Docker, active_profile file lives under HERMES_HOME."""
|
||||
from hermes_cli.profiles import _get_active_profile_path
|
||||
docker_home = tmp_path / "opt" / "data"
|
||||
docker_home.mkdir(parents=True)
|
||||
monkeypatch.setattr(Path, "home", lambda: tmp_path)
|
||||
monkeypatch.setenv("HERMES_HOME", str(docker_home))
|
||||
path = _get_active_profile_path()
|
||||
assert path == docker_home / "active_profile"
|
||||
|
||||
def test_create_profile_docker(self, tmp_path, monkeypatch):
|
||||
"""Profile created in Docker lands under HERMES_HOME/profiles/."""
|
||||
docker_home = tmp_path / "opt" / "data"
|
||||
docker_home.mkdir(parents=True)
|
||||
monkeypatch.setattr(Path, "home", lambda: tmp_path)
|
||||
monkeypatch.setenv("HERMES_HOME", str(docker_home))
|
||||
result = create_profile("orchestrator", no_alias=True)
|
||||
expected = docker_home / "profiles" / "orchestrator"
|
||||
assert result == expected
|
||||
assert expected.is_dir()
|
||||
|
||||
def test_active_profile_name_docker_default(self, tmp_path, monkeypatch):
|
||||
"""In Docker (no profile active), get_active_profile_name() returns 'default'."""
|
||||
docker_home = tmp_path / "opt" / "data"
|
||||
docker_home.mkdir(parents=True)
|
||||
monkeypatch.setattr(Path, "home", lambda: tmp_path)
|
||||
monkeypatch.setenv("HERMES_HOME", str(docker_home))
|
||||
assert get_active_profile_name() == "default"
|
||||
|
||||
def test_active_profile_name_docker_profile(self, tmp_path, monkeypatch):
|
||||
"""In Docker with a profile active, get_active_profile_name() returns the profile name."""
|
||||
docker_home = tmp_path / "opt" / "data"
|
||||
profile = docker_home / "profiles" / "orchestrator"
|
||||
profile.mkdir(parents=True)
|
||||
monkeypatch.setattr(Path, "home", lambda: tmp_path)
|
||||
monkeypatch.setenv("HERMES_HOME", str(profile))
|
||||
assert get_active_profile_name() == "orchestrator"
|
||||
|
||||
|
||||
# ===================================================================
|
||||
# Edge cases and additional coverage
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
"""Tests for the update check mechanism in hermes_cli.banner."""
|
||||
|
||||
import json
|
||||
import os
|
||||
import threading
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
|
@ -144,7 +145,8 @@ def test_invalidate_update_cache_clears_all_profiles(tmp_path):
|
|||
p.mkdir(parents=True)
|
||||
(p / ".update_check").write_text('{"ts":1,"behind":50}')
|
||||
|
||||
with patch.object(Path, "home", return_value=tmp_path):
|
||||
with patch.object(Path, "home", return_value=tmp_path), \
|
||||
patch.dict(os.environ, {"HERMES_HOME": str(default_home)}):
|
||||
_invalidate_update_cache()
|
||||
|
||||
# All three caches should be gone
|
||||
|
|
@ -161,7 +163,8 @@ def test_invalidate_update_cache_no_profiles_dir(tmp_path):
|
|||
default_home.mkdir()
|
||||
(default_home / ".update_check").write_text('{"ts":1,"behind":5}')
|
||||
|
||||
with patch.object(Path, "home", return_value=tmp_path):
|
||||
with patch.object(Path, "home", return_value=tmp_path), \
|
||||
patch.dict(os.environ, {"HERMES_HOME": str(default_home)}):
|
||||
_invalidate_update_cache()
|
||||
|
||||
assert not (default_home / ".update_check").exists()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue