import { describe, expect, it, vi } from 'vitest'; import type { ApprovalHandler, ApprovalRequest, ApprovalResponse } from '#/index'; import { Session } from '#/index'; import type { SDKRpcClientBase } from '#/rpc'; describe('Session approval handler', () => { it('registers an approval handler and returns approved responses', async () => { const rpc = new FakeSDKRpcClient(); const session = new Session({ id: 'ses_approval_handler', workDir: '/tmp', rpc: rpc.asRpc(), }); const handler = vi.fn(async (request: ApprovalRequest) => { expect(request).toMatchObject({ toolCallId: 'tool_1', toolName: 'Bash', action: 'Run command', }); return { decision: 'approved' as const, selectedLabel: 'Approve once' }; }); session.setApprovalHandler(handler); await expect( rpc.requestApproval( session.id, 'main', approvalRequest({ toolCallId: 'tool_1', toolName: 'Bash', action: 'Run command', }), ), ).resolves.toEqual({ decision: 'approved', selectedLabel: 'Approve once' }); expect(handler).toHaveBeenCalledTimes(1); }); it('sends rejected responses with feedback', async () => { const rpc = new FakeSDKRpcClient(); const session = new Session({ id: 'ses_approval_rejected', workDir: '/tmp', rpc: rpc.asRpc(), }); session.setApprovalHandler(() => ({ decision: 'rejected', feedback: 'No writes.' })); await expect( rpc.requestApproval( session.id, 'main', approvalRequest({ toolCallId: 'tool_2', toolName: 'Write', action: 'Write file', }), ), ).resolves.toEqual({ decision: 'rejected', feedback: 'No writes.' }); }); it('sends session-scoped approved responses when requested', async () => { const rpc = new FakeSDKRpcClient(); const session = new Session({ id: 'ses_approval_scope', workDir: '/tmp', rpc: rpc.asRpc(), }); session.setApprovalHandler(() => ({ decision: 'approved', scope: 'session' })); await expect( rpc.requestApproval( session.id, 'main', approvalRequest({ toolCallId: 'tool_scope', toolName: 'Bash', action: 'Run command', }), ), ).resolves.toEqual({ decision: 'approved', scope: 'session' }); }); it('cancels approval requests when no handler is registered', async () => { const rpc = new FakeSDKRpcClient(); const session = new Session({ id: 'ses_approval_default', workDir: '/tmp', rpc: rpc.asRpc(), }); await expect( rpc.requestApproval( session.id, 'main', approvalRequest({ toolCallId: 'tool_3', toolName: 'Bash', action: 'Run command', }), ), ).resolves.toEqual({ decision: 'cancelled', feedback: 'No approval handler registered.', }); await session.close(); expect(rpc.closeSession).toHaveBeenCalledWith({ sessionId: session.id }); }); it('cancels approval requests when the handler throws', async () => { const rpc = new FakeSDKRpcClient(); const session = new Session({ id: 'ses_approval_throw', workDir: '/tmp', rpc: rpc.asRpc(), }); session.setApprovalHandler(() => { throw new Error('boom'); }); await expect( rpc.requestApproval( session.id, 'main', approvalRequest({ toolCallId: 'tool_4', toolName: 'Bash', action: 'Run command', }), ), ).resolves.toEqual({ decision: 'cancelled', feedback: 'Approval handler failed.', }); }); it('responds to the original subagent id', async () => { const rpc = new FakeSDKRpcClient(); const session = new Session({ id: 'ses_approval_subagent', workDir: '/tmp', rpc: rpc.asRpc(), }); const handler = vi.fn(() => ({ decision: 'approved' as const })); session.setApprovalHandler(handler); await expect( rpc.requestApproval( session.id, 'agent-1', approvalRequest({ toolCallId: 'tool_5', toolName: 'Read', action: 'Read file', }), ), ).resolves.toEqual({ decision: 'approved' }); expect(handler).toHaveBeenCalledWith( expect.objectContaining({ sessionId: session.id, agentId: 'agent-1', }), ); }); }); interface ApprovalRequestInput { readonly toolCallId: string; readonly toolName: string; readonly action: string; } function approvalRequest(input: ApprovalRequestInput): ApprovalRequest { return { toolCallId: input.toolCallId, toolName: input.toolName, action: input.action, display: { kind: 'generic', summary: input.action }, }; } class FakeSDKRpcClient { private readonly approvalHandlers = new Map(); readonly closeSession = vi.fn(async (_input: { readonly sessionId: string }) => {}); asRpc(): SDKRpcClientBase { return this as unknown as SDKRpcClientBase; } setApprovalHandler(sessionId: string, handler: ApprovalHandler | undefined): void { if (handler === undefined) { this.approvalHandlers.delete(sessionId); return; } this.approvalHandlers.set(sessionId, handler); } async requestApproval( sessionId: string, agentId: string, request: ApprovalRequest, ): Promise { const handler = this.approvalHandlers.get(sessionId); if (handler === undefined) { return { decision: 'cancelled', feedback: 'No approval handler registered.', }; } try { return await handler({ ...request, sessionId, agentId } as ApprovalRequest); } catch { return { decision: 'cancelled', feedback: 'Approval handler failed.', }; } } clearSessionHandlers(sessionId: string): void { this.approvalHandlers.delete(sessionId); } }