test(chat): lock /init + /focus command.dispatch contract (issues #733 #734) (#735)

Both commands are real backend command.dispatch subcommands (name=="init" / name=="focus", hermes-agent 2b0fb72ac). Mobile already routes them generically via RpcDispatch (name/arg/session_id) and handles responses (type:send -> submitPrompt for /init, type:exec -> assistant message for /focus) + surfaces them from commands.catalog.

Adds 6 contract tests mirroring the existing /help + /queue + /status cases:
- /init and /focus forward name+arg+session_id to COMMAND_DISPATCH
- /init with extra arg and /focus on/off forward the arg
- type:send (init) submits the returned prompt via sendMessage
- type:exec (focus) surfaces output as an assistant message

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
This commit is contained in:
M57andM57 authored and GitHub committed 2026-07-27 20:05:22 +03:00
1 parent 5c1492a80e
commit 13895585b0
1 file changed
+160
@@ -342,4 +342,164 @@ class SlashCommandDispatchRpcTest {
?.content,
)
}
@Test
fun `slash init forwards name=init to COMMAND_DISPATCH`() =
runTest {
val (vm, sessionId) = createViewModelWithSession()
val methodSlot = slot<String>()
val paramsSlot = slot<Map<String, Any>>()
every {
HermesWsClient.request(capture(methodSlot), capture(paramsSlot), any())
} answers {
CompletableDeferred<Any?>(Unit)
}
// /init generates-or-updates AGENTS.md (backend command.dispatch
// name=="init" branch, hermes-agent tui_gateway/server.py ~L15451).
// NOT client-special-cased -> RpcDispatch.
vm.sendMessage("/init")
advanceUntilIdle()
assertEquals(WsMethods.COMMAND_DISPATCH, methodSlot.captured)
val params = paramsSlot.captured
assertEquals("init", params["name"])
assertEquals("", params["arg"])
assertEquals(sessionId, params["session_id"])
}
@Test
fun `slash init with extra arg forwards arg separately`() =
runTest {
val (vm, sessionId) = createViewModelWithSession()
val methodSlot = slot<String>()
val paramsSlot = slot<Map<String, Any>>()
every {
HermesWsClient.request(capture(methodSlot), capture(paramsSlot), any())
} answers {
CompletableDeferred<Any?>(Unit)
}
vm.sendMessage("/init extra context here")
advanceUntilIdle()
assertEquals(WsMethods.COMMAND_DISPATCH, methodSlot.captured)
val params = paramsSlot.captured
assertEquals("init", params["name"])
assertEquals("extra context here", params["arg"])
assertEquals(sessionId, params["session_id"])
}
@Test
fun `slash focus forwards name=focus to COMMAND_DISPATCH`() =
runTest {
val (vm, sessionId) = createViewModelWithSession()
val methodSlot = slot<String>()
val paramsSlot = slot<Map<String, Any>>()
every {
HermesWsClient.request(capture(methodSlot), capture(paramsSlot), any())
} answers {
CompletableDeferred<Any?>(Unit)
}
// /focus is the display-only focus view (backend command.dispatch
// name=="focus" branch, hermes-agent tui_gateway/server.py ~L15522).
// NOT client-special-cased -> RpcDispatch.
vm.sendMessage("/focus")
advanceUntilIdle()
assertEquals(WsMethods.COMMAND_DISPATCH, methodSlot.captured)
val params = paramsSlot.captured
assertEquals("focus", params["name"])
assertEquals("", params["arg"])
assertEquals(sessionId, params["session_id"])
}
@Test
fun `slash focus on off status forwards arg to COMMAND_DISPATCH`() =
runTest {
val (vm, sessionId) = createViewModelWithSession()
val methodSlot = slot<String>()
val paramsSlot = slot<Map<String, Any>>()
every {
HermesWsClient.request(capture(methodSlot), capture(paramsSlot), any())
} answers {
CompletableDeferred<Any?>(Unit)
}
vm.sendMessage("/focus on")
advanceUntilIdle()
assertEquals(WsMethods.COMMAND_DISPATCH, methodSlot.captured)
val params = paramsSlot.captured
assertEquals("focus", params["name"])
assertEquals("on", params["arg"])
assertEquals(sessionId, params["session_id"])
}
@Test
fun `command dispatch type=send submits the returned prompt (init)`() =
runTest {
val (vm, _) = createViewModelWithSession()
val methodSlot = slot<String>()
val paramsSlot = slot<Map<String, Any>>()
val sentText = slot<String>()
every {
HermesWsClient.request(capture(methodSlot), capture(paramsSlot), any())
} answers {
val d = CompletableDeferred<Any?>()
if (methodSlot.captured == WsMethods.COMMAND_DISPATCH) {
// Backend /init returns type:"send" with the AGENTS.md prompt.
d.complete(mapOf("type" to "send", "message" to "Scan this repo and write AGENTS.md"))
} else {
d.complete(mapOf("session_id" to "session-xyz"))
}
d
}
// submitPrompt() forwards the returned prompt via wsClient.sendMessage.
every {
HermesWsClient.sendMessage(any(), capture(sentText), any())
} answers {
val id = "send-msg-1"
arg<((String) -> Unit)?>(2)?.invoke(id)
id
}
vm.sendMessage("/init")
advanceUntilIdle()
// type:"send" must be forwarded to the agent as a normal prompt turn.
assertEquals("Scan this repo and write AGENTS.md", sentText.captured)
}
@Test
fun `command dispatch type=exec surfaces output as a message (focus)`() =
runTest {
val (vm, _) = createViewModelWithSession()
every {
HermesWsClient.request(any(), any(), any())
} answers {
val m = arg<String>(0)
val d = CompletableDeferred<Any?>()
if (m == WsMethods.COMMAND_DISPATCH) {
// Backend /focus returns type:"exec" with a notice line.
d.complete(mapOf("type" to "exec", "output" to "Focus view: ON (tool progress pinned off)"))
} else {
d.complete(mapOf("session_id" to "session-xyz"))
}
d
}
vm.sendMessage("/focus on")
advanceUntilIdle()
val last = vm.uiState.value.messages.lastOrNull()
assertEquals("Focus view: ON (tool progress pinned off)", last?.content)
}
}