refactor(restructure): rewrite all imports for hermes_agent package

Rewrite all import statements, patch() targets, sys.modules keys,
importlib.import_module() strings, and subprocess -m references to use
hermes_agent.* paths.

Strip sys.path.insert hacks from production code (rely on editable install).
Update COMPONENT_PREFIXES for logger filtering.
Fix 3 hardcoded getLogger() calls to use __name__.
Update transport and tool registry discovery paths.
Update plugin module path strings.
Add legacy process-name patterns for gateway PID detection.
Add main() to skills_sync for console_script entry point.
Fix _get_bundled_dir() path traversal after move.

Part of #14182, #14183
This commit is contained in:
alt-glitch 2026-04-23 08:35:34 +05:30
parent 65ca3ba93b
commit 4b16341975
898 changed files with 12494 additions and 12019 deletions

View file

@ -23,7 +23,7 @@ def session_db(tmp_path):
"""Create a real SessionDB for testing."""
os.environ["HERMES_HOME"] = str(tmp_path / ".hermes")
os.makedirs(tmp_path / ".hermes", exist_ok=True)
from hermes_state import SessionDB
from hermes_agent.state import SessionDB
db = SessionDB(db_path=tmp_path / ".hermes" / "test_sessions.db")
yield db
db.close()
@ -68,7 +68,7 @@ class TestBranchCommandCLI:
def test_branch_creates_new_session(self, cli_instance, session_db):
"""Branching should create a new session in the DB."""
from cli import HermesCLI
from hermes_agent.cli.repl import HermesCLI
# Call the real method on the mock, using the real implementation
HermesCLI._handle_branch_command(cli_instance, "/branch")
@ -80,7 +80,7 @@ class TestBranchCommandCLI:
def test_branch_copies_history(self, cli_instance, session_db):
"""Branching should copy all messages to the new session."""
from cli import HermesCLI
from hermes_agent.cli.repl import HermesCLI
HermesCLI._handle_branch_command(cli_instance, "/branch")
@ -89,7 +89,7 @@ class TestBranchCommandCLI:
def test_branch_preserves_parent_link(self, cli_instance, session_db):
"""The new session should reference the original as parent."""
from cli import HermesCLI
from hermes_agent.cli.repl import HermesCLI
original_id = cli_instance.session_id
HermesCLI._handle_branch_command(cli_instance, "/branch")
@ -99,7 +99,7 @@ class TestBranchCommandCLI:
def test_branch_ends_original_session(self, cli_instance, session_db):
"""The original session should be marked as ended with 'branched' reason."""
from cli import HermesCLI
from hermes_agent.cli.repl import HermesCLI
original_id = cli_instance.session_id
HermesCLI._handle_branch_command(cli_instance, "/branch")
@ -109,7 +109,7 @@ class TestBranchCommandCLI:
def test_branch_with_custom_name(self, cli_instance, session_db):
"""Custom branch name should be used as the title."""
from cli import HermesCLI
from hermes_agent.cli.repl import HermesCLI
HermesCLI._handle_branch_command(cli_instance, "/branch refactor approach")
@ -118,7 +118,7 @@ class TestBranchCommandCLI:
def test_branch_auto_title_lineage(self, cli_instance, session_db):
"""Without a name, branch should auto-generate a title from the parent's title."""
from cli import HermesCLI
from hermes_agent.cli.repl import HermesCLI
HermesCLI._handle_branch_command(cli_instance, "/branch")
@ -127,7 +127,7 @@ class TestBranchCommandCLI:
def test_branch_empty_conversation(self, cli_instance, session_db):
"""Branching with no history should show an error."""
from cli import HermesCLI
from hermes_agent.cli.repl import HermesCLI
cli_instance.conversation_history = []
HermesCLI._handle_branch_command(cli_instance, "/branch")
@ -137,7 +137,7 @@ class TestBranchCommandCLI:
def test_branch_no_session_db(self, cli_instance):
"""Branching without a session DB should show an error."""
from cli import HermesCLI
from hermes_agent.cli.repl import HermesCLI
cli_instance._session_db = None
HermesCLI._handle_branch_command(cli_instance, "/branch")
@ -147,7 +147,7 @@ class TestBranchCommandCLI:
def test_branch_syncs_agent(self, cli_instance, session_db):
"""If an agent is active, branch should sync it to the new session."""
from cli import HermesCLI
from hermes_agent.cli.repl import HermesCLI
agent = MagicMock()
agent._last_flushed_db_idx = 0
@ -162,7 +162,7 @@ class TestBranchCommandCLI:
def test_branch_sets_resumed_flag(self, cli_instance, session_db):
"""Branch should set _resumed=True to prevent auto-title generation."""
from cli import HermesCLI
from hermes_agent.cli.repl import HermesCLI
HermesCLI._handle_branch_command(cli_instance, "/branch")
@ -170,7 +170,7 @@ class TestBranchCommandCLI:
def test_fork_alias(self):
"""The /fork alias should resolve to 'branch'."""
from hermes_cli.commands import resolve_command
from hermes_agent.cli.commands import resolve_command
result = resolve_command("fork")
assert result is not None
assert result.name == "branch"
@ -181,18 +181,18 @@ class TestBranchCommandDef:
def test_branch_in_registry(self):
"""The branch command should be in the command registry."""
from hermes_cli.commands import COMMAND_REGISTRY
from hermes_agent.cli.commands import COMMAND_REGISTRY
names = [c.name for c in COMMAND_REGISTRY]
assert "branch" in names
def test_branch_has_fork_alias(self):
"""The branch command should have 'fork' as an alias."""
from hermes_cli.commands import COMMAND_REGISTRY
from hermes_agent.cli.commands import COMMAND_REGISTRY
branch = next(c for c in COMMAND_REGISTRY if c.name == "branch")
assert "fork" in branch.aliases
def test_branch_in_session_category(self):
"""The branch command should be in the Session category."""
from hermes_cli.commands import COMMAND_REGISTRY
from hermes_agent.cli.commands import COMMAND_REGISTRY
branch = next(c for c in COMMAND_REGISTRY if c.name == "branch")
assert branch.category == "Session"

View file

