fix(cli): bare /hatch no longer freezes input with an invisible raw input() prompt
This commit is contained in:
@@ -1613,11 +1613,23 @@ class CLICommandsMixin:
|
|||||||
concept = parts[1].strip() if len(parts) > 1 else ""
|
concept = parts[1].strip() if len(parts) > 1 else ""
|
||||||
|
|
||||||
if not concept:
|
if not concept:
|
||||||
try:
|
# Bare /hatch is dispatched from the process_loop daemon thread
|
||||||
concept = input("(o_o) Describe your pet: ").strip()
|
# while prompt_toolkit owns stdin — a raw input() here types into
|
||||||
except (EOFError, KeyboardInterrupt):
|
# a prompt that never renders and swallows the next keystrokes
|
||||||
print()
|
# (same class as #23185; found in the Aug 2026 full-surface CLI
|
||||||
return
|
# QA sweep: bare /hatch left the session eating input until
|
||||||
|
# Ctrl+C). Route through the thread-aware prompt helper, which
|
||||||
|
# uses run_in_terminal on the main thread and cancels cleanly
|
||||||
|
# (None) when prompting isn't safe.
|
||||||
|
prompt_helper = getattr(self, "_prompt_text_input", None)
|
||||||
|
if callable(prompt_helper):
|
||||||
|
concept = (prompt_helper("(o_o) Describe your pet: ") or "").strip()
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
concept = input("(o_o) Describe your pet: ").strip()
|
||||||
|
except (EOFError, KeyboardInterrupt):
|
||||||
|
print()
|
||||||
|
return
|
||||||
|
|
||||||
if not concept:
|
if not concept:
|
||||||
print("(o_o) Usage: /hatch <description> (e.g. /hatch a tiny cyber fox)")
|
print("(o_o) Usage: /hatch <description> (e.g. /hatch a tiny cyber fox)")
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
"""Regression test: bare /hatch must not call raw input() off the main thread.
|
||||||
|
|
||||||
|
Slash commands dispatch from the process_loop daemon thread while
|
||||||
|
prompt_toolkit owns stdin. The old code called ``input()`` directly, which
|
||||||
|
rendered nothing and silently swallowed subsequent keystrokes until Ctrl+C
|
||||||
|
(found in the Aug 2026 full-surface CLI QA sweep). The handler must route
|
||||||
|
through the thread-aware ``_prompt_text_input`` helper, which cancels
|
||||||
|
cleanly (returns None) when prompting isn't safe.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import builtins
|
||||||
|
from types import SimpleNamespace
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from hermes_cli.cli_commands_mixin import CLICommandsMixin
|
||||||
|
|
||||||
|
|
||||||
|
class _StandIn(SimpleNamespace):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def test_bare_hatch_uses_thread_aware_prompt_not_raw_input(capsys):
|
||||||
|
calls = []
|
||||||
|
|
||||||
|
def fake_prompt(prompt_text):
|
||||||
|
calls.append(prompt_text)
|
||||||
|
return None # helper cancelled (e.g. unsafe thread context)
|
||||||
|
|
||||||
|
stand_in = _StandIn(_prompt_text_input=fake_prompt)
|
||||||
|
|
||||||
|
def _boom(*a, **k): # any raw input() call is the regression
|
||||||
|
raise AssertionError("raw input() called from /hatch handler")
|
||||||
|
|
||||||
|
with patch.object(builtins, "input", _boom):
|
||||||
|
CLICommandsMixin._handle_hatch_command(stand_in, "/hatch")
|
||||||
|
|
||||||
|
assert calls, "expected /hatch to route through _prompt_text_input"
|
||||||
|
out = capsys.readouterr().out
|
||||||
|
assert "Usage: /hatch" in out # cancelled prompt falls through to usage
|
||||||
Reference in New Issue
Block a user