fix(cli): bare /hatch no longer freezes input with an invisible raw input() prompt

This commit is contained in:
Teknium
2026-08-19 18:08:12 -07:00
parent f4a866b484
commit 76bf6c7c40
2 changed files with 56 additions and 5 deletions
+17 -5
View File
@@ -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