Import AITURK IDE 1.0.0-beta.1 from Hermes 63279301; preserve MIT license
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
"""Regression test for #20271: classic-CLI hangs when messages typed during
|
||||
an agent turn never leave ``_interrupt_queue``.
|
||||
|
||||
Background
|
||||
----------
|
||||
The CLI routes user input typed while ``_agent_running`` is True into
|
||||
``_interrupt_queue`` (separate from ``_pending_input``) so that the explicit
|
||||
interrupt path can opt to deliver them as a single combined "interrupt"
|
||||
message. The explicit drain at the top of ``process_loop`` only fires when
|
||||
``busy_input_mode == "interrupt"`` AND a ``pending_message`` was
|
||||
acknowledged.
|
||||
|
||||
The original PR #17939 paired the paste-file TOCTOU fix with a separate
|
||||
drain inside ``process_loop``'s ``finally`` block: any message left in
|
||||
``_interrupt_queue`` after the agent's turn ends gets re-queued onto
|
||||
``_pending_input``. The drain was split off in #17666 / #18760 as "worth
|
||||
its own review" and never re-landed. v0.12.0 users hit a hang when typing
|
||||
during a turn that completes naturally — the message sits in
|
||||
``_interrupt_queue``, the next ``Enter`` re-routes input to the same
|
||||
blocked queue, and the CLI looks frozen.
|
||||
|
||||
This test exercises the restored ``_drain_interrupt_queue_to_pending_input``
|
||||
helper that ``process_loop`` now calls every turn. The integration into
|
||||
``process_loop`` itself is not threaded here (it requires a real
|
||||
prompt_toolkit app); the helper is unit-testable on its own and is the
|
||||
load-bearing piece.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
import queue
|
||||
import sys
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
|
||||
def _make_cli():
|
||||
"""Build a HermesCLI instance with prompt_toolkit stubbed out.
|
||||
|
||||
Mirrors the helper in ``test_cli_steer_busy_path.py``.
|
||||
"""
|
||||
_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 cli 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 TestInterruptQueueDrain:
|
||||
"""``_drain_interrupt_queue_to_pending_input`` re-queues stray messages."""
|
||||
|
||||
def test_drains_single_pending_message_into_pending_input(self):
|
||||
cli = _make_cli()
|
||||
cli._interrupt_queue.put("typed during agent turn")
|
||||
|
||||
cli._drain_interrupt_queue_to_pending_input()
|
||||
|
||||
assert cli._interrupt_queue.empty()
|
||||
assert cli._pending_input.qsize() == 1
|
||||
assert cli._pending_input.get_nowait() == "typed during agent turn"
|
||||
|
||||
def test_preserves_order_when_draining_multiple_messages(self):
|
||||
cli = _make_cli()
|
||||
for msg in ("first", "second", "third"):
|
||||
cli._interrupt_queue.put(msg)
|
||||
|
||||
cli._drain_interrupt_queue_to_pending_input()
|
||||
|
||||
assert cli._interrupt_queue.empty()
|
||||
drained = []
|
||||
while not cli._pending_input.empty():
|
||||
drained.append(cli._pending_input.get_nowait())
|
||||
assert drained == ["first", "second", "third"]
|
||||
|
||||
def test_noop_when_interrupt_queue_is_empty(self):
|
||||
cli = _make_cli()
|
||||
|
||||
cli._drain_interrupt_queue_to_pending_input()
|
||||
|
||||
assert cli._interrupt_queue.empty()
|
||||
assert cli._pending_input.empty()
|
||||
|
||||
def test_skips_falsy_messages(self):
|
||||
cli = _make_cli()
|
||||
cli._interrupt_queue.put("")
|
||||
cli._interrupt_queue.put(None)
|
||||
cli._interrupt_queue.put("real")
|
||||
|
||||
cli._drain_interrupt_queue_to_pending_input()
|
||||
|
||||
assert cli._interrupt_queue.empty()
|
||||
assert cli._pending_input.qsize() == 1
|
||||
assert cli._pending_input.get_nowait() == "real"
|
||||
|
||||
def test_swallows_exceptions_so_main_loop_never_breaks(self):
|
||||
cli = _make_cli()
|
||||
# Replace _pending_input with an object whose .put raises — simulating
|
||||
# an unexpected internal error. The drain must NOT propagate.
|
||||
broken = MagicMock(spec=queue.Queue)
|
||||
broken.put.side_effect = RuntimeError("simulated put failure")
|
||||
cli._pending_input = broken
|
||||
cli._interrupt_queue.put("anything")
|
||||
|
||||
# Should not raise.
|
||||
cli._drain_interrupt_queue_to_pending_input()
|
||||
Reference in New Issue
Block a user