Files
hermes-agent/tests/tui_gateway/test_entry_inline_handler_error.py
teknium1 4f1bbe1149 fix(tui): leave a crash-log breadcrumb when an inline stdio handler fails
The survived dispatch crash now appends an "inline dispatch crash" entry (method + traceback)
to tui_gateway_crash.log, the same trail a fatal exit leaves, so a degraded -32000 reply can be
traced to its handler instead of looking like a client bug. Also guard the id extraction for
non-dict requests so the error path itself cannot raise. The regression test feeds real JSON
lines instead of monkeypatching json.loads and pins the breadcrumb.

Co-authored-by: Konstantin Khlopkov <konstantin.khlopkov93@gmail.com>
2026-09-16 17:53:28 -07:00

53 lines
2.3 KiB
Python

"""Regression coverage for stdio inline-handler failures (#112816).
``entry.main()`` used to call ``dispatch(req)`` bare, so an exception from an
inline (non-pool) handler unwound the read loop and killed the gateway child,
losing the in-flight reply and wedging the TUI until a restart.
"""
from __future__ import annotations
import io
import json
from tui_gateway import entry
def test_main_reports_inline_handler_error_and_keeps_reading(monkeypatch):
"""A failed inline request must not prevent the next request from replying."""
replies: list[dict] = []
breadcrumbs: list[str] = []
stdin_text = "".join(json.dumps(req) + "\n" for req in (
{"jsonrpc": "2.0", "id": "broken", "method": "clipboard.save"},
{"jsonrpc": "2.0", "id": "next", "method": "ping"},
))
monkeypatch.setattr(entry, "_install_sidecar_publisher", lambda: None)
monkeypatch.setattr(entry, "ensure_mcp_discovery_started", lambda: None)
monkeypatch.setattr(entry, "resolve_skin", lambda: "default")
monkeypatch.setattr(entry.server, "_start_backend_heartbeat_refresher", lambda: None)
monkeypatch.setattr(entry.server, "_schedule_startup_orphan_sweep", lambda: None)
monkeypatch.setattr(entry.server, "_ensure_skin_watcher", lambda: None)
monkeypatch.setattr(entry, "handle_spurious_eof", lambda *_args: False)
monkeypatch.setattr(entry, "write_json", lambda payload: replies.append(payload) or True)
monkeypatch.setattr(entry, "_append_crash_log", lambda header, dump=None: breadcrumbs.append(header))
monkeypatch.setattr(entry.sys, "stdin", io.StringIO(stdin_text))
def dispatch(req):
if req["id"] == "broken":
raise NameError("clipboard backend is unavailable")
return {"jsonrpc": "2.0", "id": req["id"], "result": {"ok": True}}
monkeypatch.setattr(entry, "dispatch", dispatch)
entry.main()
assert replies[1] == {
"jsonrpc": "2.0",
"id": "broken",
"error": {"code": -32000, "message": "handler error: clipboard backend is unavailable"},
}
assert replies[2] == {"jsonrpc": "2.0", "id": "next", "result": {"ok": True}}
# Forensics: the survived crash leaves the same crash-log trail a fatal one would.
assert len(breadcrumbs) == 1 and "clipboard.save" in breadcrumbs[0]