mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-25 00:51:20 +00:00
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
146 lines
5.8 KiB
Python
146 lines
5.8 KiB
Python
"""Regression tests for classic-CLI mid-run /steer dispatch.
|
|
|
|
Background
|
|
----------
|
|
/steer sent while the agent is running used to be queued through
|
|
``self._pending_input`` alongside ordinary user input. ``process_loop``
|
|
pulls from that queue and calls ``process_command()`` — but while the
|
|
agent is running, ``process_loop`` is blocked inside ``self.chat()``.
|
|
By the time the queued /steer was pulled, ``_agent_running`` had
|
|
already flipped back to False, so ``process_command()`` took the idle
|
|
fallback (``"No agent running; queued as next turn"``) and delivered
|
|
the steer as an ordinary next-turn message.
|
|
|
|
The fix dispatches /steer inline on the UI thread when the agent is
|
|
running — matching the existing pattern for /model — so the steer
|
|
reaches ``agent.steer()`` (thread-safe) without touching the queue.
|
|
|
|
These tests exercise the detector + inline dispatch without starting a
|
|
prompt_toolkit app.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import importlib
|
|
import sys
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
|
|
def _make_cli():
|
|
"""Create a HermesCLI instance with prompt_toolkit stubbed out."""
|
|
_clean_config = {
|
|
"model": {
|
|
"default": "anthropic/claude-opus-4.6",
|
|
"base_url": "https://openrouter.ai/api/v1",
|
|
"provider": "auto",
|
|
},
|
|
"display": {"compact": False, "tool_progress": "all"},
|
|
"agent": {},
|
|
"terminal": {"env_type": "local"},
|
|
}
|
|
clean_env = {"LLM_MODEL": "", "HERMES_MAX_ITERATIONS": ""}
|
|
prompt_toolkit_stubs = {
|
|
"prompt_toolkit": MagicMock(),
|
|
"prompt_toolkit.history": MagicMock(),
|
|
"prompt_toolkit.styles": MagicMock(),
|
|
"prompt_toolkit.patch_stdout": MagicMock(),
|
|
"prompt_toolkit.application": MagicMock(),
|
|
"prompt_toolkit.layout": MagicMock(),
|
|
"prompt_toolkit.layout.processors": MagicMock(),
|
|
"prompt_toolkit.filters": MagicMock(),
|
|
"prompt_toolkit.layout.dimension": MagicMock(),
|
|
"prompt_toolkit.layout.menus": MagicMock(),
|
|
"prompt_toolkit.widgets": MagicMock(),
|
|
"prompt_toolkit.key_binding": MagicMock(),
|
|
"prompt_toolkit.completion": MagicMock(),
|
|
"prompt_toolkit.formatted_text": MagicMock(),
|
|
"prompt_toolkit.auto_suggest": MagicMock(),
|
|
}
|
|
with patch.dict(sys.modules, prompt_toolkit_stubs), patch.dict(
|
|
"os.environ", clean_env, clear=False
|
|
):
|
|
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}
|
|
):
|
|
return _cli_mod.HermesCLI()
|
|
|
|
|
|
class TestSteerInlineDetector:
|
|
"""_should_handle_steer_command_inline gates the busy-path fast dispatch."""
|
|
|
|
def test_detects_steer_when_agent_running(self):
|
|
cli = _make_cli()
|
|
cli._agent_running = True
|
|
assert cli._should_handle_steer_command_inline("/steer focus on error handling") is True
|
|
|
|
def test_ignores_steer_when_agent_idle(self):
|
|
"""Idle-path /steer should fall through to the normal process_loop
|
|
dispatch so the queue-style fallback message is emitted."""
|
|
cli = _make_cli()
|
|
cli._agent_running = False
|
|
assert cli._should_handle_steer_command_inline("/steer do something") is False
|
|
|
|
def test_ignores_non_slash_input(self):
|
|
cli = _make_cli()
|
|
cli._agent_running = True
|
|
assert cli._should_handle_steer_command_inline("steer without slash") is False
|
|
assert cli._should_handle_steer_command_inline("") is False
|
|
|
|
def test_ignores_other_slash_commands(self):
|
|
cli = _make_cli()
|
|
cli._agent_running = True
|
|
assert cli._should_handle_steer_command_inline("/queue hello") is False
|
|
assert cli._should_handle_steer_command_inline("/stop") is False
|
|
assert cli._should_handle_steer_command_inline("/help") is False
|
|
|
|
def test_ignores_steer_with_attached_images(self):
|
|
"""Image payloads take the normal path; steer doesn't accept images."""
|
|
cli = _make_cli()
|
|
cli._agent_running = True
|
|
assert cli._should_handle_steer_command_inline("/steer text", has_images=True) is False
|
|
|
|
|
|
class TestSteerBusyPathDispatch:
|
|
"""When the detector fires, process_command('/steer ...') must call
|
|
agent.steer() directly rather than the idle-path fallback."""
|
|
|
|
def test_process_command_routes_to_agent_steer(self):
|
|
"""With _agent_running=True and agent.steer present, /steer reaches
|
|
agent.steer(payload), NOT _pending_input."""
|
|
cli = _make_cli()
|
|
cli._agent_running = True
|
|
cli.agent = MagicMock()
|
|
cli.agent.steer = MagicMock(return_value=True)
|
|
# Make sure the idle-path fallback would be observable if taken
|
|
cli._pending_input = MagicMock()
|
|
|
|
cli.process_command("/steer focus on errors")
|
|
|
|
cli.agent.steer.assert_called_once_with("focus on errors")
|
|
cli._pending_input.put.assert_not_called()
|
|
|
|
def test_idle_path_queues_as_next_turn(self):
|
|
"""Control — when the agent is NOT running, /steer correctly falls
|
|
back to next-turn queue semantics. Demonstrates why the fix was
|
|
needed: the queue path only works when you can actually drain it."""
|
|
cli = _make_cli()
|
|
cli._agent_running = False
|
|
cli.agent = MagicMock()
|
|
cli.agent.steer = MagicMock(return_value=True)
|
|
cli._pending_input = MagicMock()
|
|
|
|
cli.process_command("/steer would-be-next-turn")
|
|
|
|
# Idle path does NOT call agent.steer
|
|
cli.agent.steer.assert_not_called()
|
|
# It puts the payload in the queue as a normal next-turn message
|
|
cli._pending_input.put.assert_called_once_with("would-be-next-turn")
|
|
|
|
|
|
if __name__ == "__main__": # pragma: no cover
|
|
import pytest
|
|
|
|
pytest.main([__file__, "-v"])
|