diff --git a/tests/tui_gateway/test_protocol.py b/tests/tui_gateway/test_protocol.py index 96df9823a6..a26a360a24 100644 --- a/tests/tui_gateway/test_protocol.py +++ b/tests/tui_gateway/test_protocol.py @@ -421,6 +421,69 @@ def test_slash_exec_handles_plugin_commands_in_live_gateway(server): assert worker.calls == [] +def test_slash_exec_plugin_lookup_failure_falls_back_to_worker(server): + """Plugin discovery failures must not break ordinary slash-worker commands.""" + sid = "test-session" + + class Worker: + def __init__(self): + self.calls = [] + + def run(self, cmd): + self.calls.append(cmd) + return f"worker:{cmd}" + + worker = Worker() + server._sessions[sid] = {"session_key": sid, "agent": None, "slash_worker": worker} + + with patch( + "hermes_cli.plugins.get_plugin_command_handler", + side_effect=RuntimeError("discovery boom"), + ): + resp = server.handle_request({ + "id": "r-plugin-lookup-failure", + "method": "slash.exec", + "params": {"command": "help", "session_id": sid}, + }) + + assert "error" not in resp + assert resp["result"] == {"output": "worker:help"} + assert worker.calls == ["help"] + + +def test_slash_exec_plugin_handler_error_returns_output(server): + """Plugin handler failures return slash output so the TUI does not redispatch.""" + sid = "test-session" + + class Worker: + def __init__(self): + self.calls = [] + + def run(self, cmd): + self.calls.append(cmd) + return f"worker:{cmd}" + + def handler(arg): + raise RuntimeError(f"handler boom: {arg}") + + worker = Worker() + server._sessions[sid] = {"session_key": sid, "agent": None, "slash_worker": worker} + + with patch( + "hermes_cli.plugins.get_plugin_command_handler", + lambda name: handler if name == "plugin-cmd" else None, + ): + resp = server.handle_request({ + "id": "r-plugin-handler-error", + "method": "slash.exec", + "params": {"command": "plugin-cmd hello", "session_id": sid}, + }) + + assert "error" not in resp + assert resp["result"] == {"output": "Plugin command error: handler boom: hello"} + assert worker.calls == [] + + @pytest.mark.parametrize("cmd", ["retry", "queue hello", "q hello", "steer fix the test", "plan"]) def test_slash_exec_rejects_pending_input_commands(server, cmd): """slash.exec must reject commands that use _pending_input in the CLI.""" diff --git a/tui_gateway/server.py b/tui_gateway/server.py index c59d358d74..fff8c8d51a 100644 --- a/tui_gateway/server.py +++ b/tui_gateway/server.py @@ -5189,19 +5189,26 @@ def _(rid, params: dict) -> dict: except Exception: pass - try: - from hermes_cli.plugins import ( - get_plugin_command_handler, - resolve_plugin_command_result, - ) + plugin_handler = None + resolve_plugin_command_result = None + if _cmd_base: + try: + from hermes_cli.plugins import ( + get_plugin_command_handler, + resolve_plugin_command_result, + ) - if _cmd_base: plugin_handler = get_plugin_command_handler(_cmd_base) - if plugin_handler: - result = resolve_plugin_command_result(plugin_handler(_cmd_arg)) - return _ok(rid, {"output": str(result or "(no output)")}) - except Exception as e: - return _err(rid, 4018, f"plugin command error: {e}") + except Exception: + plugin_handler = None + resolve_plugin_command_result = None + + if plugin_handler and resolve_plugin_command_result: + try: + result = resolve_plugin_command_result(plugin_handler(_cmd_arg)) + return _ok(rid, {"output": str(result or "(no output)")}) + except Exception as e: + return _ok(rid, {"output": f"Plugin command error: {e}"}) worker = session.get("slash_worker") if not worker: