This commit is contained in:
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
|
||||
|
||||
Reference in new issue
Block a user