Files
hermes-agent/tests/tui_gateway/test_interrupt_agent_loop_stopped_hook.py
T
Teknium d3202bbc8d feat(tui_gateway): fire agent_loop_stopped on session.interrupt too
Widens the new hook to the sibling interrupt surface: the TUI/desktop
session.interrupt path stops a live turn exactly like the gateway's
/stop, so plugins holding per-turn external resources get the same
signal there (platform='tui'). Gated on a genuinely running turn;
dispatch failures are swallowed so a plugin can never break the
interrupt. Docs updated to describe both surfaces.

Inspired by ChatGPT Work / Codex CLI 0.150.0 'Interrupt' hooks
(hooks that run when an active top-level turn is interrupted).
2026-09-12 22:19:44 -07:00

71 lines
2.2 KiB
Python

"""The TUI/desktop ``session.interrupt`` path is a sibling of the gateway's
``_interrupt_and_clear_session``: when a live turn is stopped, plugins holding
per-turn external resources need the same ``agent_loop_stopped`` signal.
"""
import threading
from unittest.mock import MagicMock, patch
def _make_session(running: bool) -> dict:
return {
"history_lock": threading.Lock(),
"running": running,
"queued_prompt": None,
"session_key": "agent:main:tui:dm:s1",
"agent": MagicMock(),
"_run_thread": None,
}
def _hook_calls(mock_invoke_hook):
return [
call
for call in mock_invoke_hook.call_args_list
if call.args and call.args[0] == "agent_loop_stopped"
]
@patch("hermes_cli.plugins.invoke_hook")
def test_interrupt_running_turn_fires_agent_loop_stopped(mock_invoke_hook):
from tui_gateway import server
session = _make_session(running=True)
with patch.object(server, "_clear_pending"):
server._interrupt_session_turn("s1", session)
calls = _hook_calls(mock_invoke_hook)
assert len(calls) == 1
assert calls[0].kwargs == {
"session_key": "agent:main:tui:dm:s1",
"platform": "tui",
"reason": "user_stop",
"invalidation_reason": "session_interrupt",
}
@patch("hermes_cli.plugins.invoke_hook")
def test_interrupt_idle_session_does_not_fire_hook(mock_invoke_hook):
"""No live turn -> nothing for a plugin to cancel -> no hook noise."""
from tui_gateway import server
session = _make_session(running=False)
with patch.object(server, "_clear_pending"):
server._interrupt_session_turn("s1", session)
assert _hook_calls(mock_invoke_hook) == []
@patch("hermes_cli.plugins.invoke_hook")
def test_hook_failure_does_not_break_interrupt(mock_invoke_hook):
"""A misbehaving plugin must never prevent the interrupt itself."""
mock_invoke_hook.side_effect = RuntimeError("plugin exploded")
from tui_gateway import server
session = _make_session(running=True)
with patch.object(server, "_clear_pending"):
server._interrupt_session_turn("s1", session)
# The cancel flag was still set despite the hook blowing up.
assert session["_turn_cancel_requested"] is True