refactor(#1375): type command dispatch, slash exec and attachment RPC params (#1394)

This commit is contained in:
m57 authored and GitHub committed 2026-09-30 01:09:34 +03:00
1 parent 1456e642e3
commit 26c60a10c0
8 files changed
+263 -65

No files matched your search

@@ -314,14 +314,6 @@ class RawRpcParamsRatchetTest {
* Notice HermesWsClient.kt retains its other raw uses: `CLIENT_CAPABILITIES`, `PROMPT_SUBMIT`,
* and `SESSION_REDIRECT`.
*/
val BASELINE: List<String> =
listOf(
"ui/chat/ChatViewModel.kt::COMMANDS_CATALOG",
"ui/chat/ChatViewModel.kt::COMMAND_DISPATCH",
"ui/chat/ChatViewModel.kt::COMMAND_DISPATCH",
"ui/chat/ChatViewModel.kt::FILE_ATTACH",
"ui/chat/ChatViewModel.kt::IMAGE_ATTACH_BYTES",
"ui/chat/ChatViewModel.kt::SLASH_EXEC",
)
val BASELINE: List<String> = emptyList()
}
}
@@ -0,0 +1,103 @@
package com.m57.hermescontrol.data.ws.contract
import com.m57.hermescontrol.data.remote.OkHttpProvider
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
class ChatCommandsTypesTest {
private val json = OkHttpProvider.json
@Test
fun commandsCatalogEncodesAsEmptyObject() {
assertEquals(
buildJsonObject { },
json.encodeToJsonElement(CommandsCatalogParams.serializer(), CommandsCatalogParams),
)
}
@Test
fun commandDispatchKeepsEmptyArgAndOmitsNullOnes() {
assertEquals(
buildJsonObject {
put("name", "help")
put("arg", "")
put("session_id", "s1")
},
json.encodeToJsonElement(
CommandDispatchParams.serializer(),
CommandDispatchParams(name = "help", arg = "", sessionId = "s1"),
),
)
assertEquals(
buildJsonObject { put("name", "help") },
json.encodeToJsonElement(CommandDispatchParams.serializer(), CommandDispatchParams(name = "help")),
)
}
@Test
fun slashExecEncodesSessionAndCommand() {
assertEquals(
buildJsonObject {
put("session_id", "s1")
put("command", "/status")
},
json.encodeToJsonElement(
SlashExecParams.serializer(),
SlashExecParams(sessionId = "s1", command = "/status"),
),
)
}
@Test
fun fileAttachSendsDataUrlAndNameWithoutPath() {
assertEquals(
buildJsonObject {
put("session_id", "s1")
put("data_url", "data:text/plain;base64,QQ==")
put("name", "a.txt")
},
json.encodeToJsonElement(
FileAttachParams.serializer(),
FileAttachParams(sessionId = "s1", dataUrl = "data:text/plain;base64,QQ==", name = "a.txt"),
),
)
}
@Test
fun imageAttachBytesEncodesContentBase64FilenameAndExt() {
assertEquals(
buildJsonObject {
put("session_id", "s1")
put("content_base64", "data:image/png;base64,QQ==")
put("filename", "p.png")
put("ext", "png")
},
json.encodeToJsonElement(
ImageAttachBytesParams.serializer(),
ImageAttachBytesParams(
sessionId = "s1",
contentBase64 = "data:image/png;base64,QQ==",
filename = "p.png",
ext = "png",
),
),
)
}
@Test
fun descriptorsAreRegisteredWithContractNames() {
listOf(
RpcMethods.COMMANDS_CATALOG to "commands.catalog",
RpcMethods.COMMAND_DISPATCH to "command.dispatch",
RpcMethods.SLASH_EXEC to "slash.exec",
RpcMethods.FILE_ATTACH to "file.attach",
RpcMethods.IMAGE_ATTACH_BYTES to "image.attach_bytes",
).forEach { (method, name) ->
assertTrue("$name not in RpcMethods.all", method in RpcMethods.all)
assertEquals(name, method.name)
}
}
}
@@ -1483,7 +1483,11 @@ class ChatViewModelTest {
every {
HermesWsClient.request(
WsMethods.COMMAND_DISPATCH,
mapOf("name" to "undo", "arg" to "2", "session_id" to sessionId),
buildJsonObject {
put("name", "undo")
put("arg", "2")
put("session_id", sessionId)
},
any(),
)
} returns
@@ -7238,7 +7242,7 @@ class ChatViewModelTest {
verify { HermesWsClient.request(WsMethods.IMAGE_ATTACH_BYTES, any()) }
assertEquals(
"session_id must be forwarded to image.attach_bytes",
sessionId,
JsonPrimitive(sessionId),
paramsSlot.captured["session_id"],
)
assertTrue("encoded attachment cache must be cleaned", attachmentCacheDir.listFiles().isNullOrEmpty())
@@ -36,6 +36,7 @@ import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.test.setMain
import kotlinx.serialization.json.JsonPrimitive
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
@@ -218,9 +219,9 @@ class SlashCommandDispatchRpcTest {
val dispatchIndex = methodCalls.indexOf(WsMethods.COMMAND_DISPATCH)
assertTrue("expected COMMAND_DISPATCH, got $methodCalls", dispatchIndex >= 0)
val params = paramsCalls[dispatchIndex]
assertEquals("help", params["name"])
assertEquals("", params["arg"])
assertEquals(sessionId, params["session_id"])
assertEquals(JsonPrimitive("help"), params["name"])
assertEquals(JsonPrimitive(""), params["arg"])
assertEquals(JsonPrimitive(sessionId), params["session_id"])
}
@Test
@@ -482,8 +483,8 @@ class SlashCommandDispatchRpcTest {
assertTrue("expected SLASH_EXEC fallback, got $methodCalls", execIndex >= 0)
assertTrue("dispatch must precede fallback", dispatchIndex < execIndex)
val params = paramsCalls[execIndex]
assertEquals("/status", params["command"])
assertEquals(sessionId, params["session_id"])
assertEquals(JsonPrimitive("/status"), params["command"])
assertEquals(JsonPrimitive(sessionId), params["session_id"])
// And the slash.exec output must be surfaced to the user.
val last =
@@ -629,9 +630,9 @@ class SlashCommandDispatchRpcTest {
val dispatchIndex = methodCalls.indexOf(WsMethods.COMMAND_DISPATCH)
assertTrue("expected COMMAND_DISPATCH, got $methodCalls", dispatchIndex >= 0)
val params = paramsCalls[dispatchIndex]
assertEquals("init", params["name"])
assertEquals("", params["arg"])
assertEquals(sessionId, params["session_id"])
assertEquals(JsonPrimitive("init"), params["name"])
assertEquals(JsonPrimitive(""), params["arg"])
assertEquals(JsonPrimitive(sessionId), params["session_id"])
}
@Test
@@ -654,9 +655,9 @@ class SlashCommandDispatchRpcTest {
val dispatchIndex = methodCalls.indexOf(WsMethods.COMMAND_DISPATCH)
assertTrue("expected COMMAND_DISPATCH, got $methodCalls", dispatchIndex >= 0)
val params = paramsCalls[dispatchIndex]
assertEquals("init", params["name"])
assertEquals("extra context here", params["arg"])
assertEquals(sessionId, params["session_id"])
assertEquals(JsonPrimitive("init"), params["name"])
assertEquals(JsonPrimitive("extra context here"), params["arg"])
assertEquals(JsonPrimitive(sessionId), params["session_id"])
}
@Test
@@ -682,9 +683,9 @@ class SlashCommandDispatchRpcTest {
val dispatchIndex = methodCalls.indexOf(WsMethods.COMMAND_DISPATCH)
assertTrue("expected COMMAND_DISPATCH, got $methodCalls", dispatchIndex >= 0)
val params = paramsCalls[dispatchIndex]
assertEquals("focus", params["name"])
assertEquals("", params["arg"])
assertEquals(sessionId, params["session_id"])
assertEquals(JsonPrimitive("focus"), params["name"])
assertEquals(JsonPrimitive(""), params["arg"])
assertEquals(JsonPrimitive(sessionId), params["session_id"])
}
@Test
@@ -707,9 +708,9 @@ class SlashCommandDispatchRpcTest {
val dispatchIndex = methodCalls.indexOf(WsMethods.COMMAND_DISPATCH)
assertTrue("expected COMMAND_DISPATCH, got $methodCalls", dispatchIndex >= 0)
val params = paramsCalls[dispatchIndex]
assertEquals("focus", params["name"])
assertEquals("on", params["arg"])
assertEquals(sessionId, params["session_id"])
assertEquals(JsonPrimitive("focus"), params["name"])
assertEquals(JsonPrimitive("on"), params["arg"])
assertEquals(JsonPrimitive(sessionId), params["session_id"])
}
@Test