feat(chat): full sudo.request support with expire, cancel-unblock, secret fields (#1056)

This commit is contained in:
M57 authored and GitHub committed 2026-09-03 22:08:07 +03:00
1 parent 6d2362ba8a
commit 077ad1fa81
10 files changed
+348 -7

No files matched your search

@@ -855,4 +855,71 @@ class EventParserTest {
assertEquals("1", todoUpdated.todos[1].parent)
assertTrue(todoUpdated.todos[1].isSubtask)
}
@Test
fun testParseSudoRequest_returnsSudoRequest() {
val params =
mapOf(
"type" to "sudo.request",
"session_id" to "sess-sudo-1",
"payload" to mapOf("request_id" to "sudo-1"),
)
val event = EventParser.parseParams(params)
assertTrue(event is WsEvent.SudoRequest)
val sudo = event as WsEvent.SudoRequest
assertEquals("sudo-1", sudo.requestId)
assertEquals("sess-sudo-1", sudo.sessionId)
}
@Test
fun testParseSudoExpire_returnsSudoExpire() {
val params =
mapOf(
"type" to "sudo.expire",
"session_id" to "sess-sudo-1",
"payload" to mapOf("request_id" to "sudo-1"),
)
val event = EventParser.parseParams(params)
assertTrue(event is WsEvent.SudoExpire)
val expire = event as WsEvent.SudoExpire
assertEquals("sudo-1", expire.requestId)
assertEquals("sess-sudo-1", expire.sessionId)
}
@Test
fun testParseSecretRequest_withEnvVarAndPrompt() {
val params =
mapOf(
"type" to "secret.request",
"session_id" to "sess-secret-1",
"payload" to
mapOf(
"request_id" to "secret-1",
"env_var" to "GITHUB_TOKEN",
"prompt" to "Enter your GitHub token to continue",
),
)
val event = EventParser.parseParams(params)
assertTrue(event is WsEvent.SecretRequest)
val secret = event as WsEvent.SecretRequest
assertEquals("secret-1", secret.requestId)
assertEquals("sess-secret-1", secret.sessionId)
assertEquals("GITHUB_TOKEN", secret.envVar)
assertEquals("Enter your GitHub token to continue", secret.prompt)
}
@Test
fun testParseSecretExpire_returnsSecretExpire() {
val params =
mapOf(
"type" to "secret.expire",
"session_id" to "sess-secret-1",
"payload" to mapOf("request_id" to "secret-1"),
)
val event = EventParser.parseParams(params)
assertTrue(event is WsEvent.SecretExpire)
val expire = event as WsEvent.SecretExpire
assertEquals("secret-1", expire.requestId)
assertEquals("sess-secret-1", expire.sessionId)
}
}
@@ -3159,6 +3159,130 @@ class ChatViewModelTest {
assertNull(viewModel.uiState.value.secretPrompt)
}
@Test
fun testSudoExpire_clearsMatchingPrompt() =
runTest {
val (viewModel, _) = createViewModelWithSession()
advanceUntilIdle()
mockEventsFlow.emit(WsEvent.SudoRequest(requestId = "sudo-1", sessionId = null))
advanceUntilIdle()
assertNotNull(viewModel.uiState.value.sudoPrompt)
mockEventsFlow.emit(WsEvent.SudoExpire(requestId = "sudo-1", sessionId = null))
advanceUntilIdle()
assertNull(viewModel.uiState.value.sudoPrompt)
}
@Test
fun testSudoExpire_ignoresNonMatchingRequestId() =
runTest {
val (viewModel, _) = createViewModelWithSession()
advanceUntilIdle()
mockEventsFlow.emit(WsEvent.SudoRequest(requestId = "sudo-2", sessionId = null))
advanceUntilIdle()
assertNotNull(viewModel.uiState.value.sudoPrompt)
mockEventsFlow.emit(WsEvent.SudoExpire(requestId = "sudo-stale", sessionId = null))
advanceUntilIdle()
assertNotNull(viewModel.uiState.value.sudoPrompt)
}
@Test
fun testSecretExpire_clearsMatchingPrompt() =
runTest {
val (viewModel, _) = createViewModelWithSession()
advanceUntilIdle()
mockEventsFlow.emit(WsEvent.SecretRequest(requestId = "secret-1", sessionId = null))
advanceUntilIdle()
assertNotNull(viewModel.uiState.value.secretPrompt)
mockEventsFlow.emit(WsEvent.SecretExpire(requestId = "secret-1", sessionId = null))
advanceUntilIdle()
assertNull(viewModel.uiState.value.secretPrompt)
}
@Test
fun testSecretRequest_setsEnvVarAndPrompt() =
runTest {
val (viewModel, _) = createViewModelWithSession()
advanceUntilIdle()
mockEventsFlow.emit(
WsEvent.SecretRequest(
requestId = "secret-1",
sessionId = null,
envVar = "GITHUB_TOKEN",
prompt = "Enter your GitHub token",
),
)
advanceUntilIdle()
val prompt = viewModel.uiState.value.secretPrompt
assertNotNull(prompt)
assertEquals("GITHUB_TOKEN", prompt?.envVar)
assertEquals("Enter your GitHub token", prompt?.prompt)
}
@Test
fun testDismissSudo_sendsEmptyPassword() =
runTest {
val (viewModel, sessionId) = createViewModelWithSession()
advanceUntilIdle()
mockEventsFlow.emit(WsEvent.SudoRequest(requestId = "sudo-1", sessionId = null))
advanceUntilIdle()
assertNotNull(viewModel.uiState.value.sudoPrompt)
viewModel.dismissSudo()
advanceUntilIdle()
assertNull(viewModel.uiState.value.sudoPrompt)
verify {
HermesWsClient.send(
WsMethods.SUDO_RESPOND,
withArg { params ->
assertEquals(sessionId, params["session_id"])
assertEquals("", params["password"])
assertEquals("sudo-1", params["request_id"])
},
any(),
)
}
}
@Test
fun testDismissSecret_sendsEmptyValue() =
runTest {
val (viewModel, sessionId) = createViewModelWithSession()
advanceUntilIdle()
mockEventsFlow.emit(WsEvent.SecretRequest(requestId = "secret-1", sessionId = null))
advanceUntilIdle()
assertNotNull(viewModel.uiState.value.secretPrompt)
viewModel.dismissSecret()
advanceUntilIdle()
assertNull(viewModel.uiState.value.secretPrompt)
verify {
HermesWsClient.send(
WsMethods.SECRET_RESPOND,
withArg { params ->
assertEquals(sessionId, params["session_id"])
assertEquals("", params["value"])
assertEquals("secret-1", params["request_id"])
},
any(),
)
}
}
// ── Settings ─────────────────────────────────────────────────────────────
@Test