mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-15 09:21:36 +00:00
* feat(cli): add --safe-mode troubleshooting flag Inspired by Claude Code v2.1.169 (June 2026): run Hermes with all customizations disabled to isolate setup problems from product bugs. --safe-mode implies --ignore-user-config and --ignore-rules, and additionally skips plugin discovery (hermes_cli/plugins.py) and MCP server loading (tools/mcp_tool.py) via the internal HERMES_SAFE_MODE env bridge. * fix(desktop): keep composer usable during reconnect
130 lines
4.4 KiB
Python
130 lines
4.4 KiB
Python
"""Tests for `hermes chat --safe-mode` — pristine troubleshooting runs.
|
|
|
|
Inspired by Claude Code v2.1.169's ``--safe-mode`` flag (June 2026), which
|
|
disables all customizations (CLAUDE.md, plugins, skills, hooks, MCP) for
|
|
troubleshooting. The Hermes equivalent:
|
|
|
|
* implies ``--ignore-user-config`` (built-in config defaults)
|
|
* implies ``--ignore-rules`` (no AGENTS.md/memory/preloaded-skill injection)
|
|
* skips plugin discovery entirely (``hermes_cli.plugins``)
|
|
* loads zero MCP servers (``tools.mcp_tool._load_mcp_config``)
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
|
|
import pytest
|
|
|
|
|
|
_VARS = ("HERMES_SAFE_MODE", "HERMES_IGNORE_USER_CONFIG", "HERMES_IGNORE_RULES")
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _clean_env(monkeypatch):
|
|
for var in _VARS:
|
|
monkeypatch.delenv(var, raising=False)
|
|
yield
|
|
for var in _VARS:
|
|
os.environ.pop(var, None)
|
|
|
|
|
|
class TestSafeModeEnvWiring:
|
|
"""cmd_chat must translate --safe-mode into the three env gates."""
|
|
|
|
def test_safe_mode_sets_all_gates(self):
|
|
# Mirrors the cmd_chat logic in hermes_cli/main.py.
|
|
class Args:
|
|
safe_mode = True
|
|
|
|
args = Args()
|
|
if getattr(args, "safe_mode", False):
|
|
os.environ["HERMES_SAFE_MODE"] = "1"
|
|
os.environ["HERMES_IGNORE_USER_CONFIG"] = "1"
|
|
os.environ["HERMES_IGNORE_RULES"] = "1"
|
|
|
|
assert os.environ.get("HERMES_SAFE_MODE") == "1"
|
|
assert os.environ.get("HERMES_IGNORE_USER_CONFIG") == "1"
|
|
assert os.environ.get("HERMES_IGNORE_RULES") == "1"
|
|
|
|
|
|
class TestSafeModePluginDiscovery:
|
|
"""Plugin discovery must be a no-op under HERMES_SAFE_MODE=1."""
|
|
|
|
def test_discovery_skipped(self, monkeypatch):
|
|
monkeypatch.setenv("HERMES_SAFE_MODE", "1")
|
|
from hermes_cli.plugins import PluginManager
|
|
|
|
mgr = PluginManager()
|
|
called = []
|
|
monkeypatch.setattr(
|
|
mgr, "_discover_and_load_inner", lambda: called.append(True)
|
|
)
|
|
mgr.discover_and_load()
|
|
assert called == [] # inner sweep never ran
|
|
assert mgr._discovered is True # registry settled as clean-empty
|
|
assert mgr._plugins == {}
|
|
|
|
def test_discovery_runs_without_safe_mode(self, monkeypatch):
|
|
monkeypatch.delenv("HERMES_SAFE_MODE", raising=False)
|
|
from hermes_cli.plugins import PluginManager
|
|
|
|
mgr = PluginManager()
|
|
called = []
|
|
monkeypatch.setattr(
|
|
mgr, "_discover_and_load_inner", lambda: called.append(True)
|
|
)
|
|
mgr.discover_and_load()
|
|
assert called == [True]
|
|
|
|
|
|
class TestSafeModeMCP:
|
|
"""_load_mcp_config must return no servers under HERMES_SAFE_MODE=1."""
|
|
|
|
def test_mcp_servers_empty(self, monkeypatch):
|
|
monkeypatch.setenv("HERMES_SAFE_MODE", "1")
|
|
from tools.mcp_tool import _load_mcp_config
|
|
|
|
with pytest.MonkeyPatch.context() as mp:
|
|
mp.setattr(
|
|
"hermes_cli.config.load_config",
|
|
lambda: {"mcp_servers": {"github": {"url": "https://example.com/mcp"}}},
|
|
)
|
|
assert _load_mcp_config() == {}
|
|
|
|
def test_mcp_servers_load_without_safe_mode(self, monkeypatch):
|
|
monkeypatch.delenv("HERMES_SAFE_MODE", raising=False)
|
|
from tools.mcp_tool import _load_mcp_config
|
|
|
|
with pytest.MonkeyPatch.context() as mp:
|
|
mp.setattr(
|
|
"hermes_cli.config.load_config",
|
|
lambda: {"mcp_servers": {"github": {"url": "https://example.com/mcp"}}},
|
|
)
|
|
servers = _load_mcp_config()
|
|
assert "github" in servers
|
|
|
|
|
|
class TestSafeModeParser:
|
|
"""--safe-mode must parse on both the root parser and `hermes chat`."""
|
|
|
|
def test_chat_subcommand_accepts_flag(self):
|
|
from hermes_cli._parser import build_top_level_parser
|
|
|
|
parser, _subparsers, _chat = build_top_level_parser()
|
|
args = parser.parse_args(["chat", "--safe-mode"])
|
|
assert getattr(args, "safe_mode", False) is True
|
|
|
|
def test_root_parser_accepts_flag(self):
|
|
from hermes_cli._parser import build_top_level_parser
|
|
|
|
parser, _subparsers, _chat = build_top_level_parser()
|
|
args = parser.parse_args(["--safe-mode"])
|
|
assert getattr(args, "safe_mode", False) is True
|
|
|
|
def test_default_is_off(self):
|
|
from hermes_cli._parser import build_top_level_parser
|
|
|
|
parser, _subparsers, _chat = build_top_level_parser()
|
|
args = parser.parse_args(["chat"])
|
|
assert getattr(args, "safe_mode", False) is False
|