146 lines
4.8 KiB
TypeScript
146 lines
4.8 KiB
TypeScript
import { describe, expect, test, beforeEach } from 'bun:test'
|
|
|
|
import type { RequestPermissionRequest } from '@agentclientprotocol/sdk'
|
|
|
|
import {
|
|
permissionQueue,
|
|
currentPermission,
|
|
requestPermissionFromUser,
|
|
respondToPermission,
|
|
rejectCurrentPermission
|
|
} from '@/app/ai/acp/permission'
|
|
|
|
function makeRequest(
|
|
options: { optionId: string; kind: string; name: string }[] = [
|
|
{ optionId: 'allow', kind: 'allow_once', name: 'Allow once' },
|
|
{ optionId: 'reject', kind: 'reject_once', name: 'Reject' }
|
|
]
|
|
): RequestPermissionRequest {
|
|
return {
|
|
sessionId: 'session-1',
|
|
options: options.map((o) => ({
|
|
optionId: o.optionId,
|
|
kind: o.kind as 'allow_once' | 'allow_always' | 'reject_once' | 'reject_always',
|
|
name: o.name
|
|
})),
|
|
toolCall: {
|
|
sessionUpdate: 'tool_call_update',
|
|
toolCallId: 'tc-1',
|
|
status: 'pending' as const
|
|
}
|
|
}
|
|
}
|
|
|
|
describe('acp-permission', () => {
|
|
beforeEach(() => {
|
|
permissionQueue.value = []
|
|
})
|
|
|
|
test('requestPermissionFromUser queues an entry', () => {
|
|
void requestPermissionFromUser(makeRequest())
|
|
expect(permissionQueue.value).toHaveLength(1)
|
|
expect(currentPermission.value).not.toBeNull()
|
|
expect(currentPermission.value?.request.sessionId).toBe('session-1')
|
|
respondToPermission('allow')
|
|
})
|
|
|
|
test('respondToPermission resolves with selected optionId', async () => {
|
|
const promise = requestPermissionFromUser(makeRequest())
|
|
respondToPermission('allow')
|
|
const result = await promise
|
|
expect(result.outcome.outcome).toBe('selected')
|
|
expect(result.outcome.optionId).toBe('allow')
|
|
expect(permissionQueue.value).toHaveLength(0)
|
|
})
|
|
|
|
test('rejectCurrentPermission picks reject option', async () => {
|
|
const promise = requestPermissionFromUser(makeRequest())
|
|
rejectCurrentPermission()
|
|
const result = await promise
|
|
expect(result.outcome.optionId).toBe('reject')
|
|
})
|
|
|
|
test('rejectCurrentPermission falls back to first option when no reject kind', async () => {
|
|
const req = makeRequest([{ optionId: 'only-allow', kind: 'allow_once', name: 'Allow' }])
|
|
const promise = requestPermissionFromUser(req)
|
|
rejectCurrentPermission()
|
|
const result = await promise
|
|
expect(result.outcome.optionId).toBe('only-allow')
|
|
})
|
|
|
|
test('queue handles multiple concurrent requests in order', async () => {
|
|
const p1 = requestPermissionFromUser(
|
|
makeRequest([{ optionId: 'a1', kind: 'allow_once', name: 'First' }])
|
|
)
|
|
const p2 = requestPermissionFromUser(
|
|
makeRequest([{ optionId: 'a2', kind: 'allow_once', name: 'Second' }])
|
|
)
|
|
expect(permissionQueue.value).toHaveLength(2)
|
|
expect(currentPermission.value?.request.options[0].optionId).toBe('a1')
|
|
|
|
respondToPermission('a1')
|
|
const r1 = await p1
|
|
expect(r1.outcome.optionId).toBe('a1')
|
|
expect(currentPermission.value?.request.options[0].optionId).toBe('a2')
|
|
|
|
respondToPermission('a2')
|
|
const r2 = await p2
|
|
expect(r2.outcome.optionId).toBe('a2')
|
|
expect(permissionQueue.value).toHaveLength(0)
|
|
})
|
|
|
|
test('respondToPermission is no-op when queue is empty', () => {
|
|
expect(permissionQueue.value).toHaveLength(0)
|
|
respondToPermission('anything')
|
|
expect(permissionQueue.value).toHaveLength(0)
|
|
})
|
|
|
|
test('rejectCurrentPermission is no-op when queue is empty', () => {
|
|
expect(permissionQueue.value).toHaveLength(0)
|
|
rejectCurrentPermission()
|
|
expect(permissionQueue.value).toHaveLength(0)
|
|
})
|
|
|
|
test('timer cleared on manual resolution (no double resolve)', async () => {
|
|
const promise = requestPermissionFromUser(makeRequest())
|
|
respondToPermission('allow')
|
|
const result = await promise
|
|
expect(result.outcome.optionId).toBe('allow')
|
|
// Timer should be cleared — wait to ensure no stale timeout fires
|
|
await new Promise((resolve) => {
|
|
setTimeout(resolve, 50)
|
|
})
|
|
expect(permissionQueue.value).toHaveLength(0)
|
|
})
|
|
|
|
test('timeout auto-rejects after delay', async () => {
|
|
const req = makeRequest()
|
|
// Override timeout for test speed by creating request and manually triggering the timer
|
|
const promise = requestPermissionFromUser(req)
|
|
const entry = permissionQueue.value[0]
|
|
expect(entry).toBeDefined()
|
|
|
|
// Fast-forward: manually fire the timer callback
|
|
clearTimeout(entry.timer)
|
|
const timerFn = () => {
|
|
const idx = permissionQueue.value.indexOf(entry)
|
|
if (idx === -1) return
|
|
permissionQueue.value = permissionQueue.value.filter((e) => e !== entry)
|
|
entry.resolve({
|
|
outcome: {
|
|
outcome: 'selected',
|
|
optionId:
|
|
req.options.find((o) => o.kind.startsWith('reject'))?.optionId ??
|
|
req.options[0]?.optionId ??
|
|
''
|
|
}
|
|
})
|
|
}
|
|
timerFn()
|
|
|
|
const result = await promise
|
|
expect(result.outcome.optionId).toBe('reject')
|
|
expect(permissionQueue.value).toHaveLength(0)
|
|
})
|
|
})
|