mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-25 00:51:20 +00:00
config.yaml terminal.cwd is now the single source of truth for working directory. MESSAGING_CWD and TERMINAL_CWD in .env are deprecated with a migration warning. Changes: 1. config.py: Remove MESSAGING_CWD from OPTIONAL_ENV_VARS (setup wizard no longer prompts for it). Add warn_deprecated_cwd_env_vars() that prints a migration hint when deprecated env vars are detected. 2. gateway/run.py: Replace all MESSAGING_CWD reads with TERMINAL_CWD (which is bridged from config.yaml terminal.cwd). MESSAGING_CWD is still accepted as a backward-compat fallback with deprecation warning. Config bridge skips cwd placeholder values so they don't clobber the resolved TERMINAL_CWD. 3. cli.py: Guard against lazy-import clobbering — when cli.py is imported lazily during gateway runtime (via delegate_tool), don't let load_cli_config() overwrite an already-resolved TERMINAL_CWD with os.getcwd() of the service's working directory. (#10817) 4. hermes_cli/main.py: Add 'hermes memory reset' command with --target all/memory/user and --yes flags. Profile-scoped via HERMES_HOME. Migration path for users with .env settings: Remove MESSAGING_CWD / TERMINAL_CWD from .env Add to config.yaml: terminal: cwd: /your/project/path Addresses: #10225, #4672, #10817, #7663
207 lines
8.8 KiB
Python
207 lines
8.8 KiB
Python
"""Tests for the config.yaml → env var bridge logic in gateway/run.py.
|
|
|
|
Specifically tests that top-level `cwd:` and `backend:` in config.yaml
|
|
are correctly bridged to TERMINAL_CWD / TERMINAL_ENV env vars as
|
|
convenience aliases for `terminal.cwd` / `terminal.backend`.
|
|
|
|
The bridge logic is module-level code in gateway/run.py, so we test
|
|
the semantics by reimplementing the relevant config bridge snippet and
|
|
asserting the expected env var outcomes.
|
|
"""
|
|
|
|
import os
|
|
import json
|
|
import pytest
|
|
|
|
|
|
def _simulate_config_bridge(cfg: dict, initial_env: dict | None = None):
|
|
"""Simulate the gateway config bridge logic from gateway/run.py.
|
|
|
|
Returns the resulting env dict (only TERMINAL_* and MESSAGING_CWD keys).
|
|
"""
|
|
env = dict(initial_env or {})
|
|
|
|
# --- Replicate lines 54-56: generic top-level bridge (for context) ---
|
|
for key, val in cfg.items():
|
|
if isinstance(val, (str, int, float, bool)) and key not in env:
|
|
env[key] = str(val)
|
|
|
|
# --- Replicate lines 59-87: terminal config bridge ---
|
|
terminal_cfg = cfg.get("terminal", {})
|
|
if terminal_cfg and isinstance(terminal_cfg, dict):
|
|
terminal_env_map = {
|
|
"backend": "TERMINAL_ENV",
|
|
"cwd": "TERMINAL_CWD",
|
|
"timeout": "TERMINAL_TIMEOUT",
|
|
}
|
|
for cfg_key, env_var in terminal_env_map.items():
|
|
if cfg_key in terminal_cfg:
|
|
val = terminal_cfg[cfg_key]
|
|
# Skip cwd placeholder values — don't overwrite already-resolved
|
|
# TERMINAL_CWD. Mirrors the fix in gateway/run.py.
|
|
if cfg_key == "cwd" and str(val) in (".", "auto", "cwd"):
|
|
continue
|
|
if isinstance(val, list):
|
|
env[env_var] = json.dumps(val)
|
|
else:
|
|
env[env_var] = str(val)
|
|
|
|
# --- NEW: top-level aliases (the fix being tested) ---
|
|
top_level_aliases = {
|
|
"cwd": "TERMINAL_CWD",
|
|
"backend": "TERMINAL_ENV",
|
|
}
|
|
for alias_key, alias_env in top_level_aliases.items():
|
|
if alias_env not in env:
|
|
alias_val = cfg.get(alias_key)
|
|
if isinstance(alias_val, str) and alias_val.strip():
|
|
env[alias_env] = alias_val.strip()
|
|
|
|
# --- Replicate lines 144-147: MESSAGING_CWD fallback ---
|
|
configured_cwd = env.get("TERMINAL_CWD", "")
|
|
if not configured_cwd or configured_cwd in (".", "auto", "cwd"):
|
|
messaging_cwd = env.get("MESSAGING_CWD") or "/root" # Path.home() for root
|
|
env["TERMINAL_CWD"] = messaging_cwd
|
|
|
|
return env
|
|
|
|
|
|
class TestTopLevelCwdAlias:
|
|
"""Top-level `cwd:` should be treated as `terminal.cwd`."""
|
|
|
|
def test_top_level_cwd_sets_terminal_cwd(self):
|
|
cfg = {"cwd": "/home/hermes/projects"}
|
|
result = _simulate_config_bridge(cfg)
|
|
assert result["TERMINAL_CWD"] == "/home/hermes/projects"
|
|
|
|
def test_top_level_backend_sets_terminal_env(self):
|
|
cfg = {"backend": "docker"}
|
|
result = _simulate_config_bridge(cfg)
|
|
assert result["TERMINAL_ENV"] == "docker"
|
|
|
|
def test_top_level_cwd_and_backend(self):
|
|
cfg = {"backend": "local", "cwd": "/home/hermes/projects"}
|
|
result = _simulate_config_bridge(cfg)
|
|
assert result["TERMINAL_CWD"] == "/home/hermes/projects"
|
|
assert result["TERMINAL_ENV"] == "local"
|
|
|
|
def test_nested_terminal_takes_precedence_over_top_level(self):
|
|
"""terminal.cwd should win over top-level cwd."""
|
|
cfg = {
|
|
"cwd": "/should/not/use",
|
|
"terminal": {"cwd": "/home/hermes/real"},
|
|
}
|
|
result = _simulate_config_bridge(cfg)
|
|
assert result["TERMINAL_CWD"] == "/home/hermes/real"
|
|
|
|
def test_nested_terminal_backend_takes_precedence(self):
|
|
cfg = {
|
|
"backend": "should-not-use",
|
|
"terminal": {"backend": "docker"},
|
|
}
|
|
result = _simulate_config_bridge(cfg)
|
|
assert result["TERMINAL_ENV"] == "docker"
|
|
|
|
def test_no_cwd_falls_back_to_messaging_cwd(self):
|
|
cfg = {}
|
|
result = _simulate_config_bridge(cfg, {"MESSAGING_CWD": "/home/hermes/projects"})
|
|
assert result["TERMINAL_CWD"] == "/home/hermes/projects"
|
|
|
|
def test_no_cwd_no_messaging_cwd_falls_back_to_home(self):
|
|
cfg = {}
|
|
result = _simulate_config_bridge(cfg)
|
|
assert result["TERMINAL_CWD"] == "/root" # Path.home() for root user
|
|
|
|
def test_dot_cwd_triggers_messaging_fallback(self):
|
|
"""cwd: '.' should trigger MESSAGING_CWD fallback."""
|
|
cfg = {"cwd": "."}
|
|
result = _simulate_config_bridge(cfg, {"MESSAGING_CWD": "/home/hermes"})
|
|
# "." is stripped but truthy, so it gets set as TERMINAL_CWD
|
|
# Then the MESSAGING_CWD fallback does NOT trigger since TERMINAL_CWD
|
|
# is set and not in (".", "auto", "cwd").
|
|
# Wait — "." IS in the fallback list! So this should fall through.
|
|
# Actually the alias sets it to ".", then the messaging fallback
|
|
# checks if it's in (".", "auto", "cwd") and overrides.
|
|
assert result["TERMINAL_CWD"] == "/home/hermes"
|
|
|
|
def test_auto_cwd_triggers_messaging_fallback(self):
|
|
cfg = {"cwd": "auto"}
|
|
result = _simulate_config_bridge(cfg, {"MESSAGING_CWD": "/home/hermes"})
|
|
assert result["TERMINAL_CWD"] == "/home/hermes"
|
|
|
|
def test_empty_cwd_ignored(self):
|
|
cfg = {"cwd": ""}
|
|
result = _simulate_config_bridge(cfg, {"MESSAGING_CWD": "/home/hermes"})
|
|
assert result["TERMINAL_CWD"] == "/home/hermes"
|
|
|
|
def test_whitespace_only_cwd_ignored(self):
|
|
cfg = {"cwd": " "}
|
|
result = _simulate_config_bridge(cfg, {"MESSAGING_CWD": "/fallback"})
|
|
assert result["TERMINAL_CWD"] == "/fallback"
|
|
|
|
def test_messaging_cwd_env_var_works(self):
|
|
"""MESSAGING_CWD in initial env should be picked up as fallback."""
|
|
cfg = {}
|
|
result = _simulate_config_bridge(cfg, {"MESSAGING_CWD": "/home/hermes/projects"})
|
|
assert result["TERMINAL_CWD"] == "/home/hermes/projects"
|
|
|
|
def test_top_level_cwd_beats_messaging_cwd(self):
|
|
"""Explicit top-level cwd should take precedence over MESSAGING_CWD."""
|
|
cfg = {"cwd": "/from/config"}
|
|
result = _simulate_config_bridge(cfg, {"MESSAGING_CWD": "/from/env"})
|
|
assert result["TERMINAL_CWD"] == "/from/config"
|
|
|
|
|
|
class TestNestedTerminalCwdPlaceholderSkip:
|
|
"""terminal.cwd placeholder values must not clobber TERMINAL_CWD.
|
|
|
|
When config.yaml has terminal.cwd: "." (or "auto"/"cwd"), the gateway
|
|
config bridge should NOT write that placeholder to TERMINAL_CWD.
|
|
This prevents .env or MESSAGING_CWD values from being overwritten.
|
|
See issues #10225, #4672, #10817.
|
|
"""
|
|
|
|
def test_terminal_dot_cwd_does_not_clobber_env(self):
|
|
"""terminal.cwd: '.' should not overwrite a pre-set TERMINAL_CWD."""
|
|
cfg = {"terminal": {"cwd": "."}}
|
|
result = _simulate_config_bridge(cfg, {"TERMINAL_CWD": "/my/project"})
|
|
assert result["TERMINAL_CWD"] == "/my/project"
|
|
|
|
def test_terminal_auto_cwd_does_not_clobber_env(self):
|
|
cfg = {"terminal": {"cwd": "auto"}}
|
|
result = _simulate_config_bridge(cfg, {"TERMINAL_CWD": "/my/project"})
|
|
assert result["TERMINAL_CWD"] == "/my/project"
|
|
|
|
def test_terminal_cwd_keyword_does_not_clobber_env(self):
|
|
cfg = {"terminal": {"cwd": "cwd"}}
|
|
result = _simulate_config_bridge(cfg, {"TERMINAL_CWD": "/my/project"})
|
|
assert result["TERMINAL_CWD"] == "/my/project"
|
|
|
|
def test_terminal_explicit_cwd_does_override(self):
|
|
"""terminal.cwd: '/explicit/path' SHOULD override TERMINAL_CWD."""
|
|
cfg = {"terminal": {"cwd": "/explicit/path"}}
|
|
result = _simulate_config_bridge(cfg, {"TERMINAL_CWD": "/old/value"})
|
|
assert result["TERMINAL_CWD"] == "/explicit/path"
|
|
|
|
def test_terminal_dot_cwd_falls_back_to_messaging_cwd(self):
|
|
"""terminal.cwd: '.' with no TERMINAL_CWD should fall to MESSAGING_CWD."""
|
|
cfg = {"terminal": {"cwd": "."}}
|
|
result = _simulate_config_bridge(cfg, {"MESSAGING_CWD": "/from/env"})
|
|
assert result["TERMINAL_CWD"] == "/from/env"
|
|
|
|
def test_terminal_dot_cwd_and_messaging_cwd_both_set(self):
|
|
"""Pre-set TERMINAL_CWD from .env wins over terminal.cwd: '.'."""
|
|
cfg = {"terminal": {"cwd": ".", "backend": "local"}}
|
|
result = _simulate_config_bridge(cfg, {
|
|
"TERMINAL_CWD": "/my/project",
|
|
"MESSAGING_CWD": "/fallback",
|
|
})
|
|
assert result["TERMINAL_CWD"] == "/my/project"
|
|
|
|
def test_non_cwd_terminal_keys_still_bridge(self):
|
|
"""Other terminal config keys (backend, timeout) should still bridge normally."""
|
|
cfg = {"terminal": {"cwd": ".", "backend": "docker", "timeout": "300"}}
|
|
result = _simulate_config_bridge(cfg, {"MESSAGING_CWD": "/from/env"})
|
|
assert result["TERMINAL_ENV"] == "docker"
|
|
assert result["TERMINAL_TIMEOUT"] == "300"
|
|
assert result["TERMINAL_CWD"] == "/from/env"
|