mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-25 00:51:20 +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
|
|
@ -251,18 +251,18 @@ SERVICE_DESCRIPTION = "Hermes Agent Gateway - Messaging Platform Integration"
|
||||||
def _profile_suffix() -> str:
|
def _profile_suffix() -> str:
|
||||||
"""Derive a service-name suffix from the current HERMES_HOME.
|
"""Derive a service-name suffix from the current HERMES_HOME.
|
||||||
|
|
||||||
Returns ``""`` for the default ``~/.hermes``, the profile name for
|
Returns ``""`` for the default root, the profile name for
|
||||||
``~/.hermes/profiles/<name>``, or a short hash for any other custom
|
``<root>/profiles/<name>``, or a short hash for any other path.
|
||||||
HERMES_HOME path.
|
Works correctly in Docker (HERMES_HOME=/opt/data) and standard deployments.
|
||||||
"""
|
"""
|
||||||
import hashlib
|
import hashlib
|
||||||
import re
|
import re
|
||||||
from pathlib import Path as _Path
|
from hermes_constants import get_default_hermes_root
|
||||||
home = get_hermes_home().resolve()
|
home = get_hermes_home().resolve()
|
||||||
default = (_Path.home() / ".hermes").resolve()
|
default = get_default_hermes_root().resolve()
|
||||||
if home == default:
|
if home == default:
|
||||||
return ""
|
return ""
|
||||||
# Detect ~/.hermes/profiles/<name> pattern → use the profile name
|
# Detect <root>/profiles/<name> pattern → use the profile name
|
||||||
profiles_root = (default / "profiles").resolve()
|
profiles_root = (default / "profiles").resolve()
|
||||||
try:
|
try:
|
||||||
rel = home.relative_to(profiles_root)
|
rel = home.relative_to(profiles_root)
|
||||||
|
|
@ -287,9 +287,9 @@ def _profile_arg(hermes_home: str | None = None) -> str:
|
||||||
service definition for a different user (e.g. system service).
|
service definition for a different user (e.g. system service).
|
||||||
"""
|
"""
|
||||||
import re
|
import re
|
||||||
from pathlib import Path as _Path
|
from hermes_constants import get_default_hermes_root
|
||||||
home = Path(hermes_home or str(get_hermes_home())).resolve()
|
home = Path(hermes_home or str(get_hermes_home())).resolve()
|
||||||
default = (_Path.home() / ".hermes").resolve()
|
default = get_default_hermes_root().resolve()
|
||||||
if home == default:
|
if home == default:
|
||||||
return ""
|
return ""
|
||||||
profiles_root = (default / "profiles").resolve()
|
profiles_root = (default / "profiles").resolve()
|
||||||
|
|
|
||||||
|
|
@ -97,10 +97,11 @@ def _apply_profile_override() -> None:
|
||||||
consume = 1
|
consume = 1
|
||||||
break
|
break
|
||||||
|
|
||||||
# 2. If no flag, check ~/.hermes/active_profile
|
# 2. If no flag, check active_profile in the hermes root
|
||||||
if profile_name is None:
|
if profile_name is None:
|
||||||
try:
|
try:
|
||||||
active_path = Path.home() / ".hermes" / "active_profile"
|
from hermes_constants import get_default_hermes_root
|
||||||
|
active_path = get_default_hermes_root() / "active_profile"
|
||||||
if active_path.exists():
|
if active_path.exists():
|
||||||
name = active_path.read_text().strip()
|
name = active_path.read_text().strip()
|
||||||
if name and name != "default":
|
if name and name != "default":
|
||||||
|
|
@ -3313,10 +3314,11 @@ def _invalidate_update_cache():
|
||||||
``hermes update``, every profile is now current.
|
``hermes update``, every profile is now current.
|
||||||
"""
|
"""
|
||||||
homes = []
|
homes = []
|
||||||
# Default profile home
|
# Default profile home (Docker-aware — uses /opt/data in Docker)
|
||||||
default_home = Path.home() / ".hermes"
|
from hermes_constants import get_default_hermes_root
|
||||||
|
default_home = get_default_hermes_root()
|
||||||
homes.append(default_home)
|
homes.append(default_home)
|
||||||
# Named profiles under ~/.hermes/profiles/
|
# Named profiles under <root>/profiles/
|
||||||
profiles_root = default_home / "profiles"
|
profiles_root = default_home / "profiles"
|
||||||
if profiles_root.is_dir():
|
if profiles_root.is_dir():
|
||||||
for entry in profiles_root.iterdir():
|
for entry in profiles_root.iterdir():
|
||||||
|
|
@ -4053,7 +4055,10 @@ def cmd_profile(args):
|
||||||
print(f" {name} chat Start chatting")
|
print(f" {name} chat Start chatting")
|
||||||
print(f" {name} gateway start Start the messaging gateway")
|
print(f" {name} gateway start Start the messaging gateway")
|
||||||
if clone or clone_all:
|
if clone or clone_all:
|
||||||
profile_dir_display = f"~/.hermes/profiles/{name}"
|
try:
|
||||||
|
profile_dir_display = "~/" + str(profile_dir.relative_to(Path.home()))
|
||||||
|
except ValueError:
|
||||||
|
profile_dir_display = str(profile_dir)
|
||||||
print(f"\n Edit {profile_dir_display}/.env for different API keys")
|
print(f"\n Edit {profile_dir_display}/.env for different API keys")
|
||||||
print(f" Edit {profile_dir_display}/SOUL.md for different personality")
|
print(f" Edit {profile_dir_display}/SOUL.md for different personality")
|
||||||
print()
|
print()
|
||||||
|
|
|
||||||
|
|
@ -115,16 +115,26 @@ _HERMES_SUBCOMMANDS = frozenset({
|
||||||
def _get_profiles_root() -> Path:
|
def _get_profiles_root() -> Path:
|
||||||
"""Return the directory where named profiles are stored.
|
"""Return the directory where named profiles are stored.
|
||||||
|
|
||||||
Always ``~/.hermes/profiles/`` — anchored to the user's home,
|
Anchored to the hermes root, NOT to the current HERMES_HOME
|
||||||
NOT to the current HERMES_HOME (which may itself be a profile).
|
(which may itself be a profile). This ensures ``coder profile list``
|
||||||
This ensures ``coder profile list`` can see all profiles.
|
can see all profiles.
|
||||||
|
|
||||||
|
In Docker/custom deployments where HERMES_HOME points outside
|
||||||
|
``~/.hermes``, profiles live under ``HERMES_HOME/profiles/`` so
|
||||||
|
they persist on the mounted volume.
|
||||||
"""
|
"""
|
||||||
return Path.home() / ".hermes" / "profiles"
|
return _get_default_hermes_home() / "profiles"
|
||||||
|
|
||||||
|
|
||||||
def _get_default_hermes_home() -> Path:
|
def _get_default_hermes_home() -> Path:
|
||||||
"""Return the default (pre-profile) HERMES_HOME path."""
|
"""Return the default (pre-profile) HERMES_HOME path.
|
||||||
return Path.home() / ".hermes"
|
|
||||||
|
In standard deployments this is ``~/.hermes``.
|
||||||
|
In Docker/custom deployments where HERMES_HOME is outside ``~/.hermes``
|
||||||
|
(e.g. ``/opt/data``), returns HERMES_HOME directly.
|
||||||
|
"""
|
||||||
|
from hermes_constants import get_default_hermes_root
|
||||||
|
return get_default_hermes_root()
|
||||||
|
|
||||||
|
|
||||||
def _get_active_profile_path() -> Path:
|
def _get_active_profile_path() -> Path:
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,45 @@ def get_hermes_home() -> Path:
|
||||||
return Path(os.getenv("HERMES_HOME", Path.home() / ".hermes"))
|
return Path(os.getenv("HERMES_HOME", Path.home() / ".hermes"))
|
||||||
|
|
||||||
|
|
||||||
|
def get_default_hermes_root() -> Path:
|
||||||
|
"""Return the root Hermes directory for profile-level operations.
|
||||||
|
|
||||||
|
In standard deployments this is ``~/.hermes``.
|
||||||
|
|
||||||
|
In Docker or custom deployments where ``HERMES_HOME`` points outside
|
||||||
|
``~/.hermes`` (e.g. ``/opt/data``), returns ``HERMES_HOME`` directly
|
||||||
|
— that IS the root.
|
||||||
|
|
||||||
|
In profile mode where ``HERMES_HOME`` is ``<root>/profiles/<name>``,
|
||||||
|
returns ``<root>`` so that ``profile list`` can see all profiles.
|
||||||
|
Works both for standard (``~/.hermes/profiles/coder``) and Docker
|
||||||
|
(``/opt/data/profiles/coder``) layouts.
|
||||||
|
|
||||||
|
Import-safe — no dependencies beyond stdlib.
|
||||||
|
"""
|
||||||
|
native_home = Path.home() / ".hermes"
|
||||||
|
env_home = os.environ.get("HERMES_HOME", "")
|
||||||
|
if not env_home:
|
||||||
|
return native_home
|
||||||
|
env_path = Path(env_home)
|
||||||
|
try:
|
||||||
|
env_path.resolve().relative_to(native_home.resolve())
|
||||||
|
# HERMES_HOME is under ~/.hermes (normal or profile mode)
|
||||||
|
return native_home
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Docker / custom deployment.
|
||||||
|
# Check if this is a profile path: <root>/profiles/<name>
|
||||||
|
# If the immediate parent dir is named "profiles", the root is
|
||||||
|
# the grandparent — this covers Docker profiles correctly.
|
||||||
|
if env_path.parent.name == "profiles":
|
||||||
|
return env_path.parent.parent
|
||||||
|
|
||||||
|
# Not a profile path — HERMES_HOME itself is the root
|
||||||
|
return env_path
|
||||||
|
|
||||||
|
|
||||||
def get_optional_skills_dir(default: Path | None = None) -> Path:
|
def get_optional_skills_dir(default: Path | None = None) -> Path:
|
||||||
"""Return the optional-skills directory, honoring package-manager wrappers.
|
"""Return the optional-skills directory, honoring package-manager wrappers.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -755,6 +755,7 @@ class TestProfileArg:
|
||||||
hermes_home = tmp_path / ".hermes"
|
hermes_home = tmp_path / ".hermes"
|
||||||
hermes_home.mkdir()
|
hermes_home.mkdir()
|
||||||
monkeypatch.setattr(Path, "home", lambda: tmp_path)
|
monkeypatch.setattr(Path, "home", lambda: tmp_path)
|
||||||
|
monkeypatch.setenv("HERMES_HOME", str(hermes_home))
|
||||||
result = gateway_cli._profile_arg(str(hermes_home))
|
result = gateway_cli._profile_arg(str(hermes_home))
|
||||||
assert result == ""
|
assert result == ""
|
||||||
|
|
||||||
|
|
@ -763,6 +764,7 @@ class TestProfileArg:
|
||||||
profile_dir = tmp_path / ".hermes" / "profiles" / "mybot"
|
profile_dir = tmp_path / ".hermes" / "profiles" / "mybot"
|
||||||
profile_dir.mkdir(parents=True)
|
profile_dir.mkdir(parents=True)
|
||||||
monkeypatch.setattr(Path, "home", lambda: tmp_path)
|
monkeypatch.setattr(Path, "home", lambda: tmp_path)
|
||||||
|
monkeypatch.setenv("HERMES_HOME", str(tmp_path / ".hermes"))
|
||||||
result = gateway_cli._profile_arg(str(profile_dir))
|
result = gateway_cli._profile_arg(str(profile_dir))
|
||||||
assert result == "--profile mybot"
|
assert result == "--profile mybot"
|
||||||
|
|
||||||
|
|
@ -771,6 +773,7 @@ class TestProfileArg:
|
||||||
custom_home = tmp_path / "custom" / "hermes"
|
custom_home = tmp_path / "custom" / "hermes"
|
||||||
custom_home.mkdir(parents=True)
|
custom_home.mkdir(parents=True)
|
||||||
monkeypatch.setattr(Path, "home", lambda: tmp_path)
|
monkeypatch.setattr(Path, "home", lambda: tmp_path)
|
||||||
|
monkeypatch.setenv("HERMES_HOME", str(tmp_path / ".hermes"))
|
||||||
result = gateway_cli._profile_arg(str(custom_home))
|
result = gateway_cli._profile_arg(str(custom_home))
|
||||||
assert result == ""
|
assert result == ""
|
||||||
|
|
||||||
|
|
@ -779,6 +782,7 @@ class TestProfileArg:
|
||||||
nested = tmp_path / ".hermes" / "profiles" / "mybot" / "subdir"
|
nested = tmp_path / ".hermes" / "profiles" / "mybot" / "subdir"
|
||||||
nested.mkdir(parents=True)
|
nested.mkdir(parents=True)
|
||||||
monkeypatch.setattr(Path, "home", lambda: tmp_path)
|
monkeypatch.setattr(Path, "home", lambda: tmp_path)
|
||||||
|
monkeypatch.setenv("HERMES_HOME", str(tmp_path / ".hermes"))
|
||||||
result = gateway_cli._profile_arg(str(nested))
|
result = gateway_cli._profile_arg(str(nested))
|
||||||
assert result == ""
|
assert result == ""
|
||||||
|
|
||||||
|
|
@ -787,6 +791,7 @@ class TestProfileArg:
|
||||||
bad_profile = tmp_path / ".hermes" / "profiles" / "My Bot!"
|
bad_profile = tmp_path / ".hermes" / "profiles" / "My Bot!"
|
||||||
bad_profile.mkdir(parents=True)
|
bad_profile.mkdir(parents=True)
|
||||||
monkeypatch.setattr(Path, "home", lambda: tmp_path)
|
monkeypatch.setattr(Path, "home", lambda: tmp_path)
|
||||||
|
monkeypatch.setenv("HERMES_HOME", str(tmp_path / ".hermes"))
|
||||||
result = gateway_cli._profile_arg(str(bad_profile))
|
result = gateway_cli._profile_arg(str(bad_profile))
|
||||||
assert result == ""
|
assert result == ""
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -293,12 +293,16 @@ class TestGetActiveProfileName:
|
||||||
monkeypatch.setenv("HERMES_HOME", str(profile_dir))
|
monkeypatch.setenv("HERMES_HOME", str(profile_dir))
|
||||||
assert get_active_profile_name() == "coder"
|
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
|
tmp_path = profile_env
|
||||||
custom = tmp_path / "some" / "other" / "path"
|
custom = tmp_path / "some" / "other" / "path"
|
||||||
custom.mkdir(parents=True)
|
custom.mkdir(parents=True)
|
||||||
monkeypatch.setenv("HERMES_HOME", str(custom))
|
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()
|
home = _get_default_hermes_home()
|
||||||
assert home == tmp_path / ".hermes"
|
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
|
# Edge cases and additional coverage
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
"""Tests for the update check mechanism in hermes_cli.banner."""
|
"""Tests for the update check mechanism in hermes_cli.banner."""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
@ -144,7 +145,8 @@ def test_invalidate_update_cache_clears_all_profiles(tmp_path):
|
||||||
p.mkdir(parents=True)
|
p.mkdir(parents=True)
|
||||||
(p / ".update_check").write_text('{"ts":1,"behind":50}')
|
(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()
|
_invalidate_update_cache()
|
||||||
|
|
||||||
# All three caches should be gone
|
# 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.mkdir()
|
||||||
(default_home / ".update_check").write_text('{"ts":1,"behind":5}')
|
(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()
|
_invalidate_update_cache()
|
||||||
|
|
||||||
assert not (default_home / ".update_check").exists()
|
assert not (default_home / ".update_check").exists()
|
||||||
|
|
|
||||||
62
tests/test_hermes_constants.py
Normal file
62
tests/test_hermes_constants.py
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
"""Tests for hermes_constants module."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from hermes_constants import get_default_hermes_root
|
||||||
|
|
||||||
|
|
||||||
|
class TestGetDefaultHermesRoot:
|
||||||
|
"""Tests for get_default_hermes_root() — Docker/custom deployment awareness."""
|
||||||
|
|
||||||
|
def test_no_hermes_home_returns_native(self, tmp_path, monkeypatch):
|
||||||
|
"""When HERMES_HOME is not set, returns ~/.hermes."""
|
||||||
|
monkeypatch.delenv("HERMES_HOME", raising=False)
|
||||||
|
monkeypatch.setattr(Path, "home", lambda: tmp_path)
|
||||||
|
assert get_default_hermes_root() == tmp_path / ".hermes"
|
||||||
|
|
||||||
|
def test_hermes_home_is_native(self, tmp_path, monkeypatch):
|
||||||
|
"""When HERMES_HOME = ~/.hermes, returns ~/.hermes."""
|
||||||
|
native = tmp_path / ".hermes"
|
||||||
|
native.mkdir()
|
||||||
|
monkeypatch.setattr(Path, "home", lambda: tmp_path)
|
||||||
|
monkeypatch.setenv("HERMES_HOME", str(native))
|
||||||
|
assert get_default_hermes_root() == native
|
||||||
|
|
||||||
|
def test_hermes_home_is_profile(self, tmp_path, monkeypatch):
|
||||||
|
"""When HERMES_HOME is a profile under ~/.hermes, returns ~/.hermes."""
|
||||||
|
native = tmp_path / ".hermes"
|
||||||
|
profile = native / "profiles" / "coder"
|
||||||
|
profile.mkdir(parents=True)
|
||||||
|
monkeypatch.setattr(Path, "home", lambda: tmp_path)
|
||||||
|
monkeypatch.setenv("HERMES_HOME", str(profile))
|
||||||
|
assert get_default_hermes_root() == native
|
||||||
|
|
||||||
|
def test_hermes_home_is_docker(self, tmp_path, monkeypatch):
|
||||||
|
"""When HERMES_HOME points outside ~/.hermes (Docker), returns 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))
|
||||||
|
assert get_default_hermes_root() == docker_home
|
||||||
|
|
||||||
|
def test_hermes_home_is_custom_path(self, tmp_path, monkeypatch):
|
||||||
|
"""Any HERMES_HOME outside ~/.hermes is treated as the root."""
|
||||||
|
custom = tmp_path / "my-hermes-data"
|
||||||
|
custom.mkdir()
|
||||||
|
monkeypatch.setattr(Path, "home", lambda: tmp_path)
|
||||||
|
monkeypatch.setenv("HERMES_HOME", str(custom))
|
||||||
|
assert get_default_hermes_root() == custom
|
||||||
|
|
||||||
|
def test_docker_profile_active(self, tmp_path, monkeypatch):
|
||||||
|
"""When a Docker profile is active (HERMES_HOME=<root>/profiles/<name>),
|
||||||
|
returns the Docker root, not the profile dir."""
|
||||||
|
docker_root = tmp_path / "opt" / "data"
|
||||||
|
profile = docker_root / "profiles" / "coder"
|
||||||
|
profile.mkdir(parents=True)
|
||||||
|
monkeypatch.setattr(Path, "home", lambda: tmp_path)
|
||||||
|
monkeypatch.setenv("HERMES_HOME", str(profile))
|
||||||
|
assert get_default_hermes_root() == docker_root
|
||||||
Loading…
Add table
Add a link
Reference in a new issue