@ -4,8 +4,8 @@ import time
from types import SimpleNamespace
from unittest.mock import MagicMock, patch
import cli as cli_module
from cli import HermesCLI
import hermes_agent.cli.repl as cli_module
from hermes_agent.cli.repl import HermesCLI
class _FakeBuffer:
@ -169,7 +169,7 @@ class TestCliApprovalUi:
# Simulate a compact terminal where the old unbounded panel would overflow.
import shutil as _shutil
with patch("cli.shutil.get_terminal_size",
with patch("hermes_agent.cli.repl.shutil.get_terminal_size",
return_value=_shutil.os.terminal_size((100, 20))):
fragments = cli._get_approval_display_fragments()
@ -208,7 +208,7 @@ class TestCliApprovalUi:
import shutil as _shutil
with patch("cli.shutil.get_terminal_size",
with patch("hermes_agent.cli.repl.shutil.get_terminal_size",
return_value=_shutil.os.terminal_size((100, 12))):
fragments = cli._get_approval_display_fragments()
@ -241,7 +241,7 @@ class TestCliApprovalUi:
import shutil as _shutil
with patch("cli.shutil.get_terminal_size",
with patch("hermes_agent.cli.repl.shutil.get_terminal_size",
return_value=_shutil.os.terminal_size((100, 24))):
fragments = cli._get_approval_display_fragments()
@ -273,7 +273,7 @@ class TestApprovalCallbackThreadLocalWiring:
If this ever starts passing as "visible", the thread-local isolation
is gone and the ACP race GHSA-qg5c-hvr5-hjgr may be back.
"""
from tools.terminal_tool import (
from hermes_agent.tools.terminal import (
set_approval_callback,
_get_approval_callback,
)
@ -301,7 +301,7 @@ class TestApprovalCallbackThreadLocalWiring:
This is exactly what cli.py's run_agent() closure does. If this test
fails, the CLI approval prompt freeze (#13617) has regressed.
"""
from tools.terminal_tool import (
from hermes_agent.tools.terminal import (
set_approval_callback,
set_sudo_password_callback,
_get_approval_callback,

View file

@ -10,7 +10,7 @@ from unittest.mock import MagicMock, patch
import pytest
from cli import HermesCLI
from hermes_agent.cli.repl import HermesCLI
def _make_cli():

View file

@ -3,7 +3,7 @@
import os
from unittest.mock import patch
from cli import HermesCLI
from hermes_agent.cli.repl import HermesCLI
def _assert_chrome_debug_cmd(cmd, expected_chrome, expected_port):
@ -26,8 +26,8 @@ class TestChromeDebugLaunch:
captured["kwargs"] = kwargs
return object()
with patch("cli.shutil.which", side_effect=lambda name: r"C:\Chrome\chrome.exe" if name == "chrome.exe" else None), \
patch("cli.os.path.isfile", side_effect=lambda path: path == r"C:\Chrome\chrome.exe"), \
with patch("hermes_agent.cli.repl.shutil.which", side_effect=lambda name: r"C:\Chrome\chrome.exe" if name == "chrome.exe" else None), \
patch("hermes_agent.cli.repl.os.path.isfile", side_effect=lambda path: path == r"C:\Chrome\chrome.exe"), \
patch("subprocess.Popen", side_effect=fake_popen):
assert HermesCLI._try_launch_chrome_debug(9333, "Windows") is True
@ -49,8 +49,8 @@ class TestChromeDebugLaunch:
monkeypatch.delenv("ProgramFiles(x86)", raising=False)
monkeypatch.delenv("LOCALAPPDATA", raising=False)
with patch("cli.shutil.which", return_value=None), \
patch("cli.os.path.isfile", side_effect=lambda path: path == installed), \
with patch("hermes_agent.cli.repl.shutil.which", return_value=None), \
patch("hermes_agent.cli.repl.os.path.isfile", side_effect=lambda path: path == installed), \
patch("subprocess.Popen", side_effect=fake_popen):
assert HermesCLI._try_launch_chrome_debug(9222, "Windows") is True

View file

@ -18,12 +18,12 @@ def _isolate(tmp_path, monkeypatch):
@pytest.fixture
def cli_obj(_isolate):
"""Create a minimal HermesCLI instance for banner testing."""
with patch("cli.load_cli_config", return_value={
with patch("hermes_agent.cli.repl.load_cli_config", return_value={
"display": {"tool_progress": "new"},
"terminal": {},
}), patch("cli.get_tool_definitions", return_value=[]), \
patch("cli.build_welcome_banner"):
from cli import HermesCLI
}), patch("hermes_agent.cli.repl.get_tool_definitions", return_value=[]), \
patch("hermes_agent.cli.repl.build_welcome_banner"):
from hermes_agent.cli.repl import HermesCLI
obj = HermesCLI.__new__(HermesCLI)
obj.model = "test-model"
obj.enabled_toolsets = ["hermes-core"]
@ -47,8 +47,8 @@ class TestLowContextWarning:
def test_no_warning_for_normal_context(self, cli_obj):
"""No warning when context is 32k+."""
cli_obj.agent.context_compressor.context_length = 32768
with patch("cli.get_tool_definitions", return_value=[]), \
patch("cli.build_welcome_banner"):
with patch("hermes_agent.cli.repl.get_tool_definitions", return_value=[]), \
patch("hermes_agent.cli.repl.build_welcome_banner"):
cli_obj.show_banner()
# Check that no yellow warning was printed
@ -59,8 +59,8 @@ class TestLowContextWarning:
def test_warning_for_low_context(self, cli_obj):
"""Warning shown when context is 4096 (Ollama default)."""
cli_obj.agent.context_compressor.context_length = 4096
with patch("cli.get_tool_definitions", return_value=[]), \
patch("cli.build_welcome_banner"):
with patch("hermes_agent.cli.repl.get_tool_definitions", return_value=[]), \
patch("hermes_agent.cli.repl.build_welcome_banner"):
cli_obj.show_banner()
calls = [str(c) for c in cli_obj.console.print.call_args_list]
@ -71,8 +71,8 @@ class TestLowContextWarning:
def test_warning_for_2048_context(self, cli_obj):
"""Warning shown for 2048 tokens (common LM Studio default)."""
cli_obj.agent.context_compressor.context_length = 2048
with patch("cli.get_tool_definitions", return_value=[]), \
patch("cli.build_welcome_banner"):
with patch("hermes_agent.cli.repl.get_tool_definitions", return_value=[]), \
patch("hermes_agent.cli.repl.build_welcome_banner"):
cli_obj.show_banner()
calls = [str(c) for c in cli_obj.console.print.call_args_list]
@ -82,8 +82,8 @@ class TestLowContextWarning:
def test_no_warning_at_boundary(self, cli_obj):
"""No warning at exactly 8192 — 8192 is borderline but included in warning."""
cli_obj.agent.context_compressor.context_length = 8192
with patch("cli.get_tool_definitions", return_value=[]), \
patch("cli.build_welcome_banner"):
with patch("hermes_agent.cli.repl.get_tool_definitions", return_value=[]), \
patch("hermes_agent.cli.repl.build_welcome_banner"):
cli_obj.show_banner()
calls = [str(c) for c in cli_obj.console.print.call_args_list]
@ -93,8 +93,8 @@ class TestLowContextWarning:
def test_no_warning_above_boundary(self, cli_obj):
"""No warning at 16384."""
cli_obj.agent.context_compressor.context_length = 16384
with patch("cli.get_tool_definitions", return_value=[]), \
patch("cli.build_welcome_banner"):
with patch("hermes_agent.cli.repl.get_tool_definitions", return_value=[]), \
patch("hermes_agent.cli.repl.build_welcome_banner"):
cli_obj.show_banner()
calls = [str(c) for c in cli_obj.console.print.call_args_list]
@ -105,8 +105,8 @@ class TestLowContextWarning:
"""Ollama-specific fix shown when port 11434 detected."""
cli_obj.agent.context_compressor.context_length = 4096
cli_obj.base_url = "http://localhost:11434/v1"
with patch("cli.get_tool_definitions", return_value=[]), \
patch("cli.build_welcome_banner"):
with patch("hermes_agent.cli.repl.get_tool_definitions", return_value=[]), \
patch("hermes_agent.cli.repl.build_welcome_banner"):
cli_obj.show_banner()
calls = [str(c) for c in cli_obj.console.print.call_args_list]
@ -117,8 +117,8 @@ class TestLowContextWarning:
"""LM Studio-specific fix shown when port 1234 detected."""
cli_obj.agent.context_compressor.context_length = 2048
cli_obj.base_url = "http://localhost:1234/v1"
with patch("cli.get_tool_definitions", return_value=[]), \
patch("cli.build_welcome_banner"):
with patch("hermes_agent.cli.repl.get_tool_definitions", return_value=[]), \
patch("hermes_agent.cli.repl.build_welcome_banner"):
cli_obj.show_banner()
calls = [str(c) for c in cli_obj.console.print.call_args_list]
@ -129,8 +129,8 @@ class TestLowContextWarning:
"""Generic fix shown for unknown servers."""
cli_obj.agent.context_compressor.context_length = 4096
cli_obj.base_url = "http://localhost:8080/v1"
with patch("cli.get_tool_definitions", return_value=[]), \
patch("cli.build_welcome_banner"):
with patch("hermes_agent.cli.repl.get_tool_definitions", return_value=[]), \
patch("hermes_agent.cli.repl.build_welcome_banner"):
cli_obj.show_banner()
calls = [str(c) for c in cli_obj.console.print.call_args_list]
@ -140,8 +140,8 @@ class TestLowContextWarning:
def test_no_warning_when_no_context_length(self, cli_obj):
"""No warning when context length is not yet known."""
cli_obj.agent.context_compressor.context_length = None
with patch("cli.get_tool_definitions", return_value=[]), \
patch("cli.build_welcome_banner"):
with patch("hermes_agent.cli.repl.get_tool_definitions", return_value=[]), \
patch("hermes_agent.cli.repl.build_welcome_banner"):
cli_obj.show_banner()
calls = [str(c) for c in cli_obj.console.print.call_args_list]
@ -153,7 +153,7 @@ class TestLowContextWarning:
cli_obj.agent.context_compressor.context_length = 4096
with patch("shutil.get_terminal_size", return_value=os.terminal_size((70, 40))), \
patch("cli._build_compact_banner", return_value="compact banner"):
patch("hermes_agent.cli.repl._build_compact_banner", return_value="compact banner"):
cli_obj.show_banner()
calls = [str(c) for c in cli_obj.console.print.call_args_list]

View file

@ -2,7 +2,7 @@
from unittest.mock import MagicMock, patch
from cli import HermesCLI
from hermes_agent.cli.repl import HermesCLI
def _make_cli() -> HermesCLI:
@ -64,7 +64,7 @@ def test_copy_invalid_index_does_not_copy():
cli_obj = _make_cli()
cli_obj.conversation_history = [{"role": "assistant", "content": "only"}]
with patch.object(cli_obj, "_write_osc52_clipboard") as mock_copy, patch("cli._cprint") as mock_print:
with patch.object(cli_obj, "_write_osc52_clipboard") as mock_copy, patch("hermes_agent.cli.repl._cprint") as mock_print:
cli_obj.process_command("/copy 99")
mock_copy.assert_not_called()

View file

@ -49,7 +49,7 @@ def _make_cli(**kwargs):
with patch.dict(sys.modules, prompt_toolkit_stubs), patch.dict(
"os.environ", clean_env, clear=False
):
import cli as _cli_mod
import hermes_agent.cli.repl as _cli_mod
_cli_mod = importlib.reload(_cli_mod)
with patch.object(_cli_mod, "get_tool_definitions", return_value=[]), patch.dict(

View file

@ -2,7 +2,7 @@
from unittest.mock import patch
from cli import HermesCLI
from hermes_agent.cli.repl import HermesCLI
class _FakeBuffer:
@ -43,7 +43,7 @@ def test_open_external_editor_uses_prompt_toolkit_buffer_editor():
def test_open_external_editor_rejects_when_no_tui():
cli_obj = _make_cli(with_app=False)
with patch("cli._cprint") as mock_cprint:
with patch("hermes_agent.cli.repl._cprint") as mock_cprint:
assert cli_obj._open_external_editor() is False
assert mock_cprint.called
@ -54,7 +54,7 @@ def test_open_external_editor_rejects_modal_prompts():
cli_obj = _make_cli()
cli_obj._approval_state = {"selected": 0}
with patch("cli._cprint") as mock_cprint:
with patch("hermes_agent.cli.repl._cprint") as mock_cprint:
assert cli_obj._open_external_editor() is False
assert mock_cprint.called

View file

@ -7,7 +7,7 @@ from pathlib import Path
import pytest
from cli import _detect_file_drop
from hermes_agent.cli.repl import _detect_file_drop
# ---------------------------------------------------------------------------

View file

@ -1,7 +1,7 @@
from pathlib import Path
from unittest.mock import patch
from cli import (
from hermes_agent.cli.repl import (
HermesCLI,
_collect_query_images,
_format_image_attachment_badges,
@ -26,7 +26,7 @@ class TestImageCommand:
img = _make_image(tmp_path / "photo.png")
cli_obj = _make_cli()
with patch("cli._cprint"):
with patch("hermes_agent.cli.repl._cprint"):
cli_obj._handle_image_command(f"/image {img}")
assert cli_obj._attached_images == [img]
@ -35,7 +35,7 @@ class TestImageCommand:
img = _make_image(tmp_path / "my photo.png")
cli_obj = _make_cli()
with patch("cli._cprint"):
with patch("hermes_agent.cli.repl._cprint"):
cli_obj._handle_image_command(f'/image "{img}"')
assert cli_obj._attached_images == [img]
@ -45,7 +45,7 @@ class TestImageCommand:
file_path.write_text("hello\n", encoding="utf-8")
cli_obj = _make_cli()
with patch("cli._cprint") as mock_print:
with patch("hermes_agent.cli.repl._cprint") as mock_print:
cli_obj._handle_image_command(f"/image {file_path}")
assert cli_obj._attached_images == []
@ -84,7 +84,7 @@ class TestCollectQueryImages:
class TestTermuxImageHints:
def test_termux_example_image_path_prefers_real_shared_storage_root(self, monkeypatch):
existing = {"/sdcard", "/storage/emulated/0"}
monkeypatch.setattr("cli.os.path.isdir", lambda path: path in existing)
monkeypatch.setattr("hermes_agent.cli.repl.os.path.isdir", lambda path: path in existing)
hint = _termux_example_image_path()

View file

@ -5,8 +5,6 @@ import os
import sys
from unittest.mock import MagicMock, patch
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
def _make_cli(env_overrides=None, config_overrides=None, **kwargs):
"""Create a HermesCLI instance with minimal mocking."""
@ -46,7 +44,7 @@ def _make_cli(env_overrides=None, config_overrides=None, **kwargs):
}
with patch.dict(sys.modules, prompt_toolkit_stubs), \
patch.dict("os.environ", clean_env, clear=False):
import cli as _cli_mod
import hermes_agent.cli.repl as _cli_mod
_cli_mod = importlib.reload(_cli_mod)
with patch.object(_cli_mod, "get_tool_definitions", return_value=[]), \
patch.dict(_cli_mod.__dict__, {"CLI_CONFIG": _clean_config}):
@ -266,7 +264,7 @@ class TestRootLevelProviderOverride:
},
}))
import cli
import hermes_agent.cli.repl
monkeypatch.setattr(cli, "_hermes_home", hermes_home)
cfg = cli.load_cli_config()
@ -289,7 +287,7 @@ class TestRootLevelProviderOverride:
},
}))
import cli
import hermes_agent.cli.repl
monkeypatch.setattr(cli, "_hermes_home", hermes_home)
cfg = cli.load_cli_config()
@ -298,7 +296,7 @@ class TestRootLevelProviderOverride:
def test_normalize_root_model_keys_moves_to_model(self):
"""_normalize_root_model_keys migrates root keys into model section."""
from hermes_cli.config import _normalize_root_model_keys
from hermes_agent.cli.config import _normalize_root_model_keys
config = {
"provider": "opencode-go",
@ -317,7 +315,7 @@ class TestRootLevelProviderOverride:
def test_normalize_root_model_keys_does_not_override_existing(self):
"""Existing model.provider is never overridden by root-level key."""
from hermes_cli.config import _normalize_root_model_keys
from hermes_agent.cli.config import _normalize_root_model_keys
config = {
"provider": "stale-provider",

View file

@ -18,7 +18,7 @@ import time
import unittest
from unittest.mock import MagicMock, patch, PropertyMock
from tools.interrupt import set_interrupt, is_interrupted
from hermes_agent.tools.interrupt import set_interrupt, is_interrupted
class TestCLISubagentInterrupt(unittest.TestCase):
@ -32,7 +32,7 @@ class TestCLISubagentInterrupt(unittest.TestCase):
def test_full_delegate_interrupt_flow(self):
"""Full integration: parent runs delegate_task, main thread interrupts."""
from run_agent import AIAgent
from hermes_agent.agent.loop import AIAgent
interrupt_detected = threading.Event()
child_started = threading.Event()
@ -98,8 +98,8 @@ class TestCLISubagentInterrupt(unittest.TestCase):
}
# Patch AIAgent to use our mock
from tools.delegate_tool import _run_single_child
from run_agent import IterationBudget
from hermes_agent.tools.delegate import _run_single_child
from hermes_agent.agent.loop import IterationBudget
parent.iteration_budget = IterationBudget(max_total=100)
@ -109,7 +109,7 @@ class TestCLISubagentInterrupt(unittest.TestCase):
def run_delegate():
try:
with patch('run_agent.AIAgent') as MockAgent:
with patch('hermes_agent.agent.loop.AIAgent') as MockAgent:
mock_instance = MagicMock()
mock_instance._interrupt_requested = False
mock_instance._interrupt_message = None

View file

@ -2,7 +2,7 @@
from unittest.mock import patch
from cli import HermesCLI
from hermes_agent.cli.repl import HermesCLI
class TestCLILoadingIndicator:

View file

@ -3,7 +3,7 @@ from io import StringIO
from rich.console import Console
from rich.markdown import Markdown
from cli import _render_final_assistant_content
from hermes_agent.cli.repl import _render_final_assistant_content
def _render_to_text(renderable) -> str:

View file

@ -6,7 +6,7 @@ from unittest.mock import MagicMock, patch
def _make_cli(tmp_path, mcp_servers=None):
"""Create a minimal HermesCLI instance with mocked config."""
import cli as cli_mod
import hermes_agent.cli.repl as cli_mod
obj = object.__new__(cli_mod.HermesCLI)
obj.config = {"mcp_servers": mcp_servers or {}}
obj._agent_running = False
@ -32,7 +32,7 @@ class TestMCPConfigWatch:
"""If mtime and mcp_servers unchanged, _reload_mcp is NOT called."""
obj, cfg_file = _make_cli(tmp_path)
with patch("hermes_cli.config.get_config_path", return_value=cfg_file):
with patch("hermes_agent.cli.config.get_config_path", return_value=cfg_file):
obj._check_config_mcp_changes()
obj._reload_mcp.assert_not_called()
@ -47,7 +47,7 @@ class TestMCPConfigWatch:
# Force mtime to appear changed
obj._config_mtime = 0.0
with patch("hermes_cli.config.get_config_path", return_value=cfg_file):
with patch("hermes_agent.cli.config.get_config_path", return_value=cfg_file):
obj._check_config_mcp_changes()
obj._reload_mcp.assert_not_called()
@ -61,7 +61,7 @@ class TestMCPConfigWatch:
cfg_file.write_text(yaml.dump({"mcp_servers": {"github": {"url": "https://mcp.github.com"}}}))
obj._config_mtime = 0.0 # force stale mtime
with patch("hermes_cli.config.get_config_path", return_value=cfg_file):
with patch("hermes_agent.cli.config.get_config_path", return_value=cfg_file):
obj._check_config_mcp_changes()
obj._reload_mcp.assert_called_once()
@ -75,7 +75,7 @@ class TestMCPConfigWatch:
cfg_file.write_text(yaml.dump({"mcp_servers": {}}))
obj._config_mtime = 0.0
with patch("hermes_cli.config.get_config_path", return_value=cfg_file):
with patch("hermes_agent.cli.config.get_config_path", return_value=cfg_file):
obj._check_config_mcp_changes()
obj._reload_mcp.assert_called_once()
@ -85,7 +85,7 @@ class TestMCPConfigWatch:
obj, cfg_file = _make_cli(tmp_path)
obj._last_config_check = time.monotonic() # just checked
with patch("hermes_cli.config.get_config_path", return_value=cfg_file), \
with patch("hermes_agent.cli.config.get_config_path", return_value=cfg_file), \
patch.object(Path, "stat") as mock_stat:
obj._check_config_mcp_changes()
mock_stat.assert_not_called()
@ -97,7 +97,7 @@ class TestMCPConfigWatch:
obj, cfg_file = _make_cli(tmp_path)
missing = tmp_path / "nonexistent.yaml"
with patch("hermes_cli.config.get_config_path", return_value=missing):
with patch("hermes_agent.cli.config.get_config_path", return_value=missing):
obj._check_config_mcp_changes() # should not raise
obj._reload_mcp.assert_not_called()

View file

@ -8,8 +8,8 @@ import sys
from datetime import timedelta
from unittest.mock import MagicMock, patch
from hermes_state import SessionDB
from tools.todo_tool import TodoStore
from hermes_agent.state import SessionDB
from hermes_agent.tools.todo import TodoStore
class _FakeCompressor:
@ -111,7 +111,7 @@ def _make_cli(env_overrides=None, config_overrides=None, **kwargs):
with patch.dict(sys.modules, prompt_toolkit_stubs), patch.dict(
"os.environ", clean_env, clear=False
):
import cli as _cli_mod
import hermes_agent.cli.repl as _cli_mod
_cli_mod = importlib.reload(_cli_mod)
with patch.object(_cli_mod, "get_tool_definitions", return_value=[]), patch.dict(

View file

@ -2,8 +2,8 @@
from unittest.mock import MagicMock, patch
from agent.skill_commands import scan_skill_commands
from cli import HermesCLI
from hermes_agent.agent.skill_commands import scan_skill_commands
from hermes_agent.cli.repl import HermesCLI
def _make_cli():
@ -38,7 +38,7 @@ class TestCLIPlanCommand:
def test_plan_command_queues_plan_skill_message(self, tmp_path, monkeypatch):
cli_obj = _make_cli()
with patch("tools.skills_tool.SKILLS_DIR", tmp_path):
with patch("hermes_agent.tools.skills.tool.SKILLS_DIR", tmp_path):
_make_plan_skill(tmp_path)
scan_skill_commands()
result = cli_obj.process_command("/plan Add OAuth login")
@ -56,7 +56,7 @@ class TestCLIPlanCommand:
def test_plan_without_args_uses_skill_context_guidance(self, tmp_path, monkeypatch):
cli_obj = _make_cli()
with patch("tools.skills_tool.SKILLS_DIR", tmp_path):
with patch("hermes_agent.tools.skills.tool.SKILLS_DIR", tmp_path):
_make_plan_skill(tmp_path)
scan_skill_commands()
cli_obj.process_command("/plan")

View file

@ -1,6 +1,6 @@
"""Tests for slash command prefix matching in HermesCLI.process_command."""
from unittest.mock import MagicMock, patch
from cli import HermesCLI
from hermes_agent.cli.repl import HermesCLI
def _make_cli():
@ -72,7 +72,7 @@ class TestSlashCommandPrefixMatching:
def test_ambiguous_prefix_shows_suggestions(self):
"""/re matches multiple commands — should show ambiguous message."""
cli_obj = _make_cli()
with patch("cli._cprint") as mock_cprint:
with patch("hermes_agent.cli.repl._cprint") as mock_cprint:
cli_obj.process_command("/re")
printed = " ".join(str(c) for c in mock_cprint.call_args_list)
assert "Ambiguous" in printed or "Did you mean" in printed
@ -80,7 +80,7 @@ class TestSlashCommandPrefixMatching:
def test_unknown_command_shows_error(self):
"""/xyz should show unknown command error."""
cli_obj = _make_cli()
with patch("cli._cprint") as mock_cprint:
with patch("hermes_agent.cli.repl._cprint") as mock_cprint:
cli_obj.process_command("/xyz")
printed = " ".join(str(c) for c in mock_cprint.call_args_list)
assert "Unknown command" in printed
@ -99,7 +99,7 @@ class TestSlashCommandPrefixMatching:
printed = []
cli_obj.console.print = lambda *a, **kw: printed.append(str(a))
import cli as cli_mod
import hermes_agent.cli.repl as cli_mod
with patch.object(cli_mod, '_skill_commands', fake_skill):
cli_obj.process_command("/test-skill-xy")
@ -113,7 +113,7 @@ class TestSlashCommandPrefixMatching:
# /help-extra is a fake skill that shares /hel prefix with /help
fake_skill = {"/help-extra": {"name": "Help Extra", "description": "test"}}
import cli as cli_mod
import hermes_agent.cli.repl as cli_mod
with patch.object(cli_mod, '_skill_commands', fake_skill), patch.object(cli_obj, 'show_help') as mock_help:
cli_obj.process_command("/help")
@ -127,7 +127,7 @@ class TestSlashCommandPrefixMatching:
cli_obj = _make_cli()
fake_skill = {"/quint-pipeline": {"name": "Quint Pipeline", "description": "test"}}
import cli as cli_mod
import hermes_agent.cli.repl as cli_mod
with patch.object(cli_mod, '_skill_commands', fake_skill):
# /quit is caught by the exact "/quit" branch → process_command returns False
result = cli_obj.process_command("/qui")
@ -141,7 +141,7 @@ class TestSlashCommandPrefixMatching:
"""/re matches /reset and /retry (both 6 chars) — no unique shortest, stays ambiguous."""
cli_obj = _make_cli()
printed = []
import cli as cli_mod
import hermes_agent.cli.repl as cli_mod
with patch.object(cli_mod, '_cprint', side_effect=lambda t: printed.append(t)):
cli_obj.process_command("/re")
combined = " ".join(printed)
@ -151,7 +151,7 @@ class TestSlashCommandPrefixMatching:
"""/help typed with /help-extra skill installed → exact match wins."""
cli_obj = _make_cli()
fake_skill = {"/help-extra": {"name": "Help Extra", "description": ""}}
import cli as cli_mod
import hermes_agent.cli.repl as cli_mod
with patch.object(cli_mod, '_skill_commands', fake_skill), \
patch.object(cli_obj, 'show_help') as mock_help:
cli_obj.process_command("/help")

View file

@ -39,7 +39,7 @@ def _make_real_cli(**kwargs):
with patch.dict(sys.modules, prompt_toolkit_stubs), patch.dict(
"os.environ", clean_env, clear=False
):
import cli as cli_mod
import hermes_agent.cli.repl as cli_mod
cli_mod = importlib.reload(cli_mod)
with patch.object(cli_mod, "get_tool_definitions", return_value=[]), patch.dict(
@ -69,7 +69,7 @@ class _DummyCLI:
def test_main_applies_preloaded_skills_to_system_prompt(monkeypatch):
import cli as cli_mod
import hermes_agent.cli.repl as cli_mod
created = {}
@ -93,7 +93,7 @@ def test_main_applies_preloaded_skills_to_system_prompt(monkeypatch):
def test_main_raises_for_unknown_preloaded_skill(monkeypatch):
import cli as cli_mod
import hermes_agent.cli.repl as cli_mod
monkeypatch.setattr(cli_mod, "HermesCLI", lambda **kwargs: _DummyCLI(**kwargs))
monkeypatch.setattr(
@ -112,7 +112,7 @@ def test_show_banner_does_not_print_skills():
cli_obj.preloaded_skills = ["hermes-agent-dev", "github-auth"]
cli_obj.console = MagicMock()
with patch("cli.build_welcome_banner") as mock_banner, patch(
with patch("hermes_agent.cli.repl.build_welcome_banner") as mock_banner, patch(
"shutil.get_terminal_size", return_value=os.terminal_size((120, 40))
):
cli_obj.show_banner()

View file

@ -6,8 +6,8 @@ from types import SimpleNamespace
import pytest
from hermes_cli.auth import AuthError
from hermes_cli import main as hermes_main
from hermes_agent.cli.auth.auth import AuthError
from hermes_agent.cli import main as hermes_main
# ---------------------------------------------------------------------------
@ -26,7 +26,7 @@ def _reset_modules(prefixes: tuple[str, ...]):
@pytest.fixture(autouse=True)
def _restore_cli_and_tool_modules():
"""Save and restore tools/cli/run_agent modules around every test."""
prefixes = ("tools", "cli", "run_agent")
prefixes = ("tools", "cli", "hermes_agent.agent.loop")
original_modules = {
name: module
for name, module in sys.modules.items()
@ -110,7 +110,7 @@ def _install_prompt_toolkit_stubs():
def _import_cli():
for name in list(sys.modules):
if name == "cli" or name == "run_agent" or name == "tools" or name.startswith("tools."):
if name == "cli" or name == "hermes_agent.agent.loop" or name == "tools" or name.startswith("tools."):
sys.modules.pop(name, None)
if "firecrawl" not in sys.modules:
@ -120,7 +120,7 @@ def _import_cli():
importlib.import_module("prompt_toolkit")
except ModuleNotFoundError:
_install_prompt_toolkit_stubs()
return importlib.import_module("cli")
return importlib.import_module("hermes_agent.cli.repl")
def test_hermes_cli_init_does_not_eagerly_resolve_runtime_provider(monkeypatch):
@ -131,8 +131,8 @@ def test_hermes_cli_init_does_not_eagerly_resolve_runtime_provider(monkeypatch):
calls["count"] += 1
raise AssertionError("resolve_runtime_provider should not be called in HermesCLI.__init__")
monkeypatch.setattr("hermes_cli.runtime_provider.resolve_runtime_provider", _unexpected_runtime_resolve)
monkeypatch.setattr("hermes_cli.runtime_provider.format_runtime_provider_error", lambda exc: str(exc))
monkeypatch.setattr("hermes_agent.cli.runtime_provider.resolve_runtime_provider", _unexpected_runtime_resolve)
monkeypatch.setattr("hermes_agent.cli.runtime_provider.format_runtime_provider_error", lambda exc: str(exc))
shell = cli.HermesCLI(model="gpt-5", compact=True, max_turns=1)
@ -160,8 +160,8 @@ def test_runtime_resolution_failure_is_not_sticky(monkeypatch):
def __init__(self, *args, **kwargs):
self.kwargs = kwargs
monkeypatch.setattr("hermes_cli.runtime_provider.resolve_runtime_provider", _runtime_resolve)
monkeypatch.setattr("hermes_cli.runtime_provider.format_runtime_provider_error", lambda exc: str(exc))
monkeypatch.setattr("hermes_agent.cli.runtime_provider.resolve_runtime_provider", _runtime_resolve)
monkeypatch.setattr("hermes_agent.cli.runtime_provider.format_runtime_provider_error", lambda exc: str(exc))
monkeypatch.setattr(cli, "AIAgent", _DummyAgent)
shell = cli.HermesCLI(model="gpt-5", compact=True, max_turns=1)
@ -184,8 +184,8 @@ def test_runtime_resolution_rebuilds_agent_on_routing_change(monkeypatch):
"source": "env/config",
}
monkeypatch.setattr("hermes_cli.runtime_provider.resolve_runtime_provider", _runtime_resolve)
monkeypatch.setattr("hermes_cli.runtime_provider.format_runtime_provider_error", lambda exc: str(exc))
monkeypatch.setattr("hermes_agent.cli.runtime_provider.resolve_runtime_provider", _runtime_resolve)
monkeypatch.setattr("hermes_agent.cli.runtime_provider.format_runtime_provider_error", lambda exc: str(exc))
shell = cli.HermesCLI(model="gpt-5", compact=True, max_turns=1)
shell.provider = "openrouter"
@ -253,10 +253,10 @@ def test_codex_provider_replaces_incompatible_default_model(monkeypatch):
"source": "env/config",
}
monkeypatch.setattr("hermes_cli.runtime_provider.resolve_runtime_provider", _runtime_resolve)
monkeypatch.setattr("hermes_cli.runtime_provider.format_runtime_provider_error", lambda exc: str(exc))
monkeypatch.setattr("hermes_agent.cli.runtime_provider.resolve_runtime_provider", _runtime_resolve)
monkeypatch.setattr("hermes_agent.cli.runtime_provider.format_runtime_provider_error", lambda exc: str(exc))
monkeypatch.setattr(
"hermes_cli.codex_models.get_codex_model_ids",
"hermes_agent.cli.models.codex.get_codex_model_ids",
lambda access_token=None: ["gpt-5.2-codex", "gpt-5.1-codex-mini"],
)
@ -271,7 +271,7 @@ def test_codex_provider_replaces_incompatible_default_model(monkeypatch):
def test_model_flow_nous_prints_subscription_guidance_without_mutating_explicit_tts(monkeypatch, capsys):
monkeypatch.setattr("hermes_cli.nous_subscription.managed_nous_tools_enabled", lambda: True)
monkeypatch.setattr("hermes_agent.cli.nous_subscription.managed_nous_tools_enabled", lambda: True)
config = {
"model": {"provider": "nous", "default": "claude-opus-4-6"},
"tts": {"provider": "elevenlabs"},
@ -279,23 +279,23 @@ def test_model_flow_nous_prints_subscription_guidance_without_mutating_explicit_
}
monkeypatch.setattr(
"hermes_cli.auth.get_provider_auth_state",
"hermes_agent.cli.auth.auth.get_provider_auth_state",
lambda provider: {"access_token": "nous-token"},
)
monkeypatch.setattr(
"hermes_cli.auth.resolve_nous_runtime_credentials",
"hermes_agent.cli.auth.auth.resolve_nous_runtime_credentials",
lambda *args, **kwargs: {
"base_url": "https://inference.example.com/v1",
"api_key": "nous-key",
},
)
monkeypatch.setattr(
"hermes_cli.auth.fetch_nous_models",
"hermes_agent.cli.auth.auth.fetch_nous_models",
lambda *args, **kwargs: ["claude-opus-4-6"],
)
monkeypatch.setattr("hermes_cli.auth._prompt_model_selection", lambda model_ids, current_model="", pricing=None, **kw: "claude-opus-4-6")
monkeypatch.setattr("hermes_cli.auth._save_model_choice", lambda model: None)
monkeypatch.setattr("hermes_cli.auth._update_config_for_provider", lambda provider, url: None)
monkeypatch.setattr("hermes_agent.cli.auth.auth._prompt_model_selection", lambda model_ids, current_model="", pricing=None, **kw: "claude-opus-4-6")
monkeypatch.setattr("hermes_agent.cli.auth.auth._save_model_choice", lambda model: None)
monkeypatch.setattr("hermes_agent.cli.auth.auth._update_config_for_provider", lambda provider, url: None)
hermes_main._model_flow_nous(config, current_model="claude-opus-4-6")
@ -306,30 +306,30 @@ def test_model_flow_nous_prints_subscription_guidance_without_mutating_explicit_
def test_model_flow_nous_offers_tool_gateway_prompt_when_unconfigured(monkeypatch, capsys):
monkeypatch.setattr("hermes_cli.nous_subscription.managed_nous_tools_enabled", lambda: True)
monkeypatch.setattr("hermes_agent.cli.nous_subscription.managed_nous_tools_enabled", lambda: True)
config = {
"model": {"provider": "nous", "default": "claude-opus-4-6"},
"tts": {"provider": "edge"},
}
monkeypatch.setattr(
"hermes_cli.auth.get_provider_auth_state",
"hermes_agent.cli.auth.auth.get_provider_auth_state",
lambda provider: {"access_token": "***"},
)
monkeypatch.setattr(
"hermes_cli.auth.resolve_nous_runtime_credentials",
"hermes_agent.cli.auth.auth.resolve_nous_runtime_credentials",
lambda *args, **kwargs: {
"base_url": "https://inference.example.com/v1",
"api_key": "***",
},
)
monkeypatch.setattr(
"hermes_cli.auth.fetch_nous_models",
"hermes_agent.cli.auth.auth.fetch_nous_models",
lambda *args, **kwargs: ["claude-opus-4-6"],
)
monkeypatch.setattr("hermes_cli.auth._prompt_model_selection", lambda model_ids, current_model="", pricing=None, **kw: "claude-opus-4-6")
monkeypatch.setattr("hermes_cli.auth._save_model_choice", lambda model: None)
monkeypatch.setattr("hermes_cli.auth._update_config_for_provider", lambda provider, url: None)
monkeypatch.setattr("hermes_agent.cli.auth.auth._prompt_model_selection", lambda model_ids, current_model="", pricing=None, **kw: "claude-opus-4-6")
monkeypatch.setattr("hermes_agent.cli.auth.auth._save_model_choice", lambda model: None)
monkeypatch.setattr("hermes_agent.cli.auth.auth._update_config_for_provider", lambda provider, url: None)
hermes_main._model_flow_nous(config, current_model="claude-opus-4-6")
out = capsys.readouterr().out
@ -363,11 +363,11 @@ def test_codex_provider_uses_config_model(monkeypatch):
"source": "env/config",
}
monkeypatch.setattr("hermes_cli.runtime_provider.resolve_runtime_provider", _runtime_resolve)
monkeypatch.setattr("hermes_cli.runtime_provider.format_runtime_provider_error", lambda exc: str(exc))
monkeypatch.setattr("hermes_agent.cli.runtime_provider.resolve_runtime_provider", _runtime_resolve)
monkeypatch.setattr("hermes_agent.cli.runtime_provider.format_runtime_provider_error", lambda exc: str(exc))
# Prevent live API call from overriding the config model
monkeypatch.setattr(
"hermes_cli.codex_models.get_codex_model_ids",
"hermes_agent.cli.models.codex.get_codex_model_ids",
lambda access_token=None: ["gpt-5.2-codex"],
)
@ -406,11 +406,11 @@ def test_codex_config_model_not_replaced_by_normalization(monkeypatch):
"source": "env/config",
}
monkeypatch.setattr("hermes_cli.runtime_provider.resolve_runtime_provider", _runtime_resolve)
monkeypatch.setattr("hermes_cli.runtime_provider.format_runtime_provider_error", lambda exc: str(exc))
monkeypatch.setattr("hermes_agent.cli.runtime_provider.resolve_runtime_provider", _runtime_resolve)
monkeypatch.setattr("hermes_agent.cli.runtime_provider.format_runtime_provider_error", lambda exc: str(exc))
# API returns a DIFFERENT model than what the user configured
monkeypatch.setattr(
"hermes_cli.codex_models.get_codex_model_ids",
"hermes_agent.cli.models.codex.get_codex_model_ids",
lambda access_token=None: ["gpt-5.4", "gpt-5.3-codex"],
)
@ -441,8 +441,8 @@ def test_codex_provider_preserves_explicit_codex_model(monkeypatch):
"source": "env/config",
}
monkeypatch.setattr("hermes_cli.runtime_provider.resolve_runtime_provider", _runtime_resolve)
monkeypatch.setattr("hermes_cli.runtime_provider.format_runtime_provider_error", lambda exc: str(exc))
monkeypatch.setattr("hermes_agent.cli.runtime_provider.resolve_runtime_provider", _runtime_resolve)
monkeypatch.setattr("hermes_agent.cli.runtime_provider.format_runtime_provider_error", lambda exc: str(exc))
shell = cli.HermesCLI(model="gpt-5.1-codex-mini", compact=True, max_turns=1)
@ -468,8 +468,8 @@ def test_codex_provider_strips_provider_prefix_from_model(monkeypatch):
"source": "env/config",
}
monkeypatch.setattr("hermes_cli.runtime_provider.resolve_runtime_provider", _runtime_resolve)
monkeypatch.setattr("hermes_cli.runtime_provider.format_runtime_provider_error", lambda exc: str(exc))
monkeypatch.setattr("hermes_agent.cli.runtime_provider.resolve_runtime_provider", _runtime_resolve)
monkeypatch.setattr("hermes_agent.cli.runtime_provider.format_runtime_provider_error", lambda exc: str(exc))
shell = cli.HermesCLI(model="openai/gpt-5.3-codex", compact=True, max_turns=1)
@ -479,19 +479,19 @@ def test_codex_provider_strips_provider_prefix_from_model(monkeypatch):
def test_cmd_model_falls_back_to_auto_on_invalid_provider(monkeypatch, capsys):
monkeypatch.setattr(
"hermes_cli.config.load_config",
"hermes_agent.cli.config.load_config",
lambda: {"model": {"default": "gpt-5", "provider": "invalid-provider"}},
)
monkeypatch.setattr("hermes_cli.config.save_config", lambda cfg: None)
monkeypatch.setattr("hermes_cli.config.get_env_value", lambda key: "")
monkeypatch.setattr("hermes_cli.config.save_env_value", lambda key, value: None)
monkeypatch.setattr("hermes_agent.cli.config.save_config", lambda cfg: None)
monkeypatch.setattr("hermes_agent.cli.config.get_env_value", lambda key: "")
monkeypatch.setattr("hermes_agent.cli.config.save_env_value", lambda key, value: None)
def _resolve_provider(requested, **kwargs):
if requested == "invalid-provider":
raise AuthError("Unknown provider 'invalid-provider'.", code="invalid_provider")
return "openrouter"
monkeypatch.setattr("hermes_cli.auth.resolve_provider", _resolve_provider)
monkeypatch.setattr("hermes_agent.cli.auth.auth.resolve_provider", _resolve_provider)
monkeypatch.setattr(hermes_main, "_prompt_provider_choice", lambda choices, **kwargs: len(choices) - 1)
monkeypatch.setattr("sys.stdin", type("FakeTTY", (), {"isatty": lambda self: True})())
@ -505,16 +505,16 @@ def test_cmd_model_falls_back_to_auto_on_invalid_provider(monkeypatch, capsys):
def test_model_flow_custom_saves_verified_v1_base_url(monkeypatch, capsys):
monkeypatch.setattr(
"hermes_cli.config.get_env_value",
"hermes_agent.cli.config.get_env_value",
lambda key: "" if key in {"OPENAI_BASE_URL", "OPENAI_API_KEY"} else "",
)
saved_env = {}
monkeypatch.setattr("hermes_cli.config.save_env_value", lambda key, value: saved_env.__setitem__(key, value))
monkeypatch.setattr("hermes_cli.auth._save_model_choice", lambda model: saved_env.__setitem__("MODEL", model))
monkeypatch.setattr("hermes_cli.auth.deactivate_provider", lambda: None)
monkeypatch.setattr("hermes_cli.main._save_custom_provider", lambda *args, **kwargs: None)
monkeypatch.setattr("hermes_agent.cli.config.save_env_value", lambda key, value: saved_env.__setitem__(key, value))
monkeypatch.setattr("hermes_agent.cli.auth.auth._save_model_choice", lambda model: saved_env.__setitem__("MODEL", model))
monkeypatch.setattr("hermes_agent.cli.auth.auth.deactivate_provider", lambda: None)
monkeypatch.setattr("hermes_agent.cli.main._save_custom_provider", lambda *args, **kwargs: None)
monkeypatch.setattr(
"hermes_cli.models.probe_api_models",
"hermes_agent.cli.models.models.probe_api_models",
lambda api_key, base_url: {
"models": ["llm"],
"probed_url": "http://localhost:8000/v1/models",
@ -524,10 +524,10 @@ def test_model_flow_custom_saves_verified_v1_base_url(monkeypatch, capsys):
},
)
monkeypatch.setattr(
"hermes_cli.config.load_config",
"hermes_agent.cli.config.load_config",
lambda: {"model": {"default": "", "provider": "custom", "base_url": ""}},
)
monkeypatch.setattr("hermes_cli.config.save_config", lambda cfg: None)
monkeypatch.setattr("hermes_agent.cli.config.save_config", lambda cfg: None)
# After the probe detects a single model ("llm"), the flow asks
# "Use this model? [Y/n]:" — confirm with Enter, then context length,
@ -549,14 +549,14 @@ def test_model_flow_custom_saves_verified_v1_base_url(monkeypatch, capsys):
def test_cmd_model_forwards_nous_login_tls_options(monkeypatch):
monkeypatch.setattr(hermes_main, "_require_tty", lambda *a: None)
monkeypatch.setattr(
"hermes_cli.config.load_config",
"hermes_agent.cli.config.load_config",
lambda: {"model": {"default": "gpt-5", "provider": "nous"}},
)
monkeypatch.setattr("hermes_cli.config.save_config", lambda cfg: None)
monkeypatch.setattr("hermes_cli.config.get_env_value", lambda key: "")
monkeypatch.setattr("hermes_cli.config.save_env_value", lambda key, value: None)
monkeypatch.setattr("hermes_cli.auth.resolve_provider", lambda requested, **kwargs: "nous")
monkeypatch.setattr("hermes_cli.auth.get_provider_auth_state", lambda provider_id: None)
monkeypatch.setattr("hermes_agent.cli.config.save_config", lambda cfg: None)
monkeypatch.setattr("hermes_agent.cli.config.get_env_value", lambda key: "")
monkeypatch.setattr("hermes_agent.cli.config.save_env_value", lambda key, value: None)
monkeypatch.setattr("hermes_agent.cli.auth.auth.resolve_provider", lambda requested, **kwargs: "nous")
monkeypatch.setattr("hermes_agent.cli.auth.auth.get_provider_auth_state", lambda provider_id: None)
monkeypatch.setattr(hermes_main, "_prompt_provider_choice", lambda choices, **kwargs: 0)
captured = {}
@ -571,7 +571,7 @@ def test_cmd_model_forwards_nous_login_tls_options(monkeypatch):
captured["ca_bundle"] = login_args.ca_bundle
captured["insecure"] = login_args.insecure
monkeypatch.setattr("hermes_cli.auth._login_nous", _fake_login)
monkeypatch.setattr("hermes_agent.cli.auth.auth._login_nous", _fake_login)
hermes_main.cmd_model(
SimpleNamespace(
@ -603,18 +603,18 @@ def test_cmd_model_forwards_nous_login_tls_options(monkeypatch):
# ---------------------------------------------------------------------------
def test_auto_provider_name_localhost():
from hermes_cli.main import _auto_provider_name
from hermes_agent.cli.main import _auto_provider_name
assert _auto_provider_name("http://localhost:11434/v1") == "Local (localhost:11434)"
assert _auto_provider_name("http://127.0.0.1:1234/v1") == "Local (127.0.0.1:1234)"
def test_auto_provider_name_runpod():
from hermes_cli.main import _auto_provider_name
from hermes_agent.cli.main import _auto_provider_name
assert "RunPod" in _auto_provider_name("https://xyz.runpod.io/v1")
def test_auto_provider_name_remote():
from hermes_cli.main import _auto_provider_name
from hermes_agent.cli.main import _auto_provider_name
result = _auto_provider_name("https://api.together.xyz/v1")
assert result == "Api.together.xyz"
@ -622,18 +622,18 @@ def test_auto_provider_name_remote():
def test_save_custom_provider_uses_provided_name(monkeypatch, tmp_path):
"""When a display name is passed, it should appear in the saved entry."""
import yaml
from hermes_cli.main import _save_custom_provider
from hermes_agent.cli.main import _save_custom_provider
cfg_path = tmp_path / "config.yaml"
cfg_path.write_text(yaml.dump({}))
monkeypatch.setattr(
"hermes_cli.config.load_config", lambda: yaml.safe_load(cfg_path.read_text()) or {},
"hermes_agent.cli.config.load_config", lambda: yaml.safe_load(cfg_path.read_text()) or {},
)
saved = {}
def _save(cfg):
saved.update(cfg)
monkeypatch.setattr("hermes_cli.config.save_config", _save)
monkeypatch.setattr("hermes_agent.cli.config.save_config", _save)
_save_custom_provider("http://localhost:11434/v1", name="Ollama")
entries = saved.get("custom_providers", [])

View file

@ -21,15 +21,15 @@ class TestSaveConfigValueAtomic:
"model": {"default": "test-model", "provider": "openrouter"},
"display": {"skin": "default"},
}))
monkeypatch.setattr("cli._hermes_home", hermes_home)
monkeypatch.setattr("hermes_agent.cli.repl._hermes_home", hermes_home)
return config_path
def test_calls_atomic_yaml_write(self, config_env, monkeypatch):
"""save_config_value must route through atomic_yaml_write, not bare open()."""
mock_atomic = MagicMock()
monkeypatch.setattr("utils.atomic_yaml_write", mock_atomic)
monkeypatch.setattr("hermes_agent.utils.atomic_yaml_write", mock_atomic)
from cli import save_config_value
from hermes_agent.cli.repl import save_config_value
save_config_value("display.skin", "mono")
mock_atomic.assert_called_once()
@ -39,8 +39,8 @@ class TestSaveConfigValueAtomic:
def test_preserves_existing_keys(self, config_env):
"""Writing a new key must not clobber existing config entries."""
from cli import save_config_value
save_config_value("agent.max_turns", 50)
from hermes_agent.cli.repl import save_config_value
save_config_value("hermes_agent.agent.max_turns", 50)
result = yaml.safe_load(config_env.read_text())
assert result["model"]["default"] == "test-model"
@ -50,7 +50,7 @@ class TestSaveConfigValueAtomic:
def test_creates_nested_keys(self, config_env):
"""Dot-separated paths create intermediate dicts as needed."""
from cli import save_config_value
from hermes_agent.cli.repl import save_config_value
save_config_value("auxiliary.compression.model", "google/gemini-3-flash-preview")
result = yaml.safe_load(config_env.read_text())
@ -58,7 +58,7 @@ class TestSaveConfigValueAtomic:
def test_overwrites_existing_value(self, config_env):
"""Updating an existing key replaces the value."""
from cli import save_config_value
from hermes_agent.cli.repl import save_config_value
save_config_value("display.skin", "ares")
result = yaml.safe_load(config_env.read_text())
@ -75,7 +75,7 @@ class TestSaveConfigValueAtomic:
"model": {"default": "test-model", "provider": "openrouter"},
}))
from cli import save_config_value
from hermes_agent.cli.repl import save_config_value
save_config_value("model.default", "doubao-pro")
result = yaml.safe_load(config_env.read_text())
@ -89,9 +89,9 @@ class TestSaveConfigValueAtomic:
def exploding_write(*args, **kwargs):
raise OSError("disk full")
monkeypatch.setattr("utils.atomic_yaml_write", exploding_write)
monkeypatch.setattr("hermes_agent.utils.atomic_yaml_write", exploding_write)
from cli import save_config_value
from hermes_agent.cli.repl import save_config_value
result = save_config_value("display.skin", "broken")
assert result is False

