feat(chat): /stop also kills background processes like desktop (#1409)

This commit is contained in:
m57 authored and GitHub committed 2026-09-30 14:33:07 +03:00
1 parent 05519ba1ff
commit 0e39a71591
8 files changed
+133 -4

No files matched your search

@@ -24,8 +24,18 @@ class ProcessTypesTest {
)
}
@Test
fun stopEncodesAnEmptyObjectAndDecodesKilledCount() {
val encoded = OkHttpProvider.json.encodeToJsonElement(ProcessStopParams.serializer(), ProcessStopParams)
assertEquals(buildJsonObject {}, encoded)
val result = OkHttpProvider.json.decodeFromString(ProcessStopResult.serializer(), """{"killed":3}""")
assertEquals(ProcessStopResult(killed = 3), result)
}
@Test
fun descriptorsAreRegisteredWithContractNames() {
assertTrue(RpcMethods.PROCESS_STOP in RpcMethods.all)
assertEquals("process.stop", RpcMethods.PROCESS_STOP.name)
assertTrue(RpcMethods.PROCESS_LIST in RpcMethods.all)
assertTrue(RpcMethods.PROCESS_KILL in RpcMethods.all)
assertEquals("process.list", RpcMethods.PROCESS_LIST.name)
@@ -36,6 +36,8 @@ 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.ProcessStopParams
import com.m57.hermescontrol.data.ws.contract.ProcessStopResult
import com.m57.hermescontrol.data.ws.contract.RpcMethods
import com.m57.hermescontrol.notification.TurnCorrelationTracker
import com.m57.hermescontrol.ui.chat.fakes.FakeChatPersistenceRepository
@@ -283,6 +285,9 @@ class ChatViewModelTest {
HermesWsClient.send(arg(0), arg(1)) {}
CompletableDeferred<Any?>(Unit)
}
// /stop also fires process.stop; default to "nothing to kill" so unrelated /stop tests never hit the socket.
coEvery { HermesWsClient.call(RpcMethods.PROCESS_STOP, any(), any(), any()) } returns
ProcessStopResult(killed = 0)
mockkObject(ApiClient)
mockkObject(HermesDatabase)
@@ -966,6 +971,52 @@ class ChatViewModelTest {
verify { HermesWsClient.send(WsMethods.SESSION_INTERRUPT, any(), any()) }
}
@Test
fun testSlashCommand_stop_alsoKillsBackgroundProcesses() =
runTest {
val (viewModel, _) = createViewModelWithSession()
coEvery { HermesWsClient.call(RpcMethods.PROCESS_STOP, any(), any(), any()) } returns
ProcessStopResult(killed = 2)
viewModel.sendMessage("/stop")
advanceUntilIdle()
verify { HermesWsClient.send(WsMethods.SESSION_INTERRUPT, any(), any()) }
coVerify(exactly = 1) { HermesWsClient.call(RpcMethods.PROCESS_STOP, ProcessStopParams, any(), any()) }
val contents =
viewModel.uiState.value.messages
.map { it.content }
assertTrue(contents.contains("Stopped 2 background processes."))
}
@Test
fun testSlashCommand_interrupt_doesNotKillBackgroundProcesses() =
runTest {
val (viewModel, _) = createViewModelWithSession()
viewModel.sendMessage("/interrupt")
advanceUntilIdle()
verify { HermesWsClient.send(WsMethods.SESSION_INTERRUPT, any(), any()) }
coVerify(exactly = 0) { HermesWsClient.call(RpcMethods.PROCESS_STOP, any(), any(), any()) }
}
@Test
fun testSlashCommand_stop_reportsProcessStopFailure() =
runTest {
val (viewModel, _) = createViewModelWithSession()
coEvery { HermesWsClient.call(RpcMethods.PROCESS_STOP, any(), any(), any()) } throws
IllegalStateException("boom")
viewModel.sendMessage("/stop")
advanceUntilIdle()
assertTrue(
viewModel.uiState.value.messages
.any { it.content == "Could not stop background processes: boom" },
)
}
@Test
fun testSlashCommand_stop_andInterruptedNoticeStayInPlaceAcrossSync() =
runTest {
@@ -1041,7 +1092,15 @@ class ChatViewModelTest {
val contents = merged.map { it.content }
assertEquals(
listOf("start task", "running...", "/stop", "Session interrupted", "next prompt", "next reply"),
listOf(
"start task",
"running...",
"/stop",
"No background processes to stop.",
"Session interrupted",
"next prompt",
"next reply",
),
contents,
)
}
@@ -20,8 +20,8 @@ class SlashCommandDispatcherTest {
private val dispatcher = SlashCommandDispatcher()
@Test
fun `stop routes to Interrupt`() {
assertEquals(SlashResult.Interrupt, dispatcher.dispatch("/stop"))
fun `stop routes to Stop`() {
assertEquals(SlashResult.Stop, dispatcher.dispatch("/stop"))
}
@Test