mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-25 00:51:20 +00:00
fix(skills): factor HERMES_HOME resolution into shared _hermes_home helper
The three google-workspace scripts (setup.py, google_api.py, gws_bridge.py) each had their own way of resolving HERMES_HOME: - setup.py imported hermes_constants (crashes outside Hermes process) - google_api.py used os.getenv inline (no strip, no empty handling) - gws_bridge.py defined its own local get_hermes_home() (duplicate) Extract the common logic into _hermes_home.py which: - Delegates to hermes_constants when available (profile support, etc.) - Falls back to os.getenv with .strip() + empty-as-unset handling - Provides display_hermes_home() with ~/ shortening for profiles All three scripts now import from _hermes_home instead of duplicating. 7 regression tests cover the fallback path: env var override, default ~/.hermes, empty env var, display shortening, profile paths, and custom non-home paths. Closes #12722
This commit is contained in:
parent
f14264c438
commit
c34d3f4807
5 changed files with 127 additions and 10 deletions
42
skills/productivity/google-workspace/scripts/_hermes_home.py
Normal file
42
skills/productivity/google-workspace/scripts/_hermes_home.py
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
"""Resolve HERMES_HOME for standalone skill scripts.
|
||||||
|
|
||||||
|
Skill scripts may run outside the Hermes process (e.g. system Python,
|
||||||
|
nix env, CI) where ``hermes_constants`` is not importable. This module
|
||||||
|
provides the same ``get_hermes_home()`` and ``display_hermes_home()``
|
||||||
|
contracts as ``hermes_constants`` without requiring it on ``sys.path``.
|
||||||
|
|
||||||
|
When ``hermes_constants`` IS available it is used directly so that any
|
||||||
|
future enhancements (profile resolution, Docker detection, etc.) are
|
||||||
|
picked up automatically. The fallback path replicates the core logic
|
||||||
|
from ``hermes_constants.py`` using only the stdlib.
|
||||||
|
|
||||||
|
All scripts under ``google-workspace/scripts/`` should import from here
|
||||||
|
instead of duplicating the ``HERMES_HOME = Path(os.getenv(...))`` pattern.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
try:
|
||||||
|
from hermes_constants import display_hermes_home as display_hermes_home
|
||||||
|
from hermes_constants import get_hermes_home as get_hermes_home
|
||||||
|
except (ModuleNotFoundError, ImportError):
|
||||||
|
|
||||||
|
def get_hermes_home() -> Path:
|
||||||
|
"""Return the Hermes home directory (default: ~/.hermes).
|
||||||
|
|
||||||
|
Mirrors ``hermes_constants.get_hermes_home()``."""
|
||||||
|
val = os.environ.get("HERMES_HOME", "").strip()
|
||||||
|
return Path(val) if val else Path.home() / ".hermes"
|
||||||
|
|
||||||
|
def display_hermes_home() -> str:
|
||||||
|
"""Return a user-friendly ``~/``-shortened display string.
|
||||||
|
|
||||||
|
Mirrors ``hermes_constants.display_hermes_home()``."""
|
||||||
|
home = get_hermes_home()
|
||||||
|
try:
|
||||||
|
return "~/" + str(home.relative_to(Path.home()))
|
||||||
|
except ValueError:
|
||||||
|
return str(home)
|
||||||
|
|
@ -31,7 +31,14 @@ from datetime import datetime, timedelta, timezone
|
||||||
from email.mime.text import MIMEText
|
from email.mime.text import MIMEText
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
HERMES_HOME = Path(os.getenv("HERMES_HOME", Path.home() / ".hermes"))
|
# Ensure sibling modules (_hermes_home) are importable when run standalone.
|
||||||
|
_SCRIPTS_DIR = str(Path(__file__).resolve().parent)
|
||||||
|
if _SCRIPTS_DIR not in sys.path:
|
||||||
|
sys.path.insert(0, _SCRIPTS_DIR)
|
||||||
|
|
||||||
|
from _hermes_home import get_hermes_home
|
||||||
|
|
||||||
|
HERMES_HOME = get_hermes_home()
|
||||||
TOKEN_PATH = HERMES_HOME / "google_token.json"
|
TOKEN_PATH = HERMES_HOME / "google_token.json"
|
||||||
CLIENT_SECRET_PATH = HERMES_HOME / "google_client_secret.json"
|
CLIENT_SECRET_PATH = HERMES_HOME / "google_client_secret.json"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,9 +10,12 @@ import sys
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Ensure sibling modules (_hermes_home) are importable when run standalone.
|
||||||
|
_SCRIPTS_DIR = str(Path(__file__).resolve().parent)
|
||||||
|
if _SCRIPTS_DIR not in sys.path:
|
||||||
|
sys.path.insert(0, _SCRIPTS_DIR)
|
||||||
|
|
||||||
def get_hermes_home() -> Path:
|
from _hermes_home import get_hermes_home
|
||||||
return Path(os.environ.get("HERMES_HOME", Path.home() / ".hermes"))
|
|
||||||
|
|
||||||
|
|
||||||
def get_token_path() -> Path:
|
def get_token_path() -> Path:
|
||||||
|
|
|
||||||
|
|
@ -28,13 +28,12 @@ import subprocess
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
try:
|
# Ensure sibling modules (_hermes_home) are importable when run standalone.
|
||||||
from hermes_constants import display_hermes_home, get_hermes_home
|
_SCRIPTS_DIR = str(Path(__file__).resolve().parent)
|
||||||
except ModuleNotFoundError:
|
if _SCRIPTS_DIR not in sys.path:
|
||||||
HERMES_AGENT_ROOT = Path(__file__).resolve().parents[4]
|
sys.path.insert(0, _SCRIPTS_DIR)
|
||||||
if HERMES_AGENT_ROOT.exists():
|
|
||||||
sys.path.insert(0, str(HERMES_AGENT_ROOT))
|
from _hermes_home import display_hermes_home, get_hermes_home
|
||||||
from hermes_constants import display_hermes_home, get_hermes_home
|
|
||||||
|
|
||||||
HERMES_HOME = get_hermes_home()
|
HERMES_HOME = get_hermes_home()
|
||||||
TOKEN_PATH = HERMES_HOME / "google_token.json"
|
TOKEN_PATH = HERMES_HOME / "google_token.json"
|
||||||
|
|
|
||||||
|
|
@ -240,3 +240,69 @@ class TestExchangeAuthCode:
|
||||||
assert setup_module.TOKEN_PATH.exists()
|
assert setup_module.TOKEN_PATH.exists()
|
||||||
# Pending auth is cleaned up
|
# Pending auth is cleaned up
|
||||||
assert not setup_module.PENDING_AUTH_PATH.exists()
|
assert not setup_module.PENDING_AUTH_PATH.exists()
|
||||||
|
|
||||||
|
|
||||||
|
class TestHermesConstantsFallback:
|
||||||
|
"""Tests for _hermes_home.py fallback when hermes_constants is unavailable."""
|
||||||
|
|
||||||
|
HELPER_PATH = (
|
||||||
|
Path(__file__).resolve().parents[2]
|
||||||
|
/ "skills/productivity/google-workspace/scripts/_hermes_home.py"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _load_helper(self, monkeypatch):
|
||||||
|
"""Load _hermes_home.py with hermes_constants blocked."""
|
||||||
|
monkeypatch.setitem(sys.modules, "hermes_constants", None)
|
||||||
|
spec = importlib.util.spec_from_file_location("_hermes_home_test", self.HELPER_PATH)
|
||||||
|
module = importlib.util.module_from_spec(spec)
|
||||||
|
assert spec.loader is not None
|
||||||
|
spec.loader.exec_module(module)
|
||||||
|
return module
|
||||||
|
|
||||||
|
def test_fallback_uses_hermes_home_env_var(self, monkeypatch, tmp_path):
|
||||||
|
"""When hermes_constants is missing, HERMES_HOME comes from env var."""
|
||||||
|
monkeypatch.setenv("HERMES_HOME", str(tmp_path / "custom-hermes"))
|
||||||
|
module = self._load_helper(monkeypatch)
|
||||||
|
assert module.get_hermes_home() == tmp_path / "custom-hermes"
|
||||||
|
|
||||||
|
def test_fallback_defaults_to_dot_hermes(self, monkeypatch):
|
||||||
|
"""When hermes_constants is missing and HERMES_HOME unset, default to ~/.hermes."""
|
||||||
|
monkeypatch.delenv("HERMES_HOME", raising=False)
|
||||||
|
module = self._load_helper(monkeypatch)
|
||||||
|
assert module.get_hermes_home() == Path.home() / ".hermes"
|
||||||
|
|
||||||
|
def test_fallback_ignores_empty_hermes_home(self, monkeypatch):
|
||||||
|
"""Empty/whitespace HERMES_HOME is treated as unset."""
|
||||||
|
monkeypatch.setenv("HERMES_HOME", " ")
|
||||||
|
module = self._load_helper(monkeypatch)
|
||||||
|
assert module.get_hermes_home() == Path.home() / ".hermes"
|
||||||
|
|
||||||
|
def test_fallback_display_hermes_home_shortens_path(self, monkeypatch):
|
||||||
|
"""Fallback display_hermes_home() uses ~/ shorthand like the real one."""
|
||||||
|
monkeypatch.delenv("HERMES_HOME", raising=False)
|
||||||
|
module = self._load_helper(monkeypatch)
|
||||||
|
assert module.display_hermes_home() == "~/.hermes"
|
||||||
|
|
||||||
|
def test_fallback_display_hermes_home_profile_path(self, monkeypatch):
|
||||||
|
"""Fallback display_hermes_home() handles profile paths under ~/."""
|
||||||
|
monkeypatch.setenv("HERMES_HOME", str(Path.home() / ".hermes/profiles/coder"))
|
||||||
|
module = self._load_helper(monkeypatch)
|
||||||
|
assert module.display_hermes_home() == "~/.hermes/profiles/coder"
|
||||||
|
|
||||||
|
def test_fallback_display_hermes_home_custom_path(self, monkeypatch):
|
||||||
|
"""Fallback display_hermes_home() returns full path for non-home locations."""
|
||||||
|
monkeypatch.setenv("HERMES_HOME", "/opt/hermes-custom")
|
||||||
|
module = self._load_helper(monkeypatch)
|
||||||
|
assert module.display_hermes_home() == "/opt/hermes-custom"
|
||||||
|
|
||||||
|
def test_delegates_to_hermes_constants_when_available(self):
|
||||||
|
"""When hermes_constants IS importable, _hermes_home delegates to it."""
|
||||||
|
spec = importlib.util.spec_from_file_location(
|
||||||
|
"_hermes_home_happy", self.HELPER_PATH
|
||||||
|
)
|
||||||
|
module = importlib.util.module_from_spec(spec)
|
||||||
|
assert spec.loader is not None
|
||||||
|
spec.loader.exec_module(module)
|
||||||
|
import hermes_constants
|
||||||
|
assert module.get_hermes_home is hermes_constants.get_hermes_home
|
||||||
|
assert module.display_hermes_home is hermes_constants.display_hermes_home
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue