mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-21 10:22:18 +00:00
fix(tui): route pending-input commands via command.dispatch (#48848)
When /goal (and other _PENDING_INPUT_COMMANDS: retry, queue, q, steer, plan, undo) were typed in the TUI desktop app, slash.exec returned error 4018 instructing the frontend to fall back to command.dispatch. Some clients failed that client-side fallback, leaving the command empty and surfacing "empty command" — the user's typed text was silently dropped. slash.exec now routes pending-input commands to command.dispatch internally, eliminating the fragile client-side fallback hop. The response is exactly what command.dispatch would have produced, so the TUI client behaves identically once the round-trip succeeds. Salvaged from #48944 — rebased onto current main. The original PR's source change and test_goal_command.py update are correct, but it missed the second test surface: tests/tui_gateway/test_protocol.py's parametrized test_slash_exec_rejects_pending_input_commands still asserted the old 4018 rejection for retry/queue/q/steer/plan, turning CI red (5 failures). That test is rewritten here as a behavior contract: slash.exec for a pending-input command must yield the same payload as a direct command.dispatch call, and must no longer emit the old "pending-input command" fallback rejection. Co-authored-by: kyssta-exe <kyssta-exe@users.noreply.github.com>
This commit is contained in:
parent
db57a1a035
commit
1699525638
3 changed files with 55 additions and 18 deletions
|
|
@ -185,15 +185,17 @@ def test_goal_requires_session(server):
|
|||
# ── slash.exec /goal routing ──────────────────────────────────────────
|
||||
|
||||
|
||||
def test_slash_exec_rejects_goal_routes_to_command_dispatch(server, session):
|
||||
"""slash.exec must reject /goal with 4018 so the TUI client falls through
|
||||
to command.dispatch. Without this, the HermesCLI slash-worker subprocess
|
||||
would set the goal but silently drop the kickoff — the queue is in-proc."""
|
||||
def test_slash_exec_routes_goal_to_command_dispatch(server, session):
|
||||
"""slash.exec must route /goal directly to command.dispatch internally
|
||||
instead of returning an error. Previously the 4018 error required the
|
||||
TUI client to retry via command.dispatch, but some clients failed the
|
||||
fallback, leaving the command empty ("empty command")."""
|
||||
sid, _, _ = session
|
||||
r = _call(server, "slash.exec", command="goal status", session_id=sid)
|
||||
assert "error" in r
|
||||
assert r["error"]["code"] == 4018
|
||||
assert "command.dispatch" in r["error"]["message"]
|
||||
# Should succeed by routing to command.dispatch internally
|
||||
assert "result" in r
|
||||
assert r["result"]["type"] == "exec"
|
||||
assert "No active goal" in r["result"]["output"]
|
||||
|
||||
|
||||
def test_pending_input_commands_includes_goal(server):
|
||||
|
|
|
|||
|
|
@ -1121,20 +1121,45 @@ def test_slash_exec_plugin_handler_error_returns_output(server):
|
|||
|
||||
|
||||
@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."""
|
||||
sid = "test-session"
|
||||
server._sessions[sid] = {"session_key": sid, "agent": None}
|
||||
def test_slash_exec_routes_pending_input_commands_to_dispatch(server, cmd):
|
||||
"""slash.exec must route _pending_input commands to command.dispatch
|
||||
internally instead of returning the old 4018 "use command.dispatch"
|
||||
fallback error (#48848). Some TUI clients failed that client-side
|
||||
fallback, dropping the input and surfacing "empty command".
|
||||
|
||||
resp = server.handle_request({
|
||||
The contract is that slash.exec produces exactly the response
|
||||
command.dispatch would for the same command — no fragile retry hop.
|
||||
"""
|
||||
base, _, arg = cmd.partition(" ")
|
||||
|
||||
def fresh_session():
|
||||
return {"session_key": "test-session", "agent": None}
|
||||
|
||||
sid = "test-session"
|
||||
|
||||
# Response from the (new) internal routing in slash.exec.
|
||||
server._sessions[sid] = fresh_session()
|
||||
routed = server.handle_request({
|
||||
"id": "r1",
|
||||
"method": "slash.exec",
|
||||
"params": {"command": cmd, "session_id": sid},
|
||||
})
|
||||
|
||||
assert "error" in resp
|
||||
assert resp["error"]["code"] == 4018
|
||||
assert "pending-input command" in resp["error"]["message"]
|
||||
# Response from calling command.dispatch directly with the parsed parts.
|
||||
server._sessions[sid] = fresh_session()
|
||||
direct = server.handle_request({
|
||||
"id": "r1",
|
||||
"method": "command.dispatch",
|
||||
"params": {"name": base, "arg": arg, "session_id": sid},
|
||||
})
|
||||
|
||||
# slash.exec must no longer emit the old client-fallback rejection.
|
||||
if "error" in routed:
|
||||
assert "pending-input command" not in routed["error"]["message"]
|
||||
|
||||
# Internal routing must yield the same payload as command.dispatch.
|
||||
assert routed.get("result") == direct.get("result")
|
||||
assert routed.get("error") == direct.get("error")
|
||||
|
||||
|
||||
def test_command_dispatch_queue_sends_message(server):
|
||||
|
|
|
|||
|
|
@ -8462,7 +8462,9 @@ _TUI_EXTRA: list[tuple[str, str, str]] = [
|
|||
|
||||
# Commands that queue messages onto _pending_input in the CLI.
|
||||
# In the TUI the slash worker subprocess has no reader for that queue,
|
||||
# so slash.exec rejects them → TUI falls through to command.dispatch.
|
||||
# so slash.exec routes them to command.dispatch internally (which handles
|
||||
# them and returns a structured payload) instead of erroring out and
|
||||
# relying on a client-side fallback. See #48848.
|
||||
_PENDING_INPUT_COMMANDS: frozenset[str] = frozenset(
|
||||
{
|
||||
"retry",
|
||||
|
|
@ -9729,8 +9731,16 @@ def _(rid, params: dict) -> dict:
|
|||
_cmd_arg = _cmd_parts[1] if len(_cmd_parts) > 1 else ""
|
||||
|
||||
if _cmd_base in _PENDING_INPUT_COMMANDS:
|
||||
return _err(
|
||||
rid, 4018, f"pending-input command: use command.dispatch for /{_cmd_base}"
|
||||
# Route directly to command.dispatch instead of returning an error
|
||||
# that requires the frontend to retry. Some TUI clients fail the
|
||||
# fallback, leaving the command empty and showing "empty command".
|
||||
return _methods["command.dispatch"](
|
||||
rid,
|
||||
{
|
||||
"name": _cmd_base,
|
||||
"arg": _cmd_arg,
|
||||
"session_id": params.get("session_id", ""),
|
||||
},
|
||||
)
|
||||
|
||||
if _cmd_base in _WORKER_BLOCKED_COMMANDS:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue