refactor(#1375): type config.set, config.get, model.options and client.capabilities (#1393)

This commit is contained in:
m57 authored and GitHub committed 2026-09-30 00:40:24 +03:00
1 parent cb0463aefc
commit 1456e642e3
12 files changed
+339 -207

No files matched your search

@@ -2,6 +2,7 @@ package com.m57.hermescontrol.data.ws
import com.m57.hermescontrol.data.model.ModelOptionsResponse
import com.m57.hermescontrol.data.remote.NetworkResult
import com.m57.hermescontrol.data.ws.contract.ModelOptionsParams
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitCancellation
@@ -21,7 +22,7 @@ class ModelOptionsRepositoryTest {
ModelOptionsRepository(
connected = { true },
request = { params ->
assertEquals(mapOf("refresh" to true, "include_unconfigured" to false), params)
assertEquals(ModelOptionsParams(refresh = true, includeUnconfigured = false), params)
Json.parseToJsonElement("""{"providers":[{"slug":"test","name":"Test","models":["a"]}]}""")
},
rest = { error("REST must not run") },
@@ -316,17 +316,9 @@ class RawRpcParamsRatchetTest {
*/
val BASELINE: List<String> =
listOf(
"data/ws/HermesWsClient.kt::CLIENT_CAPABILITIES",
"data/ws/ModelOptionsRepository.kt::MODEL_OPTIONS",
"ui/chat/ChatModelSwitchDelegate.kt::CONFIG_SET",
"ui/chat/ChatModelSwitchDelegate.kt::CONFIG_SET",
"ui/chat/ChatModelSwitchDelegate.kt::CONFIG_SET",
"ui/chat/ChatModelSwitchDelegate.kt::CONFIG_SET",
"ui/chat/ChatViewModel.kt::COMMANDS_CATALOG",
"ui/chat/ChatViewModel.kt::COMMAND_DISPATCH",
"ui/chat/ChatViewModel.kt::COMMAND_DISPATCH",
"ui/chat/ChatViewModel.kt::CONFIG_GET",
"ui/chat/ChatViewModel.kt::CONFIG_SET",
"ui/chat/ChatViewModel.kt::FILE_ATTACH",
"ui/chat/ChatViewModel.kt::IMAGE_ATTACH_BYTES",
"ui/chat/ChatViewModel.kt::SLASH_EXEC",
@@ -0,0 +1,102 @@
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 ConfigModelTypesTest {
private val json = OkHttpProvider.json
@Test
fun configSetEncodesOnlyKeyAndValueByDefault() {
assertEquals(
buildJsonObject {
put("key", "fast")
put("value", "normal")
},
json.encodeToJsonElement(ConfigSetParams.serializer(), ConfigSetParams(key = "fast", value = "normal")),
)
}
@Test
fun configSetEncodesSessionScopeAndConfirmWhenPresent() {
assertEquals(
buildJsonObject {
put("key", "model")
put("value", "gpt-4o --provider openai --session")
put("session_id", "s1")
put("scope", "global")
put("confirm_expensive_model", true)
},
json.encodeToJsonElement(
ConfigSetParams.serializer(),
ConfigSetParams(
key = "model",
value = "gpt-4o --provider openai --session",
sessionId = "s1",
scope = "global",
confirmExpensiveModel = true,
),
),
)
}
@Test
fun configGetOmitsNullSessionAndCwd() {
assertEquals(
buildJsonObject {
put("key", "reasoning")
put("session_id", "s1")
},
json.encodeToJsonElement(
ConfigGetParams.serializer(),
ConfigGetParams(key = "reasoning", sessionId = "s1"),
),
)
}
@Test
fun modelOptionsKeepsExplicitFalseAndOmitsUnsetFlags() {
assertEquals(
buildJsonObject {
put("refresh", true)
put("include_unconfigured", false)
},
json.encodeToJsonElement(
ModelOptionsParams.serializer(),
ModelOptionsParams(refresh = true, includeUnconfigured = false),
),
)
assertEquals(
buildJsonObject { },
json.encodeToJsonElement(ModelOptionsParams.serializer(), ModelOptionsParams()),
)
}
@Test
fun clientCapabilitiesAlwaysEncodesServerRequests() {
assertEquals(
buildJsonObject { put("server_requests", true) },
json.encodeToJsonElement(
ClientCapabilitiesParams.serializer(),
ClientCapabilitiesParams(serverRequests = true),
),
)
}
@Test
fun descriptorsAreRegisteredWithContractNames() {
listOf(
RpcMethods.CONFIG_SET to "config.set",
RpcMethods.CONFIG_GET to "config.get",
RpcMethods.MODEL_OPTIONS to "model.options",
RpcMethods.CLIENT_CAPABILITIES to "client.capabilities",
).forEach { (method, name) ->
assertTrue("$name not in RpcMethods.all", method in RpcMethods.all)
assertEquals(name, method.name)
}
}
}
@@ -8,7 +8,7 @@ import com.m57.hermescontrol.data.model.ModelProvider
import com.m57.hermescontrol.data.model.PinnedModel
import com.m57.hermescontrol.data.remote.NetworkError
import com.m57.hermescontrol.data.remote.NetworkResult
import com.m57.hermescontrol.data.ws.WsMethods
import com.m57.hermescontrol.data.ws.contract.ConfigSetParams
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
@@ -18,6 +18,8 @@ import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonNull
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNull
@@ -31,8 +33,7 @@ class ChatModelSwitchDelegateTest {
private val uiState =
MutableStateFlow(ChatUiState(currentSessionId = "sess-1", currentSessionModel = "anthropic/claude-3"))
private var runtimeId: String? = "runtime-1"
private val sentMethods = mutableListOf<String>()
private val sentParams = mutableListOf<Map<String, Any>>()
private val sentParams = mutableListOf<ConfigSetParams>()
private val slashCommands = mutableListOf<String>()
private val assistantMessages = mutableListOf<String>()
private var contextRefetched = 0
@@ -71,10 +72,9 @@ class ChatModelSwitchDelegateTest {
ioDispatcher = testDispatcher,
uiState = uiState,
runtimeSessionId = { runtimeId },
wsSend = { method, params, onSent ->
sentMethods.add(method)
wsSend = { params, onSent ->
sentParams.add(params)
onSent?.invoke("req-${sentMethods.size}")
onSent?.invoke("req-${sentParams.size}")
},
trackRequest = { _, _ -> },
addAssistantMessage = { assistantMessages.add(it) },
@@ -304,8 +304,9 @@ class ChatModelSwitchDelegateTest {
delegate.handleModelSwitch("/MODEL gpt-4o --provider openai --session")
advanceUntilIdle()
assertEquals(listOf(WsMethods.CONFIG_SET), sentMethods)
assertEquals("gpt-4o --provider openai --session", sentParams.first()["value"])
assertEquals(1, sentParams.size)
assertEquals("model", sentParams.first().key)
assertEquals("gpt-4o --provider openai --session", sentParams.first().value)
delegate.handleConfigSetResult(
id = "req-1",
@@ -320,8 +321,8 @@ class ChatModelSwitchDelegateTest {
delegate.confirmModelSwitchExpensive()
advanceUntilIdle()
assertEquals(2, sentMethods.size)
assertEquals(true, sentParams[1]["confirm_expensive_model"])
assertEquals(2, sentParams.size)
assertEquals(true, sentParams[1].confirmExpensiveModel)
}
@Test
@@ -366,7 +367,6 @@ class ChatModelSwitchDelegateTest {
// Switch to model with fast capability
delegate.sendSlashModel("openai", "gpt-4o")
advanceUntilIdle()
sentMethods.clear()
sentParams.clear()
assertTrue(uiState.value.currentModelCapabilities?.fast == true)
@@ -377,10 +377,10 @@ class ChatModelSwitchDelegateTest {
advanceUntilIdle()
assertTrue(uiState.value.isFastModeChanging)
assertFalse(uiState.value.fastMode) // Still unconfirmed!
assertEquals(listOf(WsMethods.CONFIG_SET), sentMethods)
assertEquals("fast", sentParams.first()["key"])
assertEquals("fast", sentParams.first()["value"])
assertEquals("runtime-1", sentParams.first()["session_id"])
assertEquals(1, sentParams.size)
assertEquals("fast", sentParams.first().key)
assertEquals("fast", sentParams.first().value)
assertEquals("runtime-1", sentParams.first().sessionId)
// Backend acknowledges
delegate.handleConfigSetResult("req-1", mapOf("key" to "fast", "value" to "fast"))
@@ -391,7 +391,7 @@ class ChatModelSwitchDelegateTest {
delegate.toggleFastMode()
advanceUntilIdle()
assertTrue(uiState.value.isFastModeChanging)
assertEquals("normal", sentParams.last()["value"])
assertEquals("normal", sentParams.last().value)
delegate.handleConfigSetResult("req-2", mapOf("key" to "fast", "value" to "normal"))
assertFalse(uiState.value.fastMode)
@@ -405,7 +405,6 @@ class ChatModelSwitchDelegateTest {
advanceUntilIdle()
delegate.sendSlashModel("openai", "gpt-4o")
advanceUntilIdle()
sentMethods.clear()
sentParams.clear()
assertTrue(uiState.value.currentModelCapabilities?.fast == true)
@@ -420,10 +419,10 @@ class ChatModelSwitchDelegateTest {
assertEquals(false, uiState.value.currentModelCapabilities?.fast)
// Further toggle attempts are no-ops
val sentCount = sentMethods.size
val sentCount = sentParams.size
delegate.toggleFastMode()
advanceUntilIdle()
assertEquals(sentCount, sentMethods.size)
assertEquals(sentCount, sentParams.size)
}
@Test
@@ -470,9 +469,8 @@ class ChatModelSwitchDelegateTest {
@Test
fun setReasoningLevel_awaitsRequest_updatesOnAck_andPreventsConcurrentPick() =
testScope.runTest {
val gate = CompletableDeferred<Any?>()
val reqMethods = mutableListOf<String>()
val reqParams = mutableListOf<Map<String, Any>>()
val gate = CompletableDeferred<JsonElement>()
val reqParams = mutableListOf<ConfigSetParams>()
val customDelegate =
ChatModelSwitchDelegate(
@@ -480,14 +478,13 @@ class ChatModelSwitchDelegateTest {
ioDispatcher = testDispatcher,
uiState = uiState,
runtimeSessionId = { runtimeId },
wsSend = { _, _, _ -> },
wsSend = { _, _ -> },
trackRequest = { _, _ -> },
addAssistantMessage = {},
handleSlashCommand = {},
fetchContextUsage = {},
dataScopeFlow = dataScope,
wsRequest = { method, params ->
reqMethods.add(method)
wsRequest = { params ->
reqParams.add(params)
gate.await()
},
@@ -503,14 +500,15 @@ class ChatModelSwitchDelegateTest {
assertEquals("ultra", uiState.value.reasoningLevel)
assertEquals("high", uiState.value.pendingReasoningLevel)
assertNull(uiState.value.reasoningWireLevel)
assertEquals(listOf(WsMethods.CONFIG_SET), reqMethods)
assertEquals("session", reqParams.first()["scope"])
assertEquals("high", reqParams.first()["value"])
assertEquals(1, reqParams.size)
assertEquals("reasoning", reqParams.first().key)
assertEquals("session", reqParams.first().scope)
assertEquals("high", reqParams.first().value)
// Second pick while in-flight is rejected (serialized)
customDelegate.setReasoningLevel("low")
runCurrent()
assertEquals(1, reqMethods.size)
assertEquals(1, reqParams.size)
// Simulate session.info winning the race and publishing the fresh
// authoritative wire before the RPC ACK resumes the delegate.
@@ -540,13 +538,13 @@ class ChatModelSwitchDelegateTest {
ioDispatcher = testDispatcher,
uiState = uiState,
runtimeSessionId = { runtimeId },
wsSend = { _, _, _ -> },
wsSend = { _, _ -> },
trackRequest = { _, _ -> },
addAssistantMessage = {},
handleSlashCommand = {},
fetchContextUsage = {},
dataScopeFlow = dataScope,
wsRequest = { _, _ -> throw RuntimeException("4002 unknown reasoning value") },
wsRequest = { throw RuntimeException("4002 unknown reasoning value") },
)
customDelegate.setReasoningLevel("ultra")
@@ -560,7 +558,7 @@ class ChatModelSwitchDelegateTest {
@Test
fun setReasoningLevel_whenRuntimeSessionNullOrBlank_doesNotSend() =
testScope.runTest {
val reqMethods = mutableListOf<String>()
val reqParams = mutableListOf<ConfigSetParams>()
runtimeId = null
val customDelegate =
@@ -569,22 +567,22 @@ class ChatModelSwitchDelegateTest {
ioDispatcher = testDispatcher,
uiState = uiState,
runtimeSessionId = { runtimeId },
wsSend = { _, _, _ -> },
wsSend = { _, _ -> },
trackRequest = { _, _ -> },
addAssistantMessage = {},
handleSlashCommand = {},
fetchContextUsage = {},
dataScopeFlow = dataScope,
wsRequest = { method, _ ->
reqMethods.add(method)
null
wsRequest = { params ->
reqParams.add(params)
JsonNull
},
)
customDelegate.setReasoningLevel("high")
advanceUntilIdle()
assertTrue(reqMethods.isEmpty())
assertTrue(reqParams.isEmpty())
assertNull(uiState.value.pendingReasoningLevel)
assertNull(uiState.value.reasoningLevel)
}
@@ -592,20 +590,20 @@ class ChatModelSwitchDelegateTest {
@Test
fun setReasoningLevel_staleScopeOrReset_dropsLateAck() =
testScope.runTest {
val gate = CompletableDeferred<Any?>()
val gate = CompletableDeferred<JsonElement>()
val customDelegate =
ChatModelSwitchDelegate(
scope = testScope,
ioDispatcher = testDispatcher,
uiState = uiState,
runtimeSessionId = { runtimeId },
wsSend = { _, _, _ -> },
wsSend = { _, _ -> },
trackRequest = { _, _ -> },
addAssistantMessage = {},
handleSlashCommand = {},
fetchContextUsage = {},
dataScopeFlow = dataScope,
wsRequest = { _, _ -> gate.await() },
wsRequest = { gate.await() },
)
customDelegate.setReasoningLevel("high")
@@ -617,7 +615,12 @@ class ChatModelSwitchDelegateTest {
assertNull(uiState.value.pendingReasoningLevel)
// Late RPC response arrives
gate.complete(mapOf("key" to "reasoning", "value" to "high"))
gate.complete(
kotlinx.serialization.json.buildJsonObject {
put("key", kotlinx.serialization.json.JsonPrimitive("reasoning"))
put("value", kotlinx.serialization.json.JsonPrimitive("high"))
},
)
advanceUntilIdle()
assertNull(uiState.value.reasoningLevel)
@@ -35,6 +35,8 @@ import com.m57.hermescontrol.data.ws.JsonRpcError
import com.m57.hermescontrol.data.ws.ModelCatalogStore
import com.m57.hermescontrol.data.ws.WsEvent
import com.m57.hermescontrol.data.ws.WsMethods
import com.m57.hermescontrol.data.ws.contract.ConfigSetParams
import com.m57.hermescontrol.data.ws.contract.RpcMethods
import com.m57.hermescontrol.notification.TurnCorrelationTracker
import com.m57.hermescontrol.ui.chat.fakes.FakeChatPersistenceRepository
import com.m57.hermescontrol.ui.chat.fakes.FakeSlashUsageStore
@@ -1295,15 +1297,9 @@ class ChatViewModelTest {
// commands). NOT command.dispatch (4018s on /model), NOT prompt.submit
// (LLM would treat it as text). Capture the config.set params.
val modelCalls = mutableListOf<Triple<String, String, String>>()
every { HermesWsClient.send(WsMethods.CONFIG_SET, any(), any()) } answers {
val params = arg<Map<String, Any>>(1)
modelCalls.add(
Triple(
params["key"] as String,
params["value"] as String,
params["session_id"] as String,
),
)
every { HermesWsClient.send(RpcMethods.CONFIG_SET, any<ConfigSetParams>(), any()) } answers {
val params = arg<ConfigSetParams>(1)
modelCalls.add(Triple(params.key, params.value, params.sessionId.orEmpty()))
"req-cfg-${modelCalls.size}"
}
@@ -1315,7 +1311,7 @@ class ChatViewModelTest {
"openai/gpt-4o",
viewModel.uiState.value.currentSessionModel,
)
verify { HermesWsClient.send(WsMethods.CONFIG_SET, any(), any()) }
verify { HermesWsClient.send(RpcMethods.CONFIG_SET, any<ConfigSetParams>(), any()) }
val call = modelCalls.firstOrNull { it.first == "model" }
assertNotNull("selection must route through config.set key=model", call)
assertEquals("gpt-4o --provider openai --session", call!!.second)
@@ -1340,7 +1336,7 @@ class ChatViewModelTest {
// (key="model"), which the gateway routes to _apply_model_switch. NOT
// command.dispatch (4018s on /model) and NOT prompt.submit (LLM would
// treat it as text).
verify { HermesWsClient.send(WsMethods.CONFIG_SET, any(), any()) }
verify { HermesWsClient.send(RpcMethods.CONFIG_SET, any<ConfigSetParams>(), any()) }
}
@Test
@@ -1353,15 +1349,9 @@ class ChatViewModelTest {
// slash prefix must be stripped, or parse_model_flags on the
// backend won't recognize it and the hot-swap silently fails.
val modelCalls = mutableListOf<Triple<String, String, String>>()
every { HermesWsClient.send(WsMethods.CONFIG_SET, any(), any()) } answers {
val params = arg<Map<String, Any>>(1)
modelCalls.add(
Triple(
params["key"] as String,
params["value"] as String,
params["session_id"] as String,
),
)
every { HermesWsClient.send(RpcMethods.CONFIG_SET, any<ConfigSetParams>(), any()) } answers {
val params = arg<ConfigSetParams>(1)
modelCalls.add(Triple(params.key, params.value, params.sessionId.orEmpty()))
"req-cfg-ci-${modelCalls.size}"
}
@@ -1388,9 +1378,9 @@ class ChatViewModelTest {
val (viewModel, sessionId) = createViewModelWithSession()
var lastReqId = ""
val capturedParams = mutableListOf<Map<String, Any>>()
every { HermesWsClient.send(WsMethods.CONFIG_SET, any(), any()) } answers {
val params = arg<Map<String, Any>>(1)
val capturedParams = mutableListOf<ConfigSetParams>()
every { HermesWsClient.send(RpcMethods.CONFIG_SET, any<ConfigSetParams>(), any()) } answers {
val params = arg<ConfigSetParams>(1)
capturedParams.add(params)
val id = "req-confirm-${capturedParams.size}"
lastReqId = id
@@ -1403,7 +1393,7 @@ class ChatViewModelTest {
advanceUntilIdle()
assertEquals(1, capturedParams.size)
assertNull(capturedParams[0]["confirm_expensive_model"])
assertNull(capturedParams[0].confirmExpensiveModel)
// Backend returns confirm_required = true
mockEventsFlow.emit(
@@ -1429,12 +1419,12 @@ class ChatViewModelTest {
assertNull(viewModel.uiState.value.modelSwitchConfirmMessage)
assertEquals(2, capturedParams.size)
assertEquals(true, capturedParams[1]["confirm_expensive_model"])
assertEquals(true, capturedParams[1].confirmExpensiveModel)
assertEquals(
"muse-spark-1.3-contributor-free --provider opencode-free --session",
capturedParams[1]["value"],
capturedParams[1].value,
)
assertEquals(sessionId, capturedParams[1]["session_id"])
assertEquals(sessionId, capturedParams[1].sessionId)
}
@Test
@@ -1443,7 +1433,7 @@ class ChatViewModelTest {
val (viewModel, _) = createViewModelWithSession()
var lastReqId = ""
every { HermesWsClient.send(WsMethods.CONFIG_SET, any(), any()) } answers {
every { HermesWsClient.send(RpcMethods.CONFIG_SET, any<ConfigSetParams>(), any()) } answers {
val id = "req-cfg-dismiss"
lastReqId = id
arg<((String) -> Unit)?>(2)?.invoke(id)
@@ -12,6 +12,9 @@ import com.m57.hermescontrol.data.ws.ConnectionStatus
import com.m57.hermescontrol.data.ws.HermesWsClient
import com.m57.hermescontrol.data.ws.WsEvent
import com.m57.hermescontrol.data.ws.WsMethods
import com.m57.hermescontrol.data.ws.contract.ConfigGetParams
import com.m57.hermescontrol.data.ws.contract.ConfigSetParams
import com.m57.hermescontrol.data.ws.contract.RpcMethods
import com.m57.hermescontrol.ui.chat.fakes.FakeChatPersistenceRepository
import com.m57.hermescontrol.ui.chat.fakes.FakeSlashUsageStore
import io.mockk.coEvery
@@ -21,6 +24,7 @@ import io.mockk.mockkObject
import io.mockk.mockkStatic
import io.mockk.slot
import io.mockk.unmockkAll
import io.mockk.verify
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -34,6 +38,7 @@ import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.test.setMain
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
@@ -271,35 +276,27 @@ class SlashCommandDispatchRpcTest {
runTest {
val (vm, sessionId) = createViewModelWithSession()
val methodCalls = mutableListOf<String>()
val paramsCalls = mutableListOf<Map<String, Any>>()
every {
HermesWsClient.request(capture(methodCalls), capture(paramsCalls), any())
val setCalls = mutableListOf<ConfigSetParams>()
coEvery {
HermesWsClient.call(RpcMethods.CONFIG_SET, capture(setCalls), any(), any())
} answers {
val result =
if (arg<String>(0) == WsMethods.CONFIG_SET) {
kotlinx.serialization.json.buildJsonObject {
put("key", kotlinx.serialization.json.JsonPrimitive("reasoning"))
put("value", kotlinx.serialization.json.JsonPrimitive("high"))
}
} else {
kotlinx.serialization.json.JsonNull
}
CompletableDeferred<Any?>(result)
kotlinx.serialization.json.buildJsonObject {
put("key", kotlinx.serialization.json.JsonPrimitive("reasoning"))
put("value", kotlinx.serialization.json.JsonPrimitive("high"))
}
}
vm.sendMessage("/reasoning high --global")
advanceUntilIdle()
val configIndex = methodCalls.indexOf(WsMethods.CONFIG_SET)
assertTrue("expected CONFIG_SET, got $methodCalls", configIndex >= 0)
val params = paramsCalls[configIndex]
assertEquals("reasoning", params["key"])
assertEquals("high", params["value"])
assertEquals(sessionId, params["session_id"])
assertEquals("global", params["scope"])
assertEquals("expected exactly one config.set, got $setCalls", 1, setCalls.size)
val params = setCalls.single()
assertEquals("reasoning", params.key)
assertEquals("high", params.value)
assertEquals(sessionId, params.sessionId)
assertEquals("global", params.scope)
assertEquals("high", vm.uiState.value.reasoningLevel)
assertTrue(WsMethods.COMMAND_DISPATCH !in methodCalls)
verify(exactly = 0) { HermesWsClient.send(WsMethods.COMMAND_DISPATCH, any(), any()) }
}
@Test
@@ -307,33 +304,26 @@ class SlashCommandDispatchRpcTest {
runTest {
val (vm, sessionId) = createViewModelWithSession()
val methodCalls = mutableListOf<String>()
val paramsCalls = mutableListOf<Map<String, Any>>()
every {
HermesWsClient.request(capture(methodCalls), capture(paramsCalls), any())
val setCalls = mutableListOf<ConfigSetParams>()
coEvery {
HermesWsClient.call(RpcMethods.CONFIG_SET, capture(setCalls), any(), any())
} answers {
val result =
if (arg<String>(0) == WsMethods.CONFIG_SET) {
kotlinx.serialization.json.buildJsonObject {
put("key", kotlinx.serialization.json.JsonPrimitive("reasoning"))
put("value", kotlinx.serialization.json.JsonPrimitive("show"))
}
} else {
kotlinx.serialization.json.JsonNull
}
CompletableDeferred<Any?>(result)
kotlinx.serialization.json.buildJsonObject {
put("key", kotlinx.serialization.json.JsonPrimitive("reasoning"))
put("value", kotlinx.serialization.json.JsonPrimitive("show"))
}
}
vm.sendMessage("/reasoning show")
advanceUntilIdle()
val configIndex = methodCalls.indexOf(WsMethods.CONFIG_SET)
assertTrue("expected CONFIG_SET, got $methodCalls", configIndex >= 0)
val params = paramsCalls[configIndex]
assertEquals("reasoning", params["key"])
assertEquals("show", params["value"])
assertEquals(sessionId, params["session_id"])
assertTrue(WsMethods.COMMAND_DISPATCH !in methodCalls)
assertEquals("expected exactly one config.set, got $setCalls", 1, setCalls.size)
val params = setCalls.single()
assertEquals("reasoning", params.key)
assertEquals("show", params.value)
assertEquals(sessionId, params.sessionId)
assertNull(params.scope)
verify(exactly = 0) { HermesWsClient.send(WsMethods.COMMAND_DISPATCH, any(), any()) }
assertEquals(
"reasoning: show",
vm.uiState.value.messages
@@ -347,32 +337,24 @@ class SlashCommandDispatchRpcTest {
runTest {
val (vm, sessionId) = createViewModelWithSession()
val methodCalls = mutableListOf<String>()
val paramsCalls = mutableListOf<Map<String, Any>>()
every {
HermesWsClient.request(capture(methodCalls), capture(paramsCalls), any())
val getCalls = mutableListOf<ConfigGetParams>()
coEvery {
HermesWsClient.call(RpcMethods.CONFIG_GET, capture(getCalls), any(), any())
} answers {
val result =
if (arg<String>(0) == WsMethods.CONFIG_GET) {
kotlinx.serialization.json.buildJsonObject {
put("value", kotlinx.serialization.json.JsonPrimitive("ultra"))
put("display", kotlinx.serialization.json.JsonPrimitive("hide"))
}
} else {
kotlinx.serialization.json.JsonNull
}
CompletableDeferred<Any?>(result)
kotlinx.serialization.json.buildJsonObject {
put("value", kotlinx.serialization.json.JsonPrimitive("ultra"))
put("display", kotlinx.serialization.json.JsonPrimitive("hide"))
}
}
vm.sendMessage("/reasoning")
advanceUntilIdle()
val configIndex = methodCalls.indexOf(WsMethods.CONFIG_GET)
assertTrue("expected CONFIG_GET, got $methodCalls", configIndex >= 0)
val params = paramsCalls[configIndex]
assertEquals("reasoning", params["key"])
assertEquals(sessionId, params["session_id"])
assertTrue(WsMethods.COMMAND_DISPATCH !in methodCalls)
assertEquals("expected exactly one config.get, got $getCalls", 1, getCalls.size)
val params = getCalls.single()
assertEquals("reasoning", params.key)
assertEquals(sessionId, params.sessionId)
verify(exactly = 0) { HermesWsClient.send(WsMethods.COMMAND_DISPATCH, any(), any()) }
assertEquals(
"reasoning: ultra · display hide",
vm.uiState.value.messages