Download tests/gateway/test_agent_loop_stopped_hook.py from SaylorTwift/hermes-agent: direct link, hf CLI and curl.
- Browser
- Download file 5.75 kB
-
https://huggingface.co/SaylorTwift/hermes-agent/resolve/main/tests/gateway/test_agent_loop_stopped_hook.py
- Command line
-
hf download hf://SaylorTwift/hermes-agent/tests/gateway/test_agent_loop_stopped_hook.py
-
curl -L -o test_agent_loop_stopped_hook.py https://huggingface.co/SaylorTwift/hermes-agent/resolve/main/tests/gateway/test_agent_loop_stopped_hook.py
5.75 kB
| """Tests that the ``agent_loop_stopped`` plugin hook fires when the gateway | |
| interrupts a running agent turn — both via ``/stop`` and via the running-agent | |
| fast-path inside ``/new``. | |
| Mirrors ``test_session_boundary_hooks.py``: we bypass ``GatewayRunner.__init__`` | |
| and stub just enough attributes to drive ``_interrupt_and_clear_session``. | |
| """ | |
| from types import SimpleNamespace | |
| from unittest.mock import AsyncMock, MagicMock, patch | |
| import pytest | |
| from gateway.config import GatewayConfig, Platform, PlatformConfig | |
| from gateway.session import SessionSource, build_session_key | |
| from hermes_cli.plugins import VALID_HOOKS | |
| def _make_source() -> SessionSource: | |
| return SessionSource( | |
| platform=Platform.TELEGRAM, | |
| user_id="u1", | |
| chat_id="c1", | |
| user_name="tester", | |
| chat_type="dm", | |
| ) | |
| def _make_runner(): | |
| from gateway.run import GatewayRunner | |
| runner = object.__new__(GatewayRunner) | |
| runner.config = GatewayConfig( | |
| platforms={Platform.TELEGRAM: PlatformConfig(enabled=True, token="***")} | |
| ) | |
| # SimpleNamespace, not MagicMock — _interrupt_and_clear_session calls | |
| # adapter.interrupt_session_activity() only when hasattr(...) is true, | |
| # and MagicMock fakes every attribute, which would push us into an | |
| # await on a non-awaitable. | |
| adapter = SimpleNamespace(send=AsyncMock()) | |
| runner.adapters = {Platform.TELEGRAM: adapter} | |
| runner.hooks = SimpleNamespace(emit=AsyncMock(), loaded_hooks=False) | |
| runner._running_agents = {} | |
| runner._pending_messages = {} | |
| # _invalidate_session_run_generation + _release_running_agent_state are | |
| # called downstream; stub to no-op so we exercise the hook emit only. | |
| runner._invalidate_session_run_generation = lambda *a, **kw: None | |
| runner._release_running_agent_state = lambda *a, **kw: None | |
| return runner | |
| def test_agent_loop_stopped_in_valid_hooks(): | |
| """Plugins must be able to subscribe to the new hook without warnings.""" | |
| assert "agent_loop_stopped" in VALID_HOOKS | |
| async def test_interrupt_fires_agent_loop_stopped_hook(mock_invoke_hook): | |
| """Calling ``_interrupt_and_clear_session`` with a real running agent | |
| must interrupt it AND dispatch the hook with the session, platform, | |
| and reasons attached.""" | |
| runner = _make_runner() | |
| source = _make_source() | |
| session_key = build_session_key(source) | |
| running_agent = MagicMock() | |
| runner._running_agents[session_key] = running_agent | |
| await runner._interrupt_and_clear_session( | |
| session_key, | |
| source, | |
| interrupt_reason="user_stop", | |
| invalidation_reason="stop_command", | |
| ) | |
| running_agent.interrupt.assert_called_once_with("user_stop") | |
| mock_invoke_hook.assert_any_call( | |
| "agent_loop_stopped", | |
| session_key=session_key, | |
| platform="telegram", | |
| reason="user_stop", | |
| invalidation_reason="stop_command", | |
| ) | |
| async def test_hook_not_fired_for_pending_sentinel_stop(mock_invoke_hook): | |
| """The pending-sentinel /stop path (slash_commands.py) has no in-flight | |
| work to cancel — the agent loop never started. The hook is gated on a | |
| real running agent and must not fire in this case.""" | |
| from gateway.run import _AGENT_PENDING_SENTINEL | |
| runner = _make_runner() | |
| source = _make_source() | |
| session_key = build_session_key(source) | |
| runner._running_agents[session_key] = _AGENT_PENDING_SENTINEL | |
| await runner._interrupt_and_clear_session( | |
| session_key, | |
| source, | |
| interrupt_reason="user_stop", | |
| invalidation_reason="stop_command_pending", | |
| ) | |
| agent_loop_stopped_calls = [ | |
| call for call in mock_invoke_hook.call_args_list | |
| if call.args and call.args[0] == "agent_loop_stopped" | |
| ] | |
| assert agent_loop_stopped_calls == [], ( | |
| f"agent_loop_stopped must not fire on the pending-sentinel path " | |
| f"(saw {len(agent_loop_stopped_calls)} call(s))" | |
| ) | |
| async def test_hook_not_fired_when_no_running_agent(mock_invoke_hook): | |
| """If there is no agent at all for the session key (already cleared), | |
| there is nothing to interrupt and the hook must not fire.""" | |
| runner = _make_runner() | |
| source = _make_source() | |
| session_key = build_session_key(source) | |
| # _running_agents stays empty — no entry for this session_key. | |
| await runner._interrupt_and_clear_session( | |
| session_key, | |
| source, | |
| interrupt_reason="user_stop", | |
| invalidation_reason="stop_command_no_agent", | |
| ) | |
| agent_loop_stopped_calls = [ | |
| call for call in mock_invoke_hook.call_args_list | |
| if call.args and call.args[0] == "agent_loop_stopped" | |
| ] | |
| assert agent_loop_stopped_calls == [] | |
| async def test_hook_dispatch_failure_does_not_break_interrupt(mock_invoke_hook): | |
| """A misbehaving plugin must not prevent the interrupt from completing.""" | |
| mock_invoke_hook.side_effect = RuntimeError("plugin exploded") | |
| runner = _make_runner() | |
| source = _make_source() | |
| session_key = build_session_key(source) | |
| running_agent = MagicMock() | |
| runner._running_agents[session_key] = running_agent | |
| # Should not raise — the hook block has try/except for exactly this. | |
| await runner._interrupt_and_clear_session( | |
| session_key, | |
| source, | |
| interrupt_reason="user_stop", | |
| invalidation_reason="stop_command", | |
| ) | |
| # The interrupt itself still happened despite the hook blowing up. | |
| running_agent.interrupt.assert_called_once_with("user_stop") | |