View file

@ -3,11 +3,11 @@ import threading
import time
from unittest.mock import patch
import cli as cli_module
import tools.skills_tool as skills_tool_module
from cli import HermesCLI
from hermes_cli.callbacks import prompt_for_secret
from tools.skills_tool import set_secret_capture_callback
import hermes_agent.cli.repl as cli_module
import hermes_agent.tools.skills.tool as skills_tool_module
from hermes_agent.cli.repl import HermesCLI
from hermes_agent.cli.ui.callbacks import prompt_for_secret
from hermes_agent.tools.skills.tool import set_secret_capture_callback
class _FakeBuffer:
@ -40,7 +40,7 @@ def test_secret_capture_callback_can_be_completed_from_cli_state_machine():
cli = _make_cli_stub(with_app=True)
results = []
with patch("hermes_cli.callbacks.save_env_value_secure") as save_secret:
with patch("hermes_agent.cli.ui.callbacks.save_env_value_secure") as save_secret:
save_secret.return_value = {
"success": True,
"stored_as": "TENOR_API_KEY",
@ -86,8 +86,8 @@ def test_cancel_secret_capture_marks_setup_skipped():
def test_secret_capture_uses_getpass_without_tui():
cli = _make_cli_stub()
with patch("hermes_cli.callbacks.getpass.getpass", return_value="secret-value"), patch(
"hermes_cli.callbacks.save_env_value_secure"
with patch("hermes_agent.cli.ui.callbacks.getpass.getpass", return_value="secret-value"), patch(
"hermes_agent.cli.ui.callbacks.save_env_value_secure"
) as save_secret:
save_secret.return_value = {
"success": True,
@ -110,8 +110,8 @@ def test_secret_capture_timeout_clears_hidden_input_buffer():
cli._clear_secret_input_buffer = clear_buffer
with patch("hermes_cli.callbacks.queue.Queue.get", side_effect=queue.Empty), patch(
"hermes_cli.callbacks._time.monotonic",
with patch("hermes_agent.cli.ui.callbacks.queue.Queue.get", side_effect=queue.Empty), patch(
"hermes_agent.cli.ui.callbacks._time.monotonic",
side_effect=[0, 121],
):
result = prompt_for_secret(cli, "TENOR_API_KEY", "Tenor API key")
@ -134,7 +134,7 @@ def test_cli_chat_registers_secret_capture_callback():
"terminal": {"env_type": "local"},
}
with patch("cli.get_tool_definitions", return_value=[]), patch.dict(
with patch("hermes_agent.cli.repl.get_tool_definitions", return_value=[]), patch.dict(
"os.environ", {"LLM_MODEL": "", "HERMES_MAX_ITERATIONS": ""}, clear=False
), patch.dict(cli_module.__dict__, {"CLI_CONFIG": clean_config}):
cli_obj = HermesCLI()

View file

@ -1,8 +1,8 @@
from types import SimpleNamespace
from unittest.mock import MagicMock, patch
from cli import HermesCLI, _rich_text_from_ansi
from hermes_cli.skin_engine import get_active_skin, set_active_skin
from hermes_agent.cli.repl import HermesCLI, _rich_text_from_ansi
from hermes_agent.cli.ui.skin_engine import get_active_skin, set_active_skin
def _make_cli_stub():
@ -72,7 +72,7 @@ class TestCliSkinPromptIntegration:
cli = _make_cli_stub()
cli._secret_state = {"response_queue": object()}
with patch("hermes_cli.skin_engine.get_active_prompt_symbol", return_value=""):
with patch("hermes_agent.cli.ui.skin_engine.get_active_prompt_symbol", return_value=""):
assert cli._get_tui_prompt_fragments() == [("class:sudo-prompt", "🔑 ⚔ ")]
def test_build_tui_style_dict_uses_skin_overrides(self):
@ -98,7 +98,7 @@ class TestCliSkinPromptIntegration:
def test_handle_skin_command_refreshes_live_tui(self, capsys):
cli = _make_cli_stub()
with patch("cli.save_config_value", return_value=True):
with patch("hermes_agent.cli.repl.save_config_value", return_value=True):
cli._handle_skin_command("/skin ares")
output = capsys.readouterr().out

View file

@ -2,7 +2,7 @@ from datetime import datetime, timedelta
from types import SimpleNamespace
from unittest.mock import MagicMock, patch
from cli import HermesCLI
from hermes_agent.cli.repl import HermesCLI
def _make_cli(model: str = "anthropic/claude-sonnet-4-20250514"):

View file

@ -4,8 +4,8 @@ from pathlib import Path
from types import SimpleNamespace
from unittest.mock import MagicMock, patch
from cli import HermesCLI
from hermes_cli.commands import resolve_command
from hermes_agent.cli.repl import HermesCLI
from hermes_agent.cli.commands import resolve_command
def _make_cli():
@ -70,7 +70,7 @@ def test_show_session_status_prints_gateway_style_summary():
"started_at": 1775791440,
}
with patch("cli.display_hermes_home", return_value="~/.hermes"):
with patch("hermes_agent.cli.repl.display_hermes_home", return_value="~/.hermes"):
cli_obj._show_session_status()
printed = "\n".join(str(call.args[0]) for call in cli_obj.console.print.call_args_list)

View file

@ -59,7 +59,7 @@ def _make_cli():
with patch.dict(sys.modules, prompt_toolkit_stubs), patch.dict(
"os.environ", clean_env, clear=False
):
import cli as _cli_mod
import hermes_agent.cli.repl as _cli_mod
_cli_mod = importlib.reload(_cli_mod)
with patch.object(_cli_mod, "get_tool_definitions", return_value=[]), patch.dict(

View file

@ -2,7 +2,7 @@
from unittest.mock import MagicMock, patch, call
from cli import HermesCLI
from hermes_agent.cli.repl import HermesCLI
def _make_cli(enabled_toolsets=None):
@ -39,9 +39,9 @@ class TestToolsSlashList:
def test_list_calls_backend(self, capsys):
cli_obj = _make_cli()
with patch("hermes_cli.tools_config.load_config",
with patch("hermes_agent.cli.tools_config.load_config",
return_value={"platform_toolsets": {"cli": ["web"]}}), \
patch("hermes_cli.tools_config.save_config"):
patch("hermes_agent.cli.tools_config.save_config"):
cli_obj._handle_tools_command("/tools list")
out = capsys.readouterr().out
assert "web" in out
@ -49,7 +49,7 @@ class TestToolsSlashList:
def test_list_does_not_modify_enabled_toolsets(self):
"""List is read-only — self.enabled_toolsets must not change."""
cli_obj = _make_cli(["web", "memory"])
with patch("hermes_cli.tools_config.load_config",
with patch("hermes_agent.cli.tools_config.load_config",
return_value={"platform_toolsets": {"cli": ["web"]}}):
cli_obj._handle_tools_command("/tools list")
assert cli_obj.enabled_toolsets == {"web", "memory"}
@ -63,11 +63,11 @@ class TestToolsSlashDisableWithReset:
def test_disable_applies_directly_and_resets_session(self):
"""Disable applies immediately (no confirmation prompt) and resets session."""
cli_obj = _make_cli(["web", "memory"])
with patch("hermes_cli.tools_config.load_config",
with patch("hermes_agent.cli.tools_config.load_config",
return_value={"platform_toolsets": {"cli": ["web", "memory"]}}), \
patch("hermes_cli.tools_config.save_config"), \
patch("hermes_cli.tools_config._get_platform_tools", return_value={"memory"}), \
patch("hermes_cli.config.load_config", return_value={}), \
patch("hermes_agent.cli.tools_config.save_config"), \
patch("hermes_agent.cli.tools_config._get_platform_tools", return_value={"memory"}), \
patch("hermes_agent.cli.config.load_config", return_value={}), \
patch.object(cli_obj, "new_session") as mock_reset:
cli_obj._handle_tools_command("/tools disable web")
mock_reset.assert_called_once()
@ -76,11 +76,11 @@ class TestToolsSlashDisableWithReset:
def test_disable_does_not_prompt_for_confirmation(self):
"""Disable no longer uses input() — it applies directly."""
cli_obj = _make_cli(["web", "memory"])
with patch("hermes_cli.tools_config.load_config",
with patch("hermes_agent.cli.tools_config.load_config",
return_value={"platform_toolsets": {"cli": ["web", "memory"]}}), \
patch("hermes_cli.tools_config.save_config"), \
patch("hermes_cli.tools_config._get_platform_tools", return_value={"memory"}), \
patch("hermes_cli.config.load_config", return_value={}), \
patch("hermes_agent.cli.tools_config.save_config"), \
patch("hermes_agent.cli.tools_config._get_platform_tools", return_value={"memory"}), \
patch("hermes_agent.cli.config.load_config", return_value={}), \
patch.object(cli_obj, "new_session"), \
patch("builtins.input") as mock_input:
cli_obj._handle_tools_command("/tools disable web")
@ -89,11 +89,11 @@ class TestToolsSlashDisableWithReset:
def test_disable_always_resets_session(self):
"""Even without a confirmation prompt, disable always resets the session."""
cli_obj = _make_cli(["web", "memory"])
with patch("hermes_cli.tools_config.load_config",
with patch("hermes_agent.cli.tools_config.load_config",
return_value={"platform_toolsets": {"cli": ["web", "memory"]}}), \
patch("hermes_cli.tools_config.save_config"), \
patch("hermes_cli.tools_config._get_platform_tools", return_value={"memory"}), \
patch("hermes_cli.config.load_config", return_value={}), \
patch("hermes_agent.cli.tools_config.save_config"), \
patch("hermes_agent.cli.tools_config._get_platform_tools", return_value={"memory"}), \
patch("hermes_agent.cli.config.load_config", return_value={}), \
patch.object(cli_obj, "new_session") as mock_reset:
cli_obj._handle_tools_command("/tools disable web")
mock_reset.assert_called_once()
@ -113,11 +113,11 @@ class TestToolsSlashEnableWithReset:
def test_enable_applies_directly_and_resets_session(self):
"""Enable applies immediately (no confirmation prompt) and resets session."""
cli_obj = _make_cli(["memory"])
with patch("hermes_cli.tools_config.load_config",
with patch("hermes_agent.cli.tools_config.load_config",
return_value={"platform_toolsets": {"cli": ["memory"]}}), \
patch("hermes_cli.tools_config.save_config"), \
patch("hermes_cli.tools_config._get_platform_tools", return_value={"memory", "web"}), \
patch("hermes_cli.config.load_config", return_value={}), \
patch("hermes_agent.cli.tools_config.save_config"), \
patch("hermes_agent.cli.tools_config._get_platform_tools", return_value={"memory", "web"}), \
patch("hermes_agent.cli.config.load_config", return_value={}), \
patch.object(cli_obj, "new_session") as mock_reset:
cli_obj._handle_tools_command("/tools enable web")
mock_reset.assert_called_once()

View file

@ -3,8 +3,6 @@ import os
import sys
from unittest.mock import MagicMock, patch
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
_cli_mod = None
@ -44,7 +42,7 @@ def _make_cli(user_message_preview=None):
"prompt_toolkit.auto_suggest": MagicMock(),
}
with patch.dict(sys.modules, prompt_toolkit_stubs), patch.dict("os.environ", clean_env, clear=False):
import cli as mod
import hermes_agent.cli.repl as mod
mod = importlib.reload(mod)
_cli_mod = mod

View file

@ -33,7 +33,7 @@ def test_focus_topic_extracted_and_passed(capsys):
return 100
return 50
with patch("agent.model_metadata.estimate_messages_tokens_rough", side_effect=_estimate):
with patch("hermes_agent.providers.metadata.estimate_messages_tokens_rough", side_effect=_estimate):
shell._manual_compress("/compress database schema")
output = capsys.readouterr().out
@ -55,7 +55,7 @@ def test_no_focus_topic_when_bare_command(capsys):
shell.agent._cached_system_prompt = ""
shell.agent._compress_context.return_value = (list(history), "")
with patch("agent.model_metadata.estimate_messages_tokens_rough", return_value=100):
with patch("hermes_agent.providers.metadata.estimate_messages_tokens_rough", return_value=100):
shell._manual_compress("/compress")
shell.agent._compress_context.assert_called_once()
@ -73,7 +73,7 @@ def test_empty_focus_after_command_treated_as_none(capsys):
shell.agent._cached_system_prompt = ""
shell.agent._compress_context.return_value = (list(history), "")
with patch("agent.model_metadata.estimate_messages_tokens_rough", return_value=100):
with patch("hermes_agent.providers.metadata.estimate_messages_tokens_rough", return_value=100):
shell._manual_compress("/compress ")
shell.agent._compress_context.assert_called_once()
@ -92,7 +92,7 @@ def test_focus_topic_printed_in_compression_banner(capsys):
shell.agent._cached_system_prompt = ""
shell.agent._compress_context.return_value = (compressed, "")
with patch("agent.model_metadata.estimate_messages_tokens_rough", return_value=100):
with patch("hermes_agent.providers.metadata.estimate_messages_tokens_rough", return_value=100):
shell._manual_compress("/compress API endpoints")
output = capsys.readouterr().out
@ -110,7 +110,7 @@ def test_no_focus_prints_standard_banner(capsys):
shell.agent._cached_system_prompt = ""
shell.agent._compress_context.return_value = (compressed, "")
with patch("agent.model_metadata.estimate_messages_tokens_rough", return_value=100):
with patch("hermes_agent.providers.metadata.estimate_messages_tokens_rough", return_value=100):
shell._manual_compress("/compress")
output = capsys.readouterr().out

View file

@ -6,7 +6,7 @@ from unittest.mock import MagicMock, patch
def _import_cli():
import hermes_cli.config as config_mod
import hermes_agent.cli.config as config_mod
if not hasattr(config_mod, "save_env_value_secure"):
config_mod.save_env_value_secure = lambda key, value: {
@ -15,7 +15,7 @@ def _import_cli():
"validated": False,
}
import cli as cli_mod
import hermes_agent.cli.repl as cli_mod
return cli_mod
@ -83,7 +83,7 @@ class TestHandleFastCommand(unittest.TestCase):
):
cli_mod.HermesCLI._handle_fast_command(stub, "/fast normal")
mock_save.assert_called_once_with("agent.service_tier", "normal")
mock_save.assert_called_once_with("hermes_agent.agent.service_tier", "normal")
self.assertIsNone(stub.service_tier)
self.assertIsNone(stub.agent)
@ -112,7 +112,7 @@ class TestPriorityProcessingModels(unittest.TestCase):
"""Verify the expanded Priority Processing model registry."""
def test_all_documented_models_supported(self):
from hermes_cli.models import model_supports_fast_mode
from hermes_agent.cli.models.models import model_supports_fast_mode
# All models from OpenAI's Priority Processing pricing table
supported = [
@ -126,14 +126,14 @@ class TestPriorityProcessingModels(unittest.TestCase):
assert model_supports_fast_mode(model), f"{model} should support fast mode"
def test_vendor_prefix_stripped(self):
from hermes_cli.models import model_supports_fast_mode
from hermes_agent.cli.models.models import model_supports_fast_mode
assert model_supports_fast_mode("openai/gpt-5.4") is True
assert model_supports_fast_mode("openai/gpt-4.1") is True
assert model_supports_fast_mode("openai/o3") is True
def test_non_priority_models_rejected(self):
from hermes_cli.models import model_supports_fast_mode
from hermes_agent.cli.models.models import model_supports_fast_mode
assert model_supports_fast_mode("gpt-5.3-codex") is False
assert model_supports_fast_mode("claude-sonnet-4") is False
@ -141,7 +141,7 @@ class TestPriorityProcessingModels(unittest.TestCase):
assert model_supports_fast_mode(None) is False
def test_resolve_overrides_returns_service_tier(self):
from hermes_cli.models import resolve_fast_mode_overrides
from hermes_agent.cli.models.models import resolve_fast_mode_overrides
result = resolve_fast_mode_overrides("gpt-5.4")
assert result == {"service_tier": "priority"}
@ -150,7 +150,7 @@ class TestPriorityProcessingModels(unittest.TestCase):
assert result == {"service_tier": "priority"}
def test_resolve_overrides_none_for_unsupported(self):
from hermes_cli.models import resolve_fast_mode_overrides
from hermes_agent.cli.models.models import resolve_fast_mode_overrides
assert resolve_fast_mode_overrides("gpt-5.3-codex") is None
assert resolve_fast_mode_overrides("claude-sonnet-4") is None
@ -218,7 +218,7 @@ class TestAnthropicFastMode(unittest.TestCase):
"""Verify Anthropic Fast Mode model support and override resolution."""
def test_anthropic_opus_supported(self):
from hermes_cli.models import model_supports_fast_mode
from hermes_agent.cli.models.models import model_supports_fast_mode
# Native Anthropic format (hyphens)
assert model_supports_fast_mode("claude-opus-4-6") is True
@ -229,7 +229,7 @@ class TestAnthropicFastMode(unittest.TestCase):
assert model_supports_fast_mode("anthropic/claude-opus-4.6") is True
def test_anthropic_non_opus_rejected(self):
from hermes_cli.models import model_supports_fast_mode
from hermes_agent.cli.models.models import model_supports_fast_mode
assert model_supports_fast_mode("claude-sonnet-4-6") is False
assert model_supports_fast_mode("claude-sonnet-4.6") is False
@ -237,14 +237,14 @@ class TestAnthropicFastMode(unittest.TestCase):
assert model_supports_fast_mode("anthropic/claude-sonnet-4.6") is False
def test_anthropic_variant_tags_stripped(self):
from hermes_cli.models import model_supports_fast_mode
from hermes_agent.cli.models.models import model_supports_fast_mode
# OpenRouter variant tags after colon should be stripped
assert model_supports_fast_mode("claude-opus-4.6:fast") is True
assert model_supports_fast_mode("claude-opus-4.6:beta") is True
def test_resolve_overrides_returns_speed_for_anthropic(self):
from hermes_cli.models import resolve_fast_mode_overrides
from hermes_agent.cli.models.models import resolve_fast_mode_overrides
result = resolve_fast_mode_overrides("claude-opus-4-6")
assert result == {"speed": "fast"}
@ -254,13 +254,13 @@ class TestAnthropicFastMode(unittest.TestCase):
def test_resolve_overrides_returns_service_tier_for_openai(self):
"""OpenAI models should still get service_tier, not speed."""
from hermes_cli.models import resolve_fast_mode_overrides
from hermes_agent.cli.models.models import resolve_fast_mode_overrides
result = resolve_fast_mode_overrides("gpt-5.4")
assert result == {"service_tier": "priority"}
def test_is_anthropic_fast_model(self):
from hermes_cli.models import _is_anthropic_fast_model
from hermes_agent.cli.models.models import _is_anthropic_fast_model
assert _is_anthropic_fast_model("claude-opus-4-6") is True
assert _is_anthropic_fast_model("claude-opus-4.6") is True
@ -309,7 +309,7 @@ class TestAnthropicFastModeAdapter(unittest.TestCase):
"""Verify build_anthropic_kwargs handles fast_mode parameter."""
def test_fast_mode_adds_speed_and_beta(self):
from agent.anthropic_adapter import build_anthropic_kwargs, _FAST_MODE_BETA
from hermes_agent.providers.anthropic_adapter import build_anthropic_kwargs, _FAST_MODE_BETA
kwargs = build_anthropic_kwargs(
model="claude-opus-4-6",
@ -325,7 +325,7 @@ class TestAnthropicFastModeAdapter(unittest.TestCase):
assert _FAST_MODE_BETA in kwargs["extra_headers"].get("anthropic-beta", "")
def test_fast_mode_off_no_speed(self):
from agent.anthropic_adapter import build_anthropic_kwargs
from hermes_agent.providers.anthropic_adapter import build_anthropic_kwargs
kwargs = build_anthropic_kwargs(
model="claude-opus-4-6",
@ -340,7 +340,7 @@ class TestAnthropicFastModeAdapter(unittest.TestCase):
assert "extra_headers" not in kwargs
def test_fast_mode_skipped_for_third_party_endpoint(self):
from agent.anthropic_adapter import build_anthropic_kwargs
from hermes_agent.providers.anthropic_adapter import build_anthropic_kwargs
kwargs = build_anthropic_kwargs(
model="claude-opus-4-6",
@ -357,7 +357,7 @@ class TestAnthropicFastModeAdapter(unittest.TestCase):
assert "extra_headers" not in kwargs
def test_fast_mode_kwargs_are_safe_for_sdk_unpacking(self):
from agent.anthropic_adapter import build_anthropic_kwargs
from hermes_agent.providers.anthropic_adapter import build_anthropic_kwargs
kwargs = build_anthropic_kwargs(
model="claude-opus-4-6",
@ -373,7 +373,7 @@ class TestAnthropicFastModeAdapter(unittest.TestCase):
class TestConfigDefault(unittest.TestCase):
def test_default_config_has_service_tier(self):
from hermes_cli.config import DEFAULT_CONFIG
from hermes_agent.cli.config import DEFAULT_CONFIG
agent = DEFAULT_CONFIG.get("agent", {})
self.assertIn("service_tier", agent)

View file

@ -2,8 +2,8 @@ from unittest.mock import MagicMock, patch
def test_gquota_uses_chat_console_when_tui_is_live():
from agent.google_oauth import GoogleOAuthError
from cli import HermesCLI
from hermes_agent.providers.google_oauth import GoogleOAuthError
from hermes_agent.cli.repl import HermesCLI
cli = HermesCLI.__new__(HermesCLI)
cli.console = MagicMock()
@ -11,10 +11,10 @@ def test_gquota_uses_chat_console_when_tui_is_live():
live_console = MagicMock()
with patch("cli.ChatConsole", return_value=live_console), \
patch("agent.google_oauth.get_valid_access_token", side_effect=GoogleOAuthError("No Google OAuth credentials found")), \
patch("agent.google_oauth.load_credentials", return_value=None), \
patch("agent.google_code_assist.retrieve_user_quota"):
with patch("hermes_agent.cli.repl.ChatConsole", return_value=live_console), \
patch("hermes_agent.providers.google_oauth.get_valid_access_token", side_effect=GoogleOAuthError("No Google OAuth credentials found")), \
patch("hermes_agent.providers.google_oauth.load_credentials", return_value=None), \
patch("hermes_agent.agent.google_code_assist.retrieve_user_quota"):
cli._handle_gquota_command("/gquota")
assert live_console.print.call_count == 2

View file

@ -28,7 +28,7 @@ def test_manual_compress_reports_noop_without_success_banner(capsys):
assert messages == history
return 100
with patch("agent.model_metadata.estimate_messages_tokens_rough", side_effect=_estimate):
with patch("hermes_agent.providers.metadata.estimate_messages_tokens_rough", side_effect=_estimate):
shell._manual_compress()
output = capsys.readouterr().out
@ -59,7 +59,7 @@ def test_manual_compress_explains_when_token_estimate_rises(capsys):
return 120
raise AssertionError(f"unexpected transcript: {messages!r}")
with patch("agent.model_metadata.estimate_messages_tokens_rough", side_effect=_estimate):
with patch("hermes_agent.providers.metadata.estimate_messages_tokens_rough", side_effect=_estimate):
shell._manual_compress()
output = capsys.readouterr().out
@ -97,7 +97,7 @@ def test_manual_compress_syncs_session_id_after_split():
shell.agent.session_id = old_id # starts in sync
shell._pending_title = "stale title"
with patch("agent.model_metadata.estimate_messages_tokens_rough", return_value=100):
with patch("hermes_agent.providers.metadata.estimate_messages_tokens_rough", return_value=100):
shell._manual_compress()
# CLI session_id must now point at the continuation child, not the parent.
@ -122,7 +122,7 @@ def test_manual_compress_no_sync_when_session_id_unchanged():
shell.agent._compress_context.return_value = (list(history), "")
shell._pending_title = "keep me"
with patch("agent.model_metadata.estimate_messages_tokens_rough", return_value=100):
with patch("hermes_agent.providers.metadata.estimate_messages_tokens_rough", return_value=100):
shell._manual_compress()
# No split → pending title untouched.

View file

@ -9,7 +9,7 @@ import yaml
class TestCLIPersonalityNone:
def _make_cli(self, personalities=None):
from cli import HermesCLI
from hermes_agent.cli.repl import HermesCLI
cli = HermesCLI.__new__(HermesCLI)
cli.personalities = personalities or {
"helpful": "You are helpful.",
@ -22,37 +22,37 @@ class TestCLIPersonalityNone:
def test_none_clears_system_prompt(self):
cli = self._make_cli()
with patch("cli.save_config_value", return_value=True):
with patch("hermes_agent.cli.repl.save_config_value", return_value=True):
cli._handle_personality_command("/personality none")
assert cli.system_prompt == ""
def test_default_clears_system_prompt(self):
cli = self._make_cli()
with patch("cli.save_config_value", return_value=True):
with patch("hermes_agent.cli.repl.save_config_value", return_value=True):
cli._handle_personality_command("/personality default")
assert cli.system_prompt == ""
def test_neutral_clears_system_prompt(self):
cli = self._make_cli()
with patch("cli.save_config_value", return_value=True):
with patch("hermes_agent.cli.repl.save_config_value", return_value=True):
cli._handle_personality_command("/personality neutral")
assert cli.system_prompt == ""
def test_none_forces_agent_reinit(self):
cli = self._make_cli()
with patch("cli.save_config_value", return_value=True):
with patch("hermes_agent.cli.repl.save_config_value", return_value=True):
cli._handle_personality_command("/personality none")
assert cli.agent is None
def test_none_saves_to_config(self):
cli = self._make_cli()
with patch("cli.save_config_value", return_value=True) as mock_save:
with patch("hermes_agent.cli.repl.save_config_value", return_value=True) as mock_save:
cli._handle_personality_command("/personality none")
mock_save.assert_called_once_with("agent.system_prompt", "")
mock_save.assert_called_once_with("hermes_agent.agent.system_prompt", "")
def test_known_personality_still_works(self):
cli = self._make_cli()
with patch("cli.save_config_value", return_value=True):
with patch("hermes_agent.cli.repl.save_config_value", return_value=True):
cli._handle_personality_command("/personality helpful")
assert cli.system_prompt == "You are helpful."
@ -81,7 +81,7 @@ class TestGatewayPersonalityNone:
return event
def _make_runner(self, personalities=None):
from gateway.run import GatewayRunner
from hermes_agent.gateway.run import GatewayRunner
runner = GatewayRunner.__new__(GatewayRunner)
runner._ephemeral_system_prompt = "You are kawaii~"
runner.config = {
@ -98,7 +98,7 @@ class TestGatewayPersonalityNone:
config_file = tmp_path / "config.yaml"
config_file.write_text(yaml.dump(config_data))
with patch("gateway.run._hermes_home", tmp_path):
with patch("hermes_agent.gateway.run._hermes_home", tmp_path):
event = self._make_event("none")
result = await runner._handle_personality_command(event)
@ -112,7 +112,7 @@ class TestGatewayPersonalityNone:
config_file = tmp_path / "config.yaml"
config_file.write_text(yaml.dump(config_data))
with patch("gateway.run._hermes_home", tmp_path):
with patch("hermes_agent.gateway.run._hermes_home", tmp_path):
event = self._make_event("default")
result = await runner._handle_personality_command(event)
@ -125,7 +125,7 @@ class TestGatewayPersonalityNone:
config_file = tmp_path / "config.yaml"
config_file.write_text(yaml.dump(config_data))
with patch("gateway.run._hermes_home", tmp_path):
with patch("hermes_agent.gateway.run._hermes_home", tmp_path):
event = self._make_event("")
result = await runner._handle_personality_command(event)
@ -138,7 +138,7 @@ class TestGatewayPersonalityNone:
config_file = tmp_path / "config.yaml"
config_file.write_text(yaml.dump(config_data))
with patch("gateway.run._hermes_home", tmp_path):
with patch("hermes_agent.gateway.run._hermes_home", tmp_path):
event = self._make_event("nonexistent")
result = await runner._handle_personality_command(event)
@ -149,8 +149,8 @@ class TestGatewayPersonalityNone:
runner = self._make_runner(personalities={})
(tmp_path / "config.yaml").write_text(yaml.dump({"agent": {"personalities": {}}}))
with patch("gateway.run._hermes_home", tmp_path), \
patch("hermes_constants.display_hermes_home", return_value="~/.hermes/profiles/coder"):
with patch("hermes_agent.gateway.run._hermes_home", tmp_path), \
patch("hermes_agent.constants.display_hermes_home", return_value="~/.hermes/profiles/coder"):
event = self._make_event("")
result = await runner._handle_personality_command(event)
@ -161,7 +161,7 @@ class TestPersonalityDictFormat:
"""Test dict-format custom personalities with description, tone, style."""
def _make_cli(self, personalities):
from cli import HermesCLI
from hermes_agent.cli.repl import HermesCLI
cli = HermesCLI.__new__(HermesCLI)
cli.personalities = personalities
cli.system_prompt = ""
@ -178,7 +178,7 @@ class TestPersonalityDictFormat:
"style": "concise",
}
})
with patch("cli.save_config_value", return_value=True):
with patch("hermes_agent.cli.repl.save_config_value", return_value=True):
cli._handle_personality_command("/personality coder")
assert "You are an expert programmer." in cli.system_prompt
@ -189,7 +189,7 @@ class TestPersonalityDictFormat:
"tone": "technical and precise",
}
})
with patch("cli.save_config_value", return_value=True):
with patch("hermes_agent.cli.repl.save_config_value", return_value=True):
cli._handle_personality_command("/personality coder")
assert "Tone: technical and precise" in cli.system_prompt
@ -200,18 +200,18 @@ class TestPersonalityDictFormat:
"style": "use code examples",
}
})
with patch("cli.save_config_value", return_value=True):
with patch("hermes_agent.cli.repl.save_config_value", return_value=True):
cli._handle_personality_command("/personality coder")
assert "Style: use code examples" in cli.system_prompt
def test_string_personality_still_works(self):
cli = self._make_cli({"helper": "You are helpful."})
with patch("cli.save_config_value", return_value=True):
with patch("hermes_agent.cli.repl.save_config_value", return_value=True):
cli._handle_personality_command("/personality helper")
assert cli.system_prompt == "You are helpful."
def test_resolve_prompt_dict_no_tone_no_style(self):
from cli import HermesCLI
from hermes_agent.cli.repl import HermesCLI
result = HermesCLI._resolve_personality_prompt({
"description": "A helper",
"system_prompt": "You are helpful.",
@ -219,6 +219,6 @@ class TestPersonalityDictFormat:
assert result == "You are helpful."
def test_resolve_prompt_string(self):
from cli import HermesCLI
from hermes_agent.cli.repl import HermesCLI
result = HermesCLI._resolve_personality_prompt("You are helpful.")
assert result == "You are helpful."

View file

@ -17,7 +17,7 @@ class TestCLIQuickCommands:
return str(call_arg)
def _make_cli(self, quick_commands):
from cli import HermesCLI
from hermes_agent.cli.repl import HermesCLI
cli = HermesCLI.__new__(HermesCLI)
cli.config = {"quick_commands": quick_commands}
cli.console = MagicMock()
@ -38,7 +38,7 @@ class TestCLIQuickCommands:
cli._app = object()
live_console = MagicMock()
with patch("cli.ChatConsole", return_value=live_console):
with patch("hermes_agent.cli.repl.ChatConsole", return_value=live_console):
result = cli.process_command("/dn")
assert result is True
@ -100,7 +100,7 @@ class TestCLIQuickCommands:
def test_quick_command_takes_priority_over_skill_commands(self):
"""Quick commands must be checked before skill slash commands."""
cli = self._make_cli({"mygif": {"type": "exec", "command": "echo overridden"}})
with patch("cli._skill_commands", {"/mygif": {"name": "gif-search"}}):
with patch("hermes_agent.cli.repl._skill_commands", {"/mygif": {"name": "gif-search"}}):
cli.process_command("/mygif")
cli.console.print.assert_called_once()
printed = self._printed_plain(cli.console.print.call_args[0][0])
@ -108,7 +108,7 @@ class TestCLIQuickCommands:
def test_unknown_command_still_shows_error(self):
cli = self._make_cli({})
with patch("cli._cprint") as mock_cprint:
with patch("hermes_agent.cli.repl._cprint") as mock_cprint:
cli.process_command("/nonexistent")
mock_cprint.assert_called()
printed = " ".join(str(c) for c in mock_cprint.call_args_list)
@ -143,7 +143,7 @@ class TestGatewayQuickCommands:
@pytest.mark.asyncio
async def test_exec_command_returns_output(self):
from gateway.run import GatewayRunner
from hermes_agent.gateway.run import GatewayRunner
runner = GatewayRunner.__new__(GatewayRunner)
runner.config = {"quick_commands": {"limits": {"type": "exec", "command": "echo ok"}}}
runner._running_agents = {}
@ -156,7 +156,7 @@ class TestGatewayQuickCommands:
@pytest.mark.asyncio
async def test_unsupported_type_returns_error(self):
from gateway.run import GatewayRunner
from hermes_agent.gateway.run import GatewayRunner
runner = GatewayRunner.__new__(GatewayRunner)
runner.config = {"quick_commands": {"bad": {"type": "prompt", "command": "echo hi"}}}
runner._running_agents = {}
@ -170,7 +170,7 @@ class TestGatewayQuickCommands:
@pytest.mark.asyncio
async def test_timeout_returns_error(self):
from gateway.run import GatewayRunner
from hermes_agent.gateway.run import GatewayRunner
import asyncio
runner = GatewayRunner.__new__(GatewayRunner)
runner.config = {"quick_commands": {"slow": {"type": "exec", "command": "sleep 100"}}}
@ -186,8 +186,8 @@ class TestGatewayQuickCommands:
@pytest.mark.asyncio
async def test_gateway_config_object_supports_quick_commands(self):
from gateway.config import GatewayConfig
from gateway.run import GatewayRunner
from hermes_agent.gateway.config import GatewayConfig
from hermes_agent.gateway.run import GatewayRunner
runner = GatewayRunner.__new__(GatewayRunner)
runner.config = GatewayConfig(

View file

@ -22,7 +22,7 @@ class TestParseReasoningConfig(unittest.TestCase):
"""Verify _parse_reasoning_config handles all effort levels."""
def _parse(self, effort):
from cli import _parse_reasoning_config
from hermes_agent.cli.repl import _parse_reasoning_config
return _parse_reasoning_config(effort)
def test_none_disables(self):
@ -101,7 +101,7 @@ class TestHandleReasoningCommand(unittest.TestCase):
def test_effort_level_sets_config(self):
"""Setting an effort level should update reasoning_config."""
from cli import _parse_reasoning_config
from hermes_agent.cli.repl import _parse_reasoning_config
stub = self._make_cli()
arg = "high"
parsed = _parse_reasoning_config(arg)
@ -109,7 +109,7 @@ class TestHandleReasoningCommand(unittest.TestCase):
self.assertEqual(stub.reasoning_config, {"enabled": True, "effort": "high"})
def test_effort_none_disables_reasoning(self):
from cli import _parse_reasoning_config
from hermes_agent.cli.repl import _parse_reasoning_config
stub = self._make_cli()
parsed = _parse_reasoning_config("none")
stub.reasoning_config = parsed
@ -117,7 +117,7 @@ class TestHandleReasoningCommand(unittest.TestCase):
def test_invalid_argument_rejected(self):
"""Invalid arguments should be rejected (parsed returns None)."""
from cli import _parse_reasoning_config
from hermes_agent.cli.repl import _parse_reasoning_config
parsed = _parse_reasoning_config("turbo")
self.assertIsNone(parsed)
@ -298,7 +298,7 @@ class TestReasoningCallback(unittest.TestCase):
class TestReasoningPreviewBuffering(unittest.TestCase):
def _make_cli(self):
from cli import HermesCLI
from hermes_agent.cli.repl import HermesCLI
cli = HermesCLI.__new__(HermesCLI)
cli.verbose = True
@ -307,7 +307,7 @@ class TestReasoningPreviewBuffering(unittest.TestCase):
cli._invalidate = lambda *args, **kwargs: None
return cli
@patch("cli._cprint")
@patch("hermes_agent.cli.repl._cprint")
def test_streamed_reasoning_chunks_wait_for_boundary(self, mock_cprint):
cli = self._make_cli()
@ -323,7 +323,7 @@ class TestReasoningPreviewBuffering(unittest.TestCase):
rendered = mock_cprint.call_args[0][0]
self.assertIn("[thinking] Let me think about this.", rendered)
@patch("cli._cprint")
@patch("hermes_agent.cli.repl._cprint")
def test_pending_reasoning_flushes_when_thinking_stops(self, mock_cprint):
cli = self._make_cli()
@ -341,8 +341,8 @@ class TestReasoningPreviewBuffering(unittest.TestCase):
rendered = mock_cprint.call_args[0][0]
self.assertIn("[thinking] see how this plays out", rendered)
@patch("cli._cprint")
@patch("cli.shutil.get_terminal_size", return_value=SimpleNamespace(columns=50))
@patch("hermes_agent.cli.repl._cprint")
@patch("hermes_agent.cli.repl.shutil.get_terminal_size", return_value=SimpleNamespace(columns=50))
def test_reasoning_preview_compacts_newlines_and_wraps_to_terminal(self, _mock_term, mock_cprint):
cli = self._make_cli()
@ -357,7 +357,7 @@ class TestReasoningPreviewBuffering(unittest.TestCase):
self.assertIn("Second paragraph with more detail here.", normalized)
self.assertNotIn("\n\n\n", plain)
@patch("cli.shutil.get_terminal_size", return_value=SimpleNamespace(columns=60))
@patch("hermes_agent.cli.repl.shutil.get_terminal_size", return_value=SimpleNamespace(columns=60))
def test_reasoning_flush_threshold_tracks_terminal_width(self, _mock_term):
cli = self._make_cli()
@ -368,7 +368,7 @@ class TestReasoningPreviewBuffering(unittest.TestCase):
class TestReasoningDisplayModeSelection(unittest.TestCase):
def _make_cli(self, *, show_reasoning=False, streaming_enabled=False, verbose=False):
from cli import HermesCLI
from hermes_agent.cli.repl import HermesCLI
cli = HermesCLI.__new__(HermesCLI)
cli.show_reasoning = show_reasoning
@ -406,7 +406,7 @@ class TestExtractReasoningFormats(unittest.TestCase):
"""Test _extract_reasoning with real provider response formats."""
def _get_extractor(self):
from run_agent import AIAgent
from hermes_agent.agent.loop import AIAgent
return AIAgent._extract_reasoning
def test_openrouter_reasoning_details(self):
@ -466,7 +466,7 @@ class TestInlineThinkBlockExtraction(unittest.TestCase):
def _make_agent(self):
"""Create a minimal agent with _build_assistant_message."""
from run_agent import AIAgent
from hermes_agent.agent.loop import AIAgent
agent = MagicMock(spec=AIAgent)
agent._build_assistant_message = AIAgent._build_assistant_message.__get__(agent)
agent._extract_reasoning = AIAgent._extract_reasoning.__get__(agent)
@ -539,7 +539,7 @@ class TestConfigDefault(unittest.TestCase):
"""Verify config default for show_reasoning."""
def test_default_config_has_show_reasoning(self):
from hermes_cli.config import DEFAULT_CONFIG
from hermes_agent.cli.config import DEFAULT_CONFIG
display = DEFAULT_CONFIG.get("display", {})
self.assertIn("show_reasoning", display)
self.assertFalse(display["show_reasoning"])
@ -549,7 +549,7 @@ class TestCommandRegistered(unittest.TestCase):
"""Verify /reasoning is in the COMMANDS dict."""
def test_reasoning_in_commands(self):
from hermes_cli.commands import COMMANDS
from hermes_agent.cli.commands import COMMANDS
self.assertIn("/reasoning", COMMANDS)
@ -561,7 +561,7 @@ class TestEndToEndPipeline(unittest.TestCase):
"""Simulate the full pipeline: extraction -> result dict -> display."""
def test_openrouter_claude_pipeline(self):
from run_agent import AIAgent
from hermes_agent.agent.loop import AIAgent
api_message = SimpleNamespace(
role="assistant",
@ -597,7 +597,7 @@ class TestEndToEndPipeline(unittest.TestCase):
self.assertIn("Python list methods", result["last_reasoning"])
def test_no_reasoning_model_pipeline(self):
from run_agent import AIAgent
from hermes_agent.agent.loop import AIAgent
api_message = SimpleNamespace(content="Paris.", tool_calls=None)
reasoning = AIAgent._extract_reasoning(None, api_message)
@ -616,7 +616,7 @@ class TestReasoningDeltasFiredFlag(unittest.TestCase):
reasoning was already streamed via _fire_reasoning_delta."""
def _make_agent(self):
from run_agent import AIAgent
from hermes_agent.agent.loop import AIAgent
agent = AIAgent.__new__(AIAgent)
agent.reasoning_callback = None
agent.stream_delta_callback = None
@ -704,7 +704,7 @@ class TestReasoningShownThisTurnFlag(unittest.TestCase):
was already shown during streaming in a tool-calling loop."""
def _make_cli(self):
from cli import HermesCLI
from hermes_agent.cli.repl import HermesCLI
cli = HermesCLI.__new__(HermesCLI)
cli.show_reasoning = True
cli.streaming_enabled = True
@ -721,14 +721,14 @@ class TestReasoningShownThisTurnFlag(unittest.TestCase):
cli._reasoning_preview_buf = ""
return cli
@patch("cli._cprint")
@patch("hermes_agent.cli.repl._cprint")
def test_streaming_reasoning_sets_turn_flag(self, mock_cprint):
cli = self._make_cli()
self.assertFalse(cli._reasoning_shown_this_turn)
cli._stream_reasoning_delta("Thinking about it...")
self.assertTrue(cli._reasoning_shown_this_turn)
@patch("cli._cprint")
@patch("hermes_agent.cli.repl._cprint")
def test_turn_flag_survives_reset_stream_state(self, mock_cprint):
"""_reasoning_shown_this_turn must NOT be cleared by
_reset_stream_state (called at intermediate turn boundaries)."""
@ -742,7 +742,7 @@ class TestReasoningShownThisTurnFlag(unittest.TestCase):
# Flag must persist
self.assertTrue(cli._reasoning_shown_this_turn)
@patch("cli._cprint")
@patch("hermes_agent.cli.repl._cprint")
def test_turn_flag_cleared_before_new_turn(self, mock_cprint):
"""The turn flag should be reset at the start of a new user turn.
This happens outside _reset_stream_state, at the call site."""

View file

@ -12,13 +12,11 @@ from unittest.mock import MagicMock, patch
import pytest
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
def _make_cli(config_overrides=None, env_overrides=None, **kwargs):
"""Create a HermesCLI instance with minimal mocking."""
import cli as _cli_mod
from cli import HermesCLI
import hermes_agent.cli.repl as _cli_mod
from hermes_agent.cli.repl import HermesCLI
_clean_config = {
"model": {
@ -41,7 +39,7 @@ def _make_cli(config_overrides=None, env_overrides=None, **kwargs):
if env_overrides:
clean_env.update(env_overrides)
with (
patch("cli.get_tool_definitions", return_value=[]),
patch("hermes_agent.cli.repl.get_tool_definitions", return_value=[]),
patch.dict("os.environ", clean_env, clear=False),
patch.dict(_cli_mod.__dict__, {"CLI_CONFIG": _clean_config}),
):
@ -627,15 +625,15 @@ class TestResumeDisplayConfig:
def test_default_config_has_resume_display(self):
"""DEFAULT_CONFIG in hermes_cli/config.py includes resume_display."""
from hermes_cli.config import DEFAULT_CONFIG
from hermes_agent.cli.config import DEFAULT_CONFIG
display = DEFAULT_CONFIG.get("display", {})
assert "resume_display" in display
assert display["resume_display"] == "full"
def test_cli_defaults_have_resume_display(self):
"""cli.py load_cli_config defaults include resume_display."""
import cli as _cli_mod
from cli import load_cli_config
import hermes_agent.cli.repl as _cli_mod
from hermes_agent.cli.repl import load_cli_config
with (
patch("pathlib.Path.exists", return_value=False),

View file

@ -1,10 +1,10 @@
import pytest
from unittest.mock import MagicMock, patch
from hermes_cli.plugins import VALID_HOOKS, PluginManager
from hermes_agent.cli.plugins import VALID_HOOKS, PluginManager
import os
import shutil
import tempfile
from cli import HermesCLI
from hermes_agent.cli.repl import HermesCLI
def test_session_hooks_in_valid_hooks():
@ -13,7 +13,7 @@ def test_session_hooks_in_valid_hooks():
assert "on_session_reset" in VALID_HOOKS
@patch("hermes_cli.plugins.invoke_hook")
@patch("hermes_agent.cli.plugins.invoke_hook")
def test_session_finalize_on_reset(mock_invoke_hook):
"""Verify on_session_finalize fires when /new or /reset is used."""
cli = HermesCLI()
@ -33,10 +33,10 @@ def test_session_finalize_on_reset(mock_invoke_hook):
)
@patch("hermes_cli.plugins.invoke_hook")
@patch("hermes_agent.cli.plugins.invoke_hook")
def test_session_finalize_on_cleanup(mock_invoke_hook):
"""Verify on_session_finalize fires during CLI exit cleanup."""
import cli as cli_mod
import hermes_agent.cli.repl as cli_mod
mock_agent = MagicMock()
mock_agent.session_id = "cleanup-session-id"
@ -50,7 +50,7 @@ def test_session_finalize_on_cleanup(mock_invoke_hook):
)
@patch("hermes_cli.plugins.invoke_hook")
@patch("hermes_agent.cli.plugins.invoke_hook")
def test_hook_errors_are_caught(mock_invoke_hook):
"""Verify hook exceptions are caught and don't crash the agent."""
mgr = PluginManager()

View file

@ -1,14 +1,12 @@
"""Tests for _stream_delta's handling of <think> tags in prose vs real reasoning blocks."""
import sys
import os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", ".."))
import pytest
def _make_cli_stub():
"""Create a minimal HermesCLI-like object with stream state."""
from cli import HermesCLI
from hermes_agent.cli.repl import HermesCLI
cli = HermesCLI.__new__(HermesCLI)
cli.show_reasoning = False
@ -130,7 +128,7 @@ class TestFlushRecovery:
from unittest.mock import patch
import shutil
with patch.object(shutil, "get_terminal_size", return_value=os.terminal_size((80, 24))):
with patch("cli._cprint"):
with patch("hermes_agent.cli.repl._cprint"):
cli._flush_stream()
assert not cli._in_reasoning_block

View file

@ -9,7 +9,7 @@ import json
import pytest
from unittest.mock import MagicMock, patch
from run_agent import (
from hermes_agent.agent.loop import (
_sanitize_surrogates,
_sanitize_messages_surrogates,
_sanitize_structure_surrogates,
@ -294,12 +294,12 @@ class TestApiMessagesSurrogateRecovery:
class TestRunConversationSurrogateSanitization:
"""Integration: verify run_conversation sanitizes user_message."""
@patch("run_agent.AIAgent._build_system_prompt")
@patch("run_agent.AIAgent._interruptible_streaming_api_call")
@patch("run_agent.AIAgent._interruptible_api_call")
@patch("hermes_agent.agent.loop.AIAgent._build_system_prompt")
@patch("hermes_agent.agent.loop.AIAgent._interruptible_streaming_api_call")
@patch("hermes_agent.agent.loop.AIAgent._interruptible_api_call")
def test_user_message_surrogates_sanitized(self, mock_api, mock_stream, mock_sys):
"""Surrogates in user_message are stripped before API call."""
from run_agent import AIAgent
from hermes_agent.agent.loop import AIAgent
mock_sys.return_value = "system prompt"

View file

@ -10,8 +10,6 @@ import sys
import importlib
from unittest.mock import MagicMock, patch
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
# Module-level reference to the cli module (set by _make_cli on first call)
_cli_mod = None
@ -49,7 +47,7 @@ def _make_cli(tool_progress="all"):
}
with patch.dict(sys.modules, prompt_toolkit_stubs), \
patch.dict("os.environ", clean_env, clear=False):
import cli as mod
import hermes_agent.cli.repl as mod
mod = importlib.reload(mod)
_cli_mod = mod
with patch.object(mod, "get_tool_definitions", return_value=[]), \
@ -79,10 +77,10 @@ class TestToolProgressScrollback:
cli = _make_cli(tool_progress="all")
with patch.object(_cli_mod, "_cprint") as mock_print:
# First call
cli._on_tool_progress("tool.started", "read_file", "cli.py", {"path": "cli.py"})
cli._on_tool_progress("tool.started", "read_file", "hermes_agent/cli/repl.py", {"path": "hermes_agent/cli/repl.py"})
cli._on_tool_progress("tool.completed", "read_file", None, None, duration=0.1, is_error=False)
# Second call (same tool)
cli._on_tool_progress("tool.started", "read_file", "run_agent.py", {"path": "run_agent.py"})
cli._on_tool_progress("tool.started", "read_file", "hermes_agent/agent/loop.py", {"path": "hermes_agent/agent/loop.py"})
cli._on_tool_progress("tool.completed", "read_file", None, None, duration=0.2, is_error=False)
assert mock_print.call_count == 2
@ -91,9 +89,9 @@ class TestToolProgressScrollback:
"""In 'new' mode, consecutive calls to the same tool only print once."""
cli = _make_cli(tool_progress="new")
with patch.object(_cli_mod, "_cprint") as mock_print:
cli._on_tool_progress("tool.started", "read_file", "cli.py", {"path": "cli.py"})
cli._on_tool_progress("tool.started", "read_file", "hermes_agent/cli/repl.py", {"path": "hermes_agent/cli/repl.py"})
cli._on_tool_progress("tool.completed", "read_file", None, None, duration=0.1, is_error=False)
cli._on_tool_progress("tool.started", "read_file", "run_agent.py", {"path": "run_agent.py"})
cli._on_tool_progress("tool.started", "read_file", "hermes_agent/agent/loop.py", {"path": "hermes_agent/agent/loop.py"})
cli._on_tool_progress("tool.completed", "read_file", None, None, duration=0.2, is_error=False)
assert mock_print.call_count == 1 # Only the first read_file
@ -102,11 +100,11 @@ class TestToolProgressScrollback:
"""In 'new' mode, a different tool name triggers a new line."""
cli = _make_cli(tool_progress="new")
with patch.object(_cli_mod, "_cprint") as mock_print:
cli._on_tool_progress("tool.started", "read_file", "cli.py", {"path": "cli.py"})
cli._on_tool_progress("tool.started", "read_file", "hermes_agent/cli/repl.py", {"path": "hermes_agent/cli/repl.py"})
cli._on_tool_progress("tool.completed", "read_file", None, None, duration=0.1, is_error=False)
cli._on_tool_progress("tool.started", "search_files", "pattern", {"pattern": "test"})
cli._on_tool_progress("tool.completed", "search_files", None, None, duration=0.3, is_error=False)
cli._on_tool_progress("tool.started", "read_file", "run_agent.py", {"path": "run_agent.py"})
cli._on_tool_progress("tool.started", "read_file", "hermes_agent/agent/loop.py", {"path": "hermes_agent/agent/loop.py"})
cli._on_tool_progress("tool.completed", "read_file", None, None, duration=0.2, is_error=False)
# read_file, search_files, read_file (3rd prints because search_files broke the streak)

View file

@ -39,7 +39,7 @@ def _force_remove_worktree(info: dict | None) -> None:
class TestWorktreeIncludeSecurity:
def test_rejects_parent_directory_file_traversal(self, git_repo):
import cli as cli_mod
import hermes_agent.cli.repl as cli_mod
outside_file = git_repo.parent / "sensitive.txt"
outside_file.write_text("SENSITIVE DATA")
@ -57,7 +57,7 @@ class TestWorktreeIncludeSecurity:
_force_remove_worktree(info)
def test_rejects_parent_directory_directory_traversal(self, git_repo):
import cli as cli_mod
import hermes_agent.cli.repl as cli_mod
outside_dir = git_repo.parent / "outside-dir"
outside_dir.mkdir()
@ -77,7 +77,7 @@ class TestWorktreeIncludeSecurity:
_force_remove_worktree(info)
def test_rejects_symlink_that_resolves_outside_repo(self, git_repo):
import cli as cli_mod
import hermes_agent.cli.repl as cli_mod
outside_file = git_repo.parent / "linked-secret.txt"
outside_file.write_text("LINKED SECRET")
@ -94,7 +94,7 @@ class TestWorktreeIncludeSecurity:
_force_remove_worktree(info)
def test_allows_valid_file_include(self, git_repo):
import cli as cli_mod
import hermes_agent.cli.repl as cli_mod
(git_repo / ".env").write_text("SECRET=***\n")
(git_repo / ".worktreeinclude").write_text(".env\n")
@ -111,7 +111,7 @@ class TestWorktreeIncludeSecurity:
_force_remove_worktree(info)
def test_allows_valid_directory_include(self, git_repo):
import cli as cli_mod
import hermes_agent.cli.repl as cli_mod
assets_dir = git_repo / ".venv" / "lib"
assets_dir.mkdir(parents=True)