feat(chat): full approval.request support (#1057)
This commit is contained in:
9 files changed
+630
-49
No files matched your search
@@ -400,6 +400,114 @@ class EventParserTest {
|
||||
assertEquals("req-expire-1", expireEvent.clarifyId)
|
||||
}
|
||||
|
||||
// ── Approval requests (full support) ───────────────────────────────
|
||||
|
||||
@Test
|
||||
fun testParseApprovalRequest_withChoicesAndRequestId() {
|
||||
val response =
|
||||
createJsonRpcResponse(
|
||||
jsonrpc = "2.0",
|
||||
id = null,
|
||||
result = null,
|
||||
error = null,
|
||||
method = "event",
|
||||
params =
|
||||
mapOf(
|
||||
"type" to "approval.request",
|
||||
"payload" to
|
||||
mapOf(
|
||||
"command" to "rm -rf /tmp/x",
|
||||
"description" to "dangerous command",
|
||||
"request_id" to "req-1",
|
||||
"choices" to listOf("once", "session", "always", "deny"),
|
||||
"allow_permanent" to true,
|
||||
"pattern_keys" to listOf("shell:rm"),
|
||||
),
|
||||
),
|
||||
)
|
||||
val event = EventParser.parse(response)
|
||||
assertTrue(event is WsEvent.ApprovalRequest)
|
||||
val approval = event as WsEvent.ApprovalRequest
|
||||
assertEquals("rm -rf /tmp/x", approval.command)
|
||||
assertEquals("req-1", approval.requestId)
|
||||
assertEquals(listOf("once", "session", "always", "deny"), approval.choices)
|
||||
assertEquals(true, approval.allowPermanent)
|
||||
assertEquals(listOf("shell:rm"), approval.patternKeys)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testParseApprovalRequest_smartDeniedDefaultsToOnceDeny() {
|
||||
val response =
|
||||
createJsonRpcResponse(
|
||||
jsonrpc = "2.0",
|
||||
id = null,
|
||||
result = null,
|
||||
error = null,
|
||||
method = "event",
|
||||
params =
|
||||
mapOf(
|
||||
"type" to "approval.request",
|
||||
"payload" to
|
||||
mapOf(
|
||||
"command" to "rm -rf /",
|
||||
"smart_denied" to true,
|
||||
),
|
||||
),
|
||||
)
|
||||
val event = EventParser.parse(response)
|
||||
assertTrue(event is WsEvent.ApprovalRequest)
|
||||
val approval = event as WsEvent.ApprovalRequest
|
||||
assertEquals(listOf("once", "deny"), approval.choices)
|
||||
assertEquals(true, approval.smartDenied)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testParseApprovalRequest_legacyDefaultsToFullChoices() {
|
||||
val response =
|
||||
createJsonRpcResponse(
|
||||
jsonrpc = "2.0",
|
||||
id = null,
|
||||
result = null,
|
||||
error = null,
|
||||
method = "event",
|
||||
params =
|
||||
mapOf(
|
||||
"type" to "approval.request",
|
||||
"payload" to mapOf("command" to "ls"),
|
||||
),
|
||||
)
|
||||
val event = EventParser.parse(response)
|
||||
assertTrue(event is WsEvent.ApprovalRequest)
|
||||
val approval = event as WsEvent.ApprovalRequest
|
||||
assertEquals(listOf("once", "session", "always", "deny"), approval.choices)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testParseApprovalRequest_allowPermanentFalseStillParses() {
|
||||
val response =
|
||||
createJsonRpcResponse(
|
||||
jsonrpc = "2.0",
|
||||
id = null,
|
||||
result = null,
|
||||
error = null,
|
||||
method = "event",
|
||||
params =
|
||||
mapOf(
|
||||
"type" to "approval.request",
|
||||
"payload" to
|
||||
mapOf(
|
||||
"command" to "rm",
|
||||
"choices" to listOf("once", "session", "always", "deny"),
|
||||
"allow_permanent" to false,
|
||||
),
|
||||
),
|
||||
)
|
||||
val event = EventParser.parse(response)
|
||||
assertTrue(event is WsEvent.ApprovalRequest)
|
||||
val approval = event as WsEvent.ApprovalRequest
|
||||
assertEquals(false, approval.allowPermanent)
|
||||
}
|
||||
|
||||
// ── TEST-07: Untested subtypes ─────────────────────────────────────
|
||||
|
||||
@Test
|
||||
|
||||
@@ -3035,6 +3035,167 @@ class ChatViewModelTest {
|
||||
assertNull(msgAfter!!.approvalInfo)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testApprovalRequest_storesRequestIdAndChoices() =
|
||||
runTest {
|
||||
val viewModel = createViewModel()
|
||||
advanceUntilIdle()
|
||||
|
||||
mockEventsFlow.emit(
|
||||
WsEvent.ApprovalRequest(
|
||||
command = "rm -rf /tmp/x",
|
||||
description = "dangerous command",
|
||||
patternKeys = listOf("shell:rm"),
|
||||
sessionId = null,
|
||||
requestId = "req-1",
|
||||
choices = listOf("once", "session", "always", "deny"),
|
||||
allowPermanent = true,
|
||||
smartDenied = false,
|
||||
),
|
||||
)
|
||||
advanceUntilIdle()
|
||||
|
||||
val msg =
|
||||
viewModel.uiState.value.messages
|
||||
.first { it.content.contains("Approval Required") }
|
||||
assertEquals("req-1", msg.approvalInfo?.requestId)
|
||||
assertEquals(
|
||||
listOf("once", "session", "always", "deny"),
|
||||
msg.approvalInfo?.choices,
|
||||
)
|
||||
assertEquals(true, msg.approvalInfo?.allowPermanent)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testRespondToApproval_mapsApproveToOnceAndSendsRequestId() =
|
||||
runTest {
|
||||
val (viewModel, sessionId) = createViewModelWithSession()
|
||||
advanceUntilIdle()
|
||||
|
||||
mockEventsFlow.emit(
|
||||
WsEvent.ApprovalRequest(
|
||||
command = "rm",
|
||||
description = "Dangerous",
|
||||
patternKeys = null,
|
||||
sessionId = null,
|
||||
requestId = "req-42",
|
||||
choices = listOf("once", "deny"),
|
||||
),
|
||||
)
|
||||
advanceUntilIdle()
|
||||
|
||||
viewModel.respondToApproval("approve")
|
||||
advanceUntilIdle()
|
||||
|
||||
verify {
|
||||
HermesWsClient.send(
|
||||
WsMethods.APPROVAL_RESPOND,
|
||||
withArg { params ->
|
||||
assertEquals(sessionId, params["session_id"])
|
||||
assertEquals("once", params["choice"])
|
||||
assertEquals("req-42", params["request_id"])
|
||||
},
|
||||
any(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testRespondToApproval_sessionChoice() =
|
||||
runTest {
|
||||
val (viewModel, sessionId) = createViewModelWithSession()
|
||||
advanceUntilIdle()
|
||||
|
||||
mockEventsFlow.emit(
|
||||
WsEvent.ApprovalRequest(
|
||||
command = "rm",
|
||||
description = "Dangerous",
|
||||
patternKeys = null,
|
||||
sessionId = null,
|
||||
requestId = "req-7",
|
||||
choices = listOf("once", "session", "always", "deny"),
|
||||
),
|
||||
)
|
||||
advanceUntilIdle()
|
||||
|
||||
viewModel.respondToApproval("session")
|
||||
advanceUntilIdle()
|
||||
|
||||
verify {
|
||||
HermesWsClient.send(
|
||||
WsMethods.APPROVAL_RESPOND,
|
||||
withArg { params ->
|
||||
assertEquals("session", params["choice"])
|
||||
assertEquals("req-7", params["request_id"])
|
||||
},
|
||||
any(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testRespondToApproval_omitsRequestIdWhenAbsent() =
|
||||
runTest {
|
||||
val (viewModel, sessionId) = createViewModelWithSession()
|
||||
advanceUntilIdle()
|
||||
|
||||
mockEventsFlow.emit(
|
||||
WsEvent.ApprovalRequest(
|
||||
command = "ls",
|
||||
description = "Safe",
|
||||
patternKeys = null,
|
||||
sessionId = null,
|
||||
),
|
||||
)
|
||||
advanceUntilIdle()
|
||||
|
||||
viewModel.respondToApproval("deny")
|
||||
advanceUntilIdle()
|
||||
|
||||
verify {
|
||||
HermesWsClient.send(
|
||||
WsMethods.APPROVAL_RESPOND,
|
||||
withArg { params ->
|
||||
assertEquals("deny", params["choice"])
|
||||
assertFalse(params.containsKey("request_id"))
|
||||
},
|
||||
any(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testRespondToApproval_triggersPendingReplay() =
|
||||
runTest {
|
||||
val (viewModel, sessionId) = createViewModelWithSession()
|
||||
advanceUntilIdle()
|
||||
|
||||
mockEventsFlow.emit(
|
||||
WsEvent.ApprovalRequest(
|
||||
command = "rm",
|
||||
description = "Dangerous",
|
||||
patternKeys = null,
|
||||
sessionId = null,
|
||||
requestId = "req-9",
|
||||
choices = listOf("once", "deny"),
|
||||
),
|
||||
)
|
||||
advanceUntilIdle()
|
||||
|
||||
viewModel.respondToApproval("once")
|
||||
advanceUntilIdle()
|
||||
|
||||
verify {
|
||||
HermesWsClient.send(
|
||||
WsMethods.APPROVAL_PENDING,
|
||||
withArg { params ->
|
||||
assertEquals(sessionId, params["session_id"])
|
||||
},
|
||||
any(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ── Sudo / secret prompt flow (issue #524) ───────────────────────────
|
||||
|
||||
@Test
|
||||
|
||||
Reference in new issue
Block a user