6 files changed
+113
-56
No files matched your search
@@ -0,0 +1,10 @@
|
|||||||
|
package com.m57.hermescontrol.data.ws.contract
|
||||||
|
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ProcessKillParams(
|
||||||
|
@SerialName("session_id") val sessionId: String,
|
||||||
|
@SerialName("process_id") val processId: String,
|
||||||
|
)
|
||||||
@@ -347,6 +347,22 @@ object RpcMethods {
|
|||||||
JsonElement.serializer(),
|
JsonElement.serializer(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Passthrough JsonElement results: callers keep their existing legacy decoders.
|
||||||
|
val PROCESS_LIST: RpcMethod<SessionIdParams, JsonElement> =
|
||||||
|
RpcMethod(
|
||||||
|
WsMethods.PROCESS_LIST,
|
||||||
|
SessionIdParams.serializer(),
|
||||||
|
JsonElement.serializer(),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Passthrough JsonElement results: callers keep their existing legacy decoders.
|
||||||
|
val PROCESS_KILL: RpcMethod<ProcessKillParams, JsonElement> =
|
||||||
|
RpcMethod(
|
||||||
|
WsMethods.PROCESS_KILL,
|
||||||
|
ProcessKillParams.serializer(),
|
||||||
|
JsonElement.serializer(),
|
||||||
|
)
|
||||||
|
|
||||||
/** Every typed method. GatewayContractTest iterates this; future migrations append here. */
|
/** Every typed method. GatewayContractTest iterates this; future migrations append here. */
|
||||||
val all: List<RpcMethod<*, *>> =
|
val all: List<RpcMethod<*, *>> =
|
||||||
listOf(
|
listOf(
|
||||||
@@ -392,5 +408,7 @@ object RpcMethods {
|
|||||||
CONNECTORS_OPERATION_WAKE,
|
CONNECTORS_OPERATION_WAKE,
|
||||||
SUBAGENT_LIST,
|
SUBAGENT_LIST,
|
||||||
SUBAGENT_TAIL,
|
SUBAGENT_TAIL,
|
||||||
|
PROCESS_LIST,
|
||||||
|
PROCESS_KILL,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -5,7 +5,10 @@ import androidx.lifecycle.viewModelScope
|
|||||||
import com.m57.hermescontrol.data.model.ProcessInfo
|
import com.m57.hermescontrol.data.model.ProcessInfo
|
||||||
import com.m57.hermescontrol.data.session.ActiveSessionHolder
|
import com.m57.hermescontrol.data.session.ActiveSessionHolder
|
||||||
import com.m57.hermescontrol.data.ws.HermesWsClient
|
import com.m57.hermescontrol.data.ws.HermesWsClient
|
||||||
import com.m57.hermescontrol.data.ws.WsMethods
|
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 com.m57.hermescontrol.data.ws.toAny
|
||||||
import com.m57.hermescontrol.ui.common.ToastHost
|
import com.m57.hermescontrol.ui.common.ToastHost
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
@@ -67,13 +70,8 @@ class ProcessesViewModel :
|
|||||||
_uiState.update { it.copy(isLoading = true, errorMessage = null) }
|
_uiState.update { it.copy(isLoading = true, errorMessage = null) }
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
try {
|
||||||
val result =
|
val result = HermesWsClient.call(RpcMethods.PROCESS_LIST, SessionIdParams(sessionId))
|
||||||
HermesWsClient
|
val processes = parseProcessList(result.toAny())
|
||||||
.request(
|
|
||||||
WsMethods.PROCESS_LIST,
|
|
||||||
mapOf("session_id" to sessionId),
|
|
||||||
).await()
|
|
||||||
val processes = parseProcessList(result)
|
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
@@ -105,14 +103,7 @@ class ProcessesViewModel :
|
|||||||
_uiState.update { it.copy(killingId = processId) }
|
_uiState.update { it.copy(killingId = processId) }
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
try {
|
||||||
HermesWsClient
|
HermesWsClient.call(RpcMethods.PROCESS_KILL, ProcessKillParams(sessionId, processId))
|
||||||
.request(
|
|
||||||
WsMethods.PROCESS_KILL,
|
|
||||||
mapOf(
|
|
||||||
"process_id" to processId,
|
|
||||||
"session_id" to sessionId,
|
|
||||||
),
|
|
||||||
).await()
|
|
||||||
_uiState.update { it.copy(killingId = null, toastMessage = "Process killed") }
|
_uiState.update { it.copy(killingId = null, toastMessage = "Process killed") }
|
||||||
load()
|
load()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
|||||||
@@ -342,8 +342,6 @@ class RawRpcParamsRatchetTest {
|
|||||||
"ui/chat/ChatViewModel.kt::FILE_ATTACH",
|
"ui/chat/ChatViewModel.kt::FILE_ATTACH",
|
||||||
"ui/chat/ChatViewModel.kt::IMAGE_ATTACH_BYTES",
|
"ui/chat/ChatViewModel.kt::IMAGE_ATTACH_BYTES",
|
||||||
"ui/chat/ChatViewModel.kt::SLASH_EXEC",
|
"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.model.ProcessInfo
|
||||||
import com.m57.hermescontrol.data.session.ActiveSessionHolder
|
import com.m57.hermescontrol.data.session.ActiveSessionHolder
|
||||||
import com.m57.hermescontrol.data.ws.HermesWsClient
|
import com.m57.hermescontrol.data.ws.HermesWsClient
|
||||||
import com.m57.hermescontrol.data.ws.WsMethods
|
import com.m57.hermescontrol.data.ws.contract.ProcessKillParams
|
||||||
import io.mockk.every
|
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.mockkObject
|
||||||
import io.mockk.slot
|
import io.mockk.slot
|
||||||
import io.mockk.unmockkAll
|
import io.mockk.unmockkAll
|
||||||
import kotlinx.coroutines.CompletableDeferred
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.test.UnconfinedTestDispatcher
|
import kotlinx.coroutines.test.UnconfinedTestDispatcher
|
||||||
import kotlinx.coroutines.test.resetMain
|
import kotlinx.coroutines.test.resetMain
|
||||||
import kotlinx.coroutines.test.setMain
|
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.After
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Assert.assertFalse
|
import org.junit.Assert.assertFalse
|
||||||
@@ -42,34 +48,40 @@ class ProcessesViewModelTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `process list parses running and exited entries`() {
|
fun `process list parses running and exited entries`() {
|
||||||
val raw =
|
val raw: JsonElement =
|
||||||
mapOf(
|
buildJsonObject {
|
||||||
"processes" to
|
put(
|
||||||
listOf(
|
"processes",
|
||||||
mapOf(
|
buildJsonArray {
|
||||||
"session_id" to "p1",
|
add(
|
||||||
"command" to "python train.py --epochs 10",
|
buildJsonObject {
|
||||||
"pid" to 1234,
|
put("session_id", "p1")
|
||||||
"status" to "running",
|
put("command", "python train.py --epochs 10")
|
||||||
"uptime_seconds" to 95,
|
put("pid", 1234)
|
||||||
"cwd" to "/work",
|
put("status", "running")
|
||||||
),
|
put("uptime_seconds", 95)
|
||||||
mapOf(
|
put("cwd", "/work")
|
||||||
"session_id" to "p2",
|
},
|
||||||
"command" to "echo done",
|
)
|
||||||
"status" to "exited",
|
add(
|
||||||
"exit_code" to 0,
|
buildJsonObject {
|
||||||
),
|
put("session_id", "p2")
|
||||||
),
|
put("command", "echo done")
|
||||||
)
|
put("status", "exited")
|
||||||
|
put("exit_code", 0)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
every {
|
val listParams = slot<SessionIdParams>()
|
||||||
HermesWsClient.request(WsMethods.PROCESS_LIST, any())
|
coEvery { HermesWsClient.call(RpcMethods.PROCESS_LIST, capture(listParams), any(), any()) } returns raw
|
||||||
} returns CompletableDeferred(raw)
|
|
||||||
|
|
||||||
val vm = ProcessesViewModel()
|
val vm = ProcessesViewModel()
|
||||||
vm.load()
|
vm.load()
|
||||||
|
|
||||||
|
assertEquals(SessionIdParams("sess-1"), listParams.captured)
|
||||||
val procs = vm.uiState.value.processes
|
val procs = vm.uiState.value.processes
|
||||||
assertEquals(2, procs.size)
|
assertEquals(2, procs.size)
|
||||||
|
|
||||||
@@ -90,26 +102,20 @@ class ProcessesViewModelTest {
|
|||||||
@Test
|
@Test
|
||||||
fun `kill issues process kill with session-scoped params then refreshes`() {
|
fun `kill issues process kill with session-scoped params then refreshes`() {
|
||||||
var listCalls = 0
|
var listCalls = 0
|
||||||
val killParamsSlot = slot<Map<String, Any>>()
|
val killParamsSlot = slot<ProcessKillParams>()
|
||||||
|
|
||||||
every {
|
coEvery { HermesWsClient.call(RpcMethods.PROCESS_LIST, any(), any(), any()) } answers {
|
||||||
HermesWsClient.request(WsMethods.PROCESS_LIST, any())
|
|
||||||
} answers {
|
|
||||||
listCalls++
|
listCalls++
|
||||||
CompletableDeferred(mapOf("processes" to emptyList<Any>()))
|
buildJsonObject { put("processes", JsonArray(emptyList())) }
|
||||||
}
|
}
|
||||||
every {
|
coEvery { HermesWsClient.call(RpcMethods.PROCESS_KILL, capture(killParamsSlot), any(), any()) } answers {
|
||||||
HermesWsClient.request(WsMethods.PROCESS_KILL, capture(killParamsSlot))
|
buildJsonObject { put("killed", true) }
|
||||||
} answers {
|
|
||||||
CompletableDeferred(mapOf("killed" to true))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val vm = ProcessesViewModel()
|
val vm = ProcessesViewModel()
|
||||||
vm.kill("p1")
|
vm.kill("p1")
|
||||||
|
|
||||||
val killParams = killParamsSlot.captured
|
assertEquals(ProcessKillParams(sessionId = "sess-1", processId = "p1"), killParamsSlot.captured)
|
||||||
assertEquals("p1", killParams["process_id"])
|
|
||||||
assertEquals("sess-1", killParams["session_id"])
|
|
||||||
// list is called once on init (session emitted) + once after kill refresh
|
// list is called once on init (session emitted) + once after kill refresh
|
||||||
assertTrue(listCalls >= 2)
|
assertTrue(listCalls >= 2)
|
||||||
assertNull(vm.uiState.value.killingId)
|
assertNull(vm.uiState.value.killingId)
|
||||||
|
|||||||
Reference in new issue
Block a user