hermes-agent/tests/hermes_cli/test_safe_mode.py
Teknium 8cf9d8689d
fix(desktop): keep composer usable during reconnect (#45488)
* 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
2026-06-13 02:36:09 -07:00

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