refactor(#1375): type process list and kill RPC params (#1390)

This commit is contained in:
m57 authored and GitHub committed 2026-09-29 22:36:35 +03:00
1 parent 27ce528005
commit 9a09974081
6 files changed
+113 -56

No files matched your search

@@ -342,8 +342,6 @@ class RawRpcParamsRatchetTest {
"ui/chat/ChatViewModel.kt::FILE_ATTACH",
"ui/chat/ChatViewModel.kt::IMAGE_ATTACH_BYTES",
"ui/chat/ChatViewModel.kt::SLASH_EXEC",
"ui/process/ProcessesViewModel.kt::PROCESS_KILL",
"ui/process/ProcessesViewModel.kt::PROCESS_LIST",
)
}
}
@@ -0,0 +1,34 @@
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 ProcessTypesTest {
@Test
fun killEncodesExactlySessionAndProcessIds() {
val encoded =
OkHttpProvider.json.encodeToJsonElement(
ProcessKillParams.serializer(),
ProcessKillParams(sessionId = "s1", processId = "p1"),
)
assertEquals(
buildJsonObject {
put("session_id", "s1")
put("process_id", "p1")
},
encoded,
)
}
@Test
fun descriptorsAreRegisteredWithContractNames() {
assertTrue(RpcMethods.PROCESS_LIST in RpcMethods.all)
assertTrue(RpcMethods.PROCESS_KILL in RpcMethods.all)
assertEquals("process.list", RpcMethods.PROCESS_LIST.name)
assertEquals("process.kill", RpcMethods.PROCESS_KILL.name)
}
}
@@ -3,17 +3,23 @@ package com.m57.hermescontrol.ui.process
import com.m57.hermescontrol.data.model.ProcessInfo
import com.m57.hermescontrol.data.session.ActiveSessionHolder
import com.m57.hermescontrol.data.ws.HermesWsClient
import com.m57.hermescontrol.data.ws.WsMethods
import io.mockk.every
import com.m57.hermescontrol.data.ws.contract.ProcessKillParams
import com.m57.hermescontrol.data.ws.contract.RpcMethods
import com.m57.hermescontrol.data.ws.contract.SessionIdParams
import io.mockk.coEvery
import io.mockk.mockkObject
import io.mockk.slot
import io.mockk.unmockkAll
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.setMain
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.buildJsonArray
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
@@ -42,34 +48,40 @@ class ProcessesViewModelTest {
@Test
fun `process list parses running and exited entries`() {
val raw =
mapOf(
"processes" to
listOf(
mapOf(
"session_id" to "p1",
"command" to "python train.py --epochs 10",
"pid" to 1234,
"status" to "running",
"uptime_seconds" to 95,
"cwd" to "/work",
),
mapOf(
"session_id" to "p2",
"command" to "echo done",
"status" to "exited",
"exit_code" to 0,
),
),
)
val raw: JsonElement =
buildJsonObject {
put(
"processes",
buildJsonArray {
add(
buildJsonObject {
put("session_id", "p1")
put("command", "python train.py --epochs 10")
put("pid", 1234)
put("status", "running")
put("uptime_seconds", 95)
put("cwd", "/work")
},
)
add(
buildJsonObject {
put("session_id", "p2")
put("command", "echo done")
put("status", "exited")
put("exit_code", 0)
},
)
},
)
}
every {
HermesWsClient.request(WsMethods.PROCESS_LIST, any())
} returns CompletableDeferred(raw)
val listParams = slot<SessionIdParams>()
coEvery { HermesWsClient.call(RpcMethods.PROCESS_LIST, capture(listParams), any(), any()) } returns raw
val vm = ProcessesViewModel()
vm.load()
assertEquals(SessionIdParams("sess-1"), listParams.captured)
val procs = vm.uiState.value.processes
assertEquals(2, procs.size)
@@ -90,26 +102,20 @@ class ProcessesViewModelTest {
@Test
fun `kill issues process kill with session-scoped params then refreshes`() {
var listCalls = 0
val killParamsSlot = slot<Map<String, Any>>()
val killParamsSlot = slot<ProcessKillParams>()
every {
HermesWsClient.request(WsMethods.PROCESS_LIST, any())
} answers {
coEvery { HermesWsClient.call(RpcMethods.PROCESS_LIST, any(), any(), any()) } answers {
listCalls++
CompletableDeferred(mapOf("processes" to emptyList<Any>()))
buildJsonObject { put("processes", JsonArray(emptyList())) }
}
every {
HermesWsClient.request(WsMethods.PROCESS_KILL, capture(killParamsSlot))
} answers {
CompletableDeferred(mapOf("killed" to true))
coEvery { HermesWsClient.call(RpcMethods.PROCESS_KILL, capture(killParamsSlot), any(), any()) } answers {
buildJsonObject { put("killed", true) }
}
val vm = ProcessesViewModel()
vm.kill("p1")
val killParams = killParamsSlot.captured
assertEquals("p1", killParams["process_id"])
assertEquals("sess-1", killParams["session_id"])
assertEquals(ProcessKillParams(sessionId = "sess-1", processId = "p1"), killParamsSlot.captured)
// list is called once on init (session emitted) + once after kill refresh
assertTrue(listCalls >= 2)
assertNull(vm.uiState.value.killingId)