/** * @license * Copyright 2025 Google LLC * SPDX-License-Identifier: Apache-2.0 */ /* eslint-disable @typescript-eslint/no-explicit-any */ import { describe, it, expect, vi, beforeEach, afterEach, Mock } from 'vitest'; import { renderHook, act } from '@testing-library/react'; import { useReactToolScheduler, mapToDisplay, } from './useReactToolScheduler.js'; import { Part, PartListUnion, PartUnion, FunctionResponse, } from '@google/genai'; import { Config, ToolCallRequestInfo, Tool, ToolRegistry, ToolResult, ToolCallConfirmationDetails, ToolConfirmationOutcome, ToolCallResponseInfo, formatLlmContentForFunctionResponse, // Import from core ToolCall, // Import from core Status as ToolCallStatusType, ApprovalMode, // Import from core } from '@gemini-code/core'; import { HistoryItemWithoutId, ToolCallStatus, HistoryItemToolGroup, } from '../types.js'; // Mocks vi.mock('@gemini-code/core', async () => { const actual = await vi.importActual('@gemini-code/core'); return { ...actual, ToolRegistry: vi.fn(), Config: vi.fn(), }; }); const mockToolRegistry = { getTool: vi.fn(), }; const mockConfig = { getToolRegistry: vi.fn(() => mockToolRegistry as unknown as ToolRegistry), getApprovalMode: vi.fn(() => ApprovalMode.DEFAULT), }; const mockTool: Tool = { name: 'mockTool', displayName: 'Mock Tool', description: 'A mock tool for testing', isOutputMarkdown: false, canUpdateOutput: false, schema: {}, validateToolParams: vi.fn(), execute: vi.fn(), shouldConfirmExecute: vi.fn(), getDescription: vi.fn((args) => `Description for ${JSON.stringify(args)}`), }; const mockToolWithLiveOutput: Tool = { ...mockTool, name: 'mockToolWithLiveOutput', displayName: 'Mock Tool With Live Output', canUpdateOutput: true, }; let mockOnUserConfirmForToolConfirmation: Mock; const mockToolRequiresConfirmation: Tool = { ...mockTool, name: 'mockToolRequiresConfirmation', displayName: 'Mock Tool Requires Confirmation', shouldConfirmExecute: vi.fn( async (): Promise => ({ type: 'edit', title: 'Mock Tool Requires Confirmation', onConfirm: mockOnUserConfirmForToolConfirmation, fileName: 'mockToolRequiresConfirmation.ts', fileDiff: 'Mock tool requires confirmation', }), ), }; describe('formatLlmContentForFunctionResponse', () => { it('should handle simple string llmContent', () => { const llmContent = 'Simple text output'; const { functionResponseJson, additionalParts } = formatLlmContentForFunctionResponse(llmContent); expect(functionResponseJson).toEqual({ output: 'Simple text output' }); expect(additionalParts).toEqual([]); }); it('should handle llmContent as a single Part with text', () => { const llmContent: Part = { text: 'Text from Part object' }; const { functionResponseJson, additionalParts } = formatLlmContentForFunctionResponse(llmContent); expect(functionResponseJson).toEqual({ output: 'Text from Part object' }); expect(additionalParts).toEqual([]); }); it('should handle llmContent as a PartListUnion array with a single text Part', () => { const llmContent: PartListUnion = [{ text: 'Text from array' }]; const { functionResponseJson, additionalParts } = formatLlmContentForFunctionResponse(llmContent); expect(functionResponseJson).toEqual({ output: 'Text from array' }); expect(additionalParts).toEqual([]); }); it('should handle llmContent with inlineData', () => { const llmContent: Part = { inlineData: { mimeType: 'image/png', data: 'base64...' }, }; const { functionResponseJson, additionalParts } = formatLlmContentForFunctionResponse(llmContent); expect(functionResponseJson).toEqual({ status: 'Binary content of type image/png was processed.', }); expect(additionalParts).toEqual([llmContent]); }); it('should handle llmContent with fileData', () => { const llmContent: Part = { fileData: { mimeType: 'application/pdf', fileUri: 'gs://...' }, }; const { functionResponseJson, additionalParts } = formatLlmContentForFunctionResponse(llmContent); expect(functionResponseJson).toEqual({ status: 'Binary content of type application/pdf was processed.', }); expect(additionalParts).toEqual([llmContent]); }); it('should handle llmContent as an array of multiple Parts (text and inlineData)', () => { const llmContent: PartListUnion = [ { text: 'Some textual description' }, { inlineData: { mimeType: 'image/jpeg', data: 'base64data...' } }, { text: 'Another text part' }, ]; const { functionResponseJson, additionalParts } = formatLlmContentForFunctionResponse(llmContent); expect(functionResponseJson).toEqual({ status: 'Tool execution succeeded.', }); expect(additionalParts).toEqual(llmContent); }); it('should handle llmContent as an array with a single inlineData Part', () => { const llmContent: PartListUnion = [ { inlineData: { mimeType: 'image/gif', data: 'gifdata...' } }, ]; const { functionResponseJson, additionalParts } = formatLlmContentForFunctionResponse(llmContent); expect(functionResponseJson).toEqual({ status: 'Binary content of type image/gif was processed.', }); expect(additionalParts).toEqual(llmContent); }); it('should handle llmContent as a generic Part (not text, inlineData, or fileData)', () => { const llmContent: Part = { functionCall: { name: 'test', args: {} } }; const { functionResponseJson, additionalParts } = formatLlmContentForFunctionResponse(llmContent); expect(functionResponseJson).toEqual({ status: 'Tool execution succeeded.', }); expect(additionalParts).toEqual([llmContent]); }); it('should handle empty string llmContent', () => { const llmContent = ''; const { functionResponseJson, additionalParts } = formatLlmContentForFunctionResponse(llmContent); expect(functionResponseJson).toEqual({ output: '' }); expect(additionalParts).toEqual([]); }); it('should handle llmContent as an empty array', () => { const llmContent: PartListUnion = []; const { functionResponseJson, additionalParts } = formatLlmContentForFunctionResponse(llmContent); expect(functionResponseJson).toEqual({ status: 'Tool execution succeeded.', }); expect(additionalParts).toEqual([]); }); it('should handle llmContent as a Part with undefined inlineData/fileData/text', () => { const llmContent: Part = {}; // An empty part object const { functionResponseJson, additionalParts } = formatLlmContentForFunctionResponse(llmContent); expect(functionResponseJson).toEqual({ status: 'Tool execution succeeded.', }); expect(additionalParts).toEqual([llmContent]); }); }); describe('useReactToolScheduler in YOLO Mode', () => { let onComplete: Mock; let setPendingHistoryItem: Mock; beforeEach(() => { onComplete = vi.fn(); setPendingHistoryItem = vi.fn(); mockToolRegistry.getTool.mockClear(); (mockToolRequiresConfirmation.execute as Mock).mockClear(); (mockToolRequiresConfirmation.shouldConfirmExecute as Mock).mockClear(); // IMPORTANT: Enable YOLO mode for this test suite (mockConfig.getApprovalMode as Mock).mockReturnValue(ApprovalMode.YOLO); vi.useFakeTimers(); }); afterEach(() => { vi.clearAllTimers(); vi.useRealTimers(); // IMPORTANT: Disable YOLO mode after this test suite (mockConfig.getApprovalMode as Mock).mockReturnValue(ApprovalMode.DEFAULT); }); const renderSchedulerInYoloMode = () => renderHook(() => useReactToolScheduler( onComplete, mockConfig as unknown as Config, setPendingHistoryItem, ), ); it('should skip confirmation and execute tool directly when yoloMode is true', async () => { mockToolRegistry.getTool.mockReturnValue(mockToolRequiresConfirmation); const expectedOutput = 'YOLO Confirmed output'; (mockToolRequiresConfirmation.execute as Mock).mockResolvedValue({ llmContent: expectedOutput, returnDisplay: 'YOLO Formatted tool output', } as ToolResult); const { result } = renderSchedulerInYoloMode(); const schedule = result.current[1]; const request: ToolCallRequestInfo = { callId: 'yoloCall', name: 'mockToolRequiresConfirmation', args: { data: 'any data' }, }; act(() => { schedule(request); }); await act(async () => { await vi.runAllTimersAsync(); // Process validation }); await act(async () => { await vi.runAllTimersAsync(); // Process scheduling }); await act(async () => { await vi.runAllTimersAsync(); // Process execution }); // Check that shouldConfirmExecute was NOT called expect( mockToolRequiresConfirmation.shouldConfirmExecute, ).not.toHaveBeenCalled(); // Check that execute WAS called expect(mockToolRequiresConfirmation.execute).toHaveBeenCalledWith( request.args, expect.any(AbortSignal), undefined, ); // Check that onComplete was called with success expect(onComplete).toHaveBeenCalledWith([ expect.objectContaining({ status: 'success', request, response: expect.objectContaining({ resultDisplay: 'YOLO Formatted tool output', responseParts: expect.arrayContaining([ expect.objectContaining({ functionResponse: expect.objectContaining({ response: { output: expectedOutput }, }), }), ]), }), }), ]); // Ensure no confirmation UI was triggered (setPendingHistoryItem should not have been called with confirmation details) const setPendingHistoryItemCalls = setPendingHistoryItem.mock.calls; const confirmationCall = setPendingHistoryItemCalls.find((call) => { const item = typeof call[0] === 'function' ? call[0]({}) : call[0]; return item?.tools?.[0]?.confirmationDetails; }); expect(confirmationCall).toBeUndefined(); }); }); describe('useReactToolScheduler', () => { // TODO(ntaylormullen): The following tests are skipped due to difficulties in // reliably testing the asynchronous state updates and interactions with timers. // These tests involve complex sequences of events, including confirmations, // live output updates, and cancellations, which are challenging to assert // correctly with the current testing setup. Further investigation is needed // to find a robust way to test these scenarios. let onComplete: Mock; let setPendingHistoryItem: Mock; let capturedOnConfirmForTest: | ((outcome: ToolConfirmationOutcome) => void | Promise) | undefined; beforeEach(() => { onComplete = vi.fn(); capturedOnConfirmForTest = undefined; setPendingHistoryItem = vi.fn((updaterOrValue) => { let pendingItem: HistoryItemWithoutId | null = null; if (typeof updaterOrValue === 'function') { // Loosen the type for prevState to allow for more flexible updates in tests const prevState: Partial = { type: 'tool_group', // Still default to tool_group for most cases tools: [], }; pendingItem = updaterOrValue(prevState as any); // Allow any for more flexibility } else { pendingItem = updaterOrValue; } // Capture onConfirm if it exists, regardless of the exact type of pendingItem // This is a common pattern in these tests. if ( (pendingItem as HistoryItemToolGroup)?.tools?.[0]?.confirmationDetails ?.onConfirm ) { capturedOnConfirmForTest = (pendingItem as HistoryItemToolGroup) .tools[0].confirmationDetails?.onConfirm; } }); mockToolRegistry.getTool.mockClear(); (mockTool.execute as Mock).mockClear(); (mockTool.shouldConfirmExecute as Mock).mockClear(); (mockToolWithLiveOutput.execute as Mock).mockClear(); (mockToolWithLiveOutput.shouldConfirmExecute as Mock).mockClear(); (mockToolRequiresConfirmation.execute as Mock).mockClear(); (mockToolRequiresConfirmation.shouldConfirmExecute as Mock).mockClear(); mockOnUserConfirmForToolConfirmation = vi.fn(); ( mockToolRequiresConfirmation.shouldConfirmExecute as Mock ).mockImplementation( async (): Promise => ({ onConfirm: mockOnUserConfirmForToolConfirmation, fileName: 'mockToolRequiresConfirmation.ts', fileDiff: 'Mock tool requires confirmation', type: 'edit', title: 'Mock Tool Requires Confirmation', }), ); vi.useFakeTimers(); }); afterEach(() => { vi.clearAllTimers(); vi.useRealTimers(); }); const renderScheduler = () => renderHook(() => useReactToolScheduler( onComplete, mockConfig as unknown as Config, setPendingHistoryItem, ), ); it('initial state should be empty', () => { const { result } = renderScheduler(); expect(result.current[0]).toEqual([]); }); it('should schedule and execute a tool call successfully', async () => { mockToolRegistry.getTool.mockReturnValue(mockTool); (mockTool.execute as Mock).mockResolvedValue({ llmContent: 'Tool output', returnDisplay: 'Formatted tool output', } as ToolResult); (mockTool.shouldConfirmExecute as Mock).mockResolvedValue(null); const { result } = renderScheduler(); const schedule = result.current[1]; const request: ToolCallRequestInfo = { callId: 'call1', name: 'mockTool', args: { param: 'value' }, }; act(() => { schedule(request); }); await act(async () => { await vi.runAllTimersAsync(); }); await act(async () => { await vi.runAllTimersAsync(); }); await act(async () => { await vi.runAllTimersAsync(); }); expect(mockTool.execute).toHaveBeenCalledWith( request.args, expect.any(AbortSignal), undefined, ); expect(onComplete).toHaveBeenCalledWith([ expect.objectContaining({ status: 'success', request, response: expect.objectContaining({ resultDisplay: 'Formatted tool output', responseParts: expect.arrayContaining([ expect.objectContaining({ functionResponse: expect.objectContaining({ response: { output: 'Tool output' }, }), }), ]), }), }), ]); expect(result.current[0]).toEqual([]); }); it('should handle tool not found', async () => { mockToolRegistry.getTool.mockReturnValue(undefined); const { result } = renderScheduler(); const schedule = result.current[1]; const request: ToolCallRequestInfo = { callId: 'call1', name: 'nonExistentTool', args: {}, }; act(() => { schedule(request); }); await act(async () => { await vi.runAllTimersAsync(); }); await act(async () => { await vi.runAllTimersAsync(); }); expect(onComplete).toHaveBeenCalledWith([ expect.objectContaining({ status: 'error', request, response: expect.objectContaining({ error: expect.objectContaining({ message: 'Tool "nonExistentTool" not found in registry.', }), }), }), ]); expect(result.current[0]).toEqual([]); }); it('should handle error during shouldConfirmExecute', async () => { mockToolRegistry.getTool.mockReturnValue(mockTool); const confirmError = new Error('Confirmation check failed'); (mockTool.shouldConfirmExecute as Mock).mockRejectedValue(confirmError); const { result } = renderScheduler(); const schedule = result.current[1]; const request: ToolCallRequestInfo = { callId: 'call1', name: 'mockTool', args: {}, }; act(() => { schedule(request); }); await act(async () => { await vi.runAllTimersAsync(); }); await act(async () => { await vi.runAllTimersAsync(); }); expect(onComplete).toHaveBeenCalledWith([ expect.objectContaining({ status: 'error', request, response: expect.objectContaining({ error: confirmError, }), }), ]); expect(result.current[0]).toEqual([]); }); it('should handle error during execute', async () => { mockToolRegistry.getTool.mockReturnValue(mockTool); (mockTool.shouldConfirmExecute as Mock).mockResolvedValue(null); const execError = new Error('Execution failed'); (mockTool.execute as Mock).mockRejectedValue(execError); const { result } = renderScheduler(); const schedule = result.current[1]; const request: ToolCallRequestInfo = { callId: 'call1', name: 'mockTool', args: {}, }; act(() => { schedule(request); }); await act(async () => { await vi.runAllTimersAsync(); }); await act(async () => { await vi.runAllTimersAsync(); }); await act(async () => { await vi.runAllTimersAsync(); }); expect(onComplete).toHaveBeenCalledWith([ expect.objectContaining({ status: 'error', request, response: expect.objectContaining({ error: execError, }), }), ]); expect(result.current[0]).toEqual([]); }); it.skip('should handle tool requiring confirmation - approved', async () => { mockToolRegistry.getTool.mockReturnValue(mockToolRequiresConfirmation); const expectedOutput = 'Confirmed output'; (mockToolRequiresConfirmation.execute as Mock).mockResolvedValue({ llmContent: expectedOutput, returnDisplay: 'Confirmed display', } as ToolResult); const { result } = renderScheduler(); const schedule = result.current[1]; const request: ToolCallRequestInfo = { callId: 'callConfirm', name: 'mockToolRequiresConfirmation', args: { data: 'sensitive' }, }; act(() => { schedule(request); }); await act(async () => { await vi.runAllTimersAsync(); }); expect(setPendingHistoryItem).toHaveBeenCalled(); expect(capturedOnConfirmForTest).toBeDefined(); await act(async () => { await capturedOnConfirmForTest?.(ToolConfirmationOutcome.ProceedOnce); }); await act(async () => { await vi.runAllTimersAsync(); }); await act(async () => { await vi.runAllTimersAsync(); }); await act(async () => { await vi.runAllTimersAsync(); }); expect(mockOnUserConfirmForToolConfirmation).toHaveBeenCalledWith( ToolConfirmationOutcome.ProceedOnce, ); expect(mockToolRequiresConfirmation.execute).toHaveBeenCalled(); expect(onComplete).toHaveBeenCalledWith([ expect.objectContaining({ status: 'success', request, response: expect.objectContaining({ resultDisplay: 'Confirmed display', responseParts: expect.arrayContaining([ expect.objectContaining({ functionResponse: expect.objectContaining({ response: { output: expectedOutput }, }), }), ]), }), }), ]); }); it.skip('should handle tool requiring confirmation - cancelled by user', async () => { mockToolRegistry.getTool.mockReturnValue(mockToolRequiresConfirmation); const { result } = renderScheduler(); const schedule = result.current[1]; const request: ToolCallRequestInfo = { callId: 'callConfirmCancel', name: 'mockToolRequiresConfirmation', args: {}, }; act(() => { schedule(request); }); await act(async () => { await vi.runAllTimersAsync(); }); expect(setPendingHistoryItem).toHaveBeenCalled(); expect(capturedOnConfirmForTest).toBeDefined(); await act(async () => { await capturedOnConfirmForTest?.(ToolConfirmationOutcome.Cancel); }); await act(async () => { await vi.runAllTimersAsync(); }); await act(async () => { await vi.runAllTimersAsync(); }); expect(mockOnUserConfirmForToolConfirmation).toHaveBeenCalledWith( ToolConfirmationOutcome.Cancel, ); expect(onComplete).toHaveBeenCalledWith([ expect.objectContaining({ status: 'cancelled', request, response: expect.objectContaining({ responseParts: expect.arrayContaining([ expect.objectContaining({ functionResponse: expect.objectContaining({ response: expect.objectContaining({ error: `User did not allow tool call ${request.name}. Reason: User cancelled.`, }), }), }), ]), }), }), ]); }); it.skip('should handle live output updates', async () => { mockToolRegistry.getTool.mockReturnValue(mockToolWithLiveOutput); let liveUpdateFn: ((output: string) => void) | undefined; let resolveExecutePromise: (value: ToolResult) => void; const executePromise = new Promise((resolve) => { resolveExecutePromise = resolve; }); (mockToolWithLiveOutput.execute as Mock).mockImplementation( async ( _args: any, _signal: any, updateFn: ((output: string) => void) | undefined, ) => { liveUpdateFn = updateFn; return executePromise; }, ); (mockToolWithLiveOutput.shouldConfirmExecute as Mock).mockResolvedValue( null, ); const { result } = renderScheduler(); const schedule = result.current[1]; const request: ToolCallRequestInfo = { callId: 'liveCall', name: 'mockToolWithLiveOutput', args: {}, }; act(() => { schedule(request); }); await act(async () => { await vi.runAllTimersAsync(); }); expect(liveUpdateFn).toBeDefined(); expect(setPendingHistoryItem).toHaveBeenCalled(); await act(async () => { liveUpdateFn?.('Live output 1'); }); await act(async () => { await vi.runAllTimersAsync(); }); await act(async () => { liveUpdateFn?.('Live output 2'); }); await act(async () => { await vi.runAllTimersAsync(); }); act(() => { resolveExecutePromise({ llmContent: 'Final output', returnDisplay: 'Final display', } as ToolResult); }); await act(async () => { await vi.runAllTimersAsync(); }); await act(async () => { await vi.runAllTimersAsync(); }); expect(onComplete).toHaveBeenCalledWith([ expect.objectContaining({ status: 'success', request, response: expect.objectContaining({ resultDisplay: 'Final display', responseParts: expect.arrayContaining([ expect.objectContaining({ functionResponse: expect.objectContaining({ response: { output: 'Final output' }, }), }), ]), }), }), ]); expect(result.current[0]).toEqual([]); }); it.skip('should cancel tool calls before execution (e.g. when status is scheduled)', async () => { mockToolRegistry.getTool.mockReturnValue(mockTool); (mockTool.shouldConfirmExecute as Mock).mockResolvedValue(null); (mockTool.execute as Mock).mockReturnValue(new Promise(() => {})); const { result } = renderScheduler(); const schedule = result.current[1]; const cancel = result.current[2]; const request: ToolCallRequestInfo = { callId: 'cancelCall', name: 'mockTool', args: {}, }; act(() => { schedule(request); }); await act(async () => { await vi.runAllTimersAsync(); }); act(() => { cancel(); }); await act(async () => { await vi.runAllTimersAsync(); }); expect(onComplete).toHaveBeenCalledWith([ expect.objectContaining({ status: 'cancelled', request, response: expect.objectContaining({ responseParts: expect.arrayContaining([ expect.objectContaining({ functionResponse: expect.objectContaining({ response: expect.objectContaining({ error: '[Operation Cancelled] Reason: User cancelled before execution', }), }), }), ]), }), }), ]); expect(mockTool.execute).not.toHaveBeenCalled(); expect(result.current[0]).toEqual([]); }); it.skip('should cancel tool calls that are awaiting approval', async () => { mockToolRegistry.getTool.mockReturnValue(mockToolRequiresConfirmation); const { result } = renderScheduler(); const schedule = result.current[1]; const cancelFn = result.current[2]; const request: ToolCallRequestInfo = { callId: 'cancelApprovalCall', name: 'mockToolRequiresConfirmation', args: {}, }; act(() => { schedule(request); }); await act(async () => { await vi.runAllTimersAsync(); }); act(() => { cancelFn(); }); await act(async () => { await vi.runAllTimersAsync(); }); expect(onComplete).toHaveBeenCalledWith([ expect.objectContaining({ status: 'cancelled', request, response: expect.objectContaining({ responseParts: expect.arrayContaining([ expect.objectContaining({ functionResponse: expect.objectContaining({ response: expect.objectContaining({ error: '[Operation Cancelled] Reason: User cancelled during approval', }), }), }), ]), }), }), ]); expect(result.current[0]).toEqual([]); }); it('should schedule and execute multiple tool calls', async () => { const tool1 = { ...mockTool, name: 'tool1', displayName: 'Tool 1', execute: vi.fn().mockResolvedValue({ llmContent: 'Output 1', returnDisplay: 'Display 1', } as ToolResult), shouldConfirmExecute: vi.fn().mockResolvedValue(null), }; const tool2 = { ...mockTool, name: 'tool2', displayName: 'Tool 2', execute: vi.fn().mockResolvedValue({ llmContent: 'Output 2', returnDisplay: 'Display 2', } as ToolResult), shouldConfirmExecute: vi.fn().mockResolvedValue(null), }; mockToolRegistry.getTool.mockImplementation((name) => { if (name === 'tool1') return tool1; if (name === 'tool2') return tool2; return undefined; }); const { result } = renderScheduler(); const schedule = result.current[1]; const requests: ToolCallRequestInfo[] = [ { callId: 'multi1', name: 'tool1', args: { p: 1 } }, { callId: 'multi2', name: 'tool2', args: { p: 2 } }, ]; act(() => { schedule(requests); }); await act(async () => { await vi.runAllTimersAsync(); }); await act(async () => { await vi.runAllTimersAsync(); }); await act(async () => { await vi.runAllTimersAsync(); }); await act(async () => { await vi.runAllTimersAsync(); }); expect(onComplete).toHaveBeenCalledTimes(1); const completedCalls = onComplete.mock.calls[0][0] as ToolCall[]; expect(completedCalls.length).toBe(2); const call1Result = completedCalls.find( (c) => c.request.callId === 'multi1', ); const call2Result = completedCalls.find( (c) => c.request.callId === 'multi2', ); expect(call1Result).toMatchObject({ status: 'success', request: requests[0], response: expect.objectContaining({ resultDisplay: 'Display 1', responseParts: expect.arrayContaining([ expect.objectContaining({ functionResponse: expect.objectContaining({ response: { output: 'Output 1' }, }), }), ]), }), }); expect(call2Result).toMatchObject({ status: 'success', request: requests[1], response: expect.objectContaining({ resultDisplay: 'Display 2', responseParts: expect.arrayContaining([ expect.objectContaining({ functionResponse: expect.objectContaining({ response: { output: 'Output 2' }, }), }), ]), }), }); expect(result.current[0]).toEqual([]); }); it.skip('should throw error if scheduling while already running', async () => { mockToolRegistry.getTool.mockReturnValue(mockTool); const longExecutePromise = new Promise((resolve) => setTimeout( () => resolve({ llmContent: 'done', returnDisplay: 'done display' }), 50, ), ); (mockTool.execute as Mock).mockReturnValue(longExecutePromise); (mockTool.shouldConfirmExecute as Mock).mockResolvedValue(null); const { result } = renderScheduler(); const schedule = result.current[1]; const request1: ToolCallRequestInfo = { callId: 'run1', name: 'mockTool', args: {}, }; const request2: ToolCallRequestInfo = { callId: 'run2', name: 'mockTool', args: {}, }; act(() => { schedule(request1); }); await act(async () => { await vi.runAllTimersAsync(); }); expect(() => schedule(request2)).toThrow( 'Cannot schedule tool calls while other tool calls are running', ); await act(async () => { await vi.advanceTimersByTimeAsync(50); await vi.runAllTimersAsync(); await act(async () => { await vi.runAllTimersAsync(); }); }); expect(onComplete).toHaveBeenCalledWith([ expect.objectContaining({ status: 'success', request: request1, response: expect.objectContaining({ resultDisplay: 'done display' }), }), ]); expect(result.current[0]).toEqual([]); }); }); describe('mapToDisplay', () => { const baseRequest: ToolCallRequestInfo = { callId: 'testCallId', name: 'testTool', args: { foo: 'bar' }, }; const baseTool: Tool = { name: 'testTool', displayName: 'Test Tool Display', description: 'Test Description', isOutputMarkdown: false, canUpdateOutput: false, schema: {}, validateToolParams: vi.fn(), execute: vi.fn(), shouldConfirmExecute: vi.fn(), getDescription: vi.fn((args) => `Desc: ${JSON.stringify(args)}`), }; const baseResponse: ToolCallResponseInfo = { callId: 'testCallId', responseParts: [ { functionResponse: { name: 'testTool', id: 'testCallId', response: { output: 'Test output' }, } as FunctionResponse, } as PartUnion, ], resultDisplay: 'Test display output', error: undefined, }; // Define a more specific type for extraProps for these tests // This helps ensure that tool and confirmationDetails are only accessed when they are expected to exist. type MapToDisplayExtraProps = | { tool?: Tool; liveOutput?: string; response?: ToolCallResponseInfo; confirmationDetails?: ToolCallConfirmationDetails; } | { tool: Tool; response?: ToolCallResponseInfo; confirmationDetails?: ToolCallConfirmationDetails; } | { response: ToolCallResponseInfo; tool?: undefined; confirmationDetails?: ToolCallConfirmationDetails; } | { confirmationDetails: ToolCallConfirmationDetails; tool?: Tool; response?: ToolCallResponseInfo; }; const testCases: Array<{ name: string; status: ToolCallStatusType; extraProps?: MapToDisplayExtraProps; expectedStatus: ToolCallStatus; expectedResultDisplay?: string; expectedName?: string; expectedDescription?: string; }> = [ { name: 'validating', status: 'validating', extraProps: { tool: baseTool }, expectedStatus: ToolCallStatus.Executing, expectedName: baseTool.displayName, expectedDescription: baseTool.getDescription(baseRequest.args), }, { name: 'awaiting_approval', status: 'awaiting_approval', extraProps: { tool: baseTool, confirmationDetails: { onConfirm: vi.fn(), type: 'edit', title: 'Test Tool Display', serverName: 'testTool', toolName: 'testTool', toolDisplayName: 'Test Tool Display', fileName: 'test.ts', fileDiff: 'Test diff', } as ToolCallConfirmationDetails, }, expectedStatus: ToolCallStatus.Confirming, expectedName: baseTool.displayName, expectedDescription: baseTool.getDescription(baseRequest.args), }, { name: 'scheduled', status: 'scheduled', extraProps: { tool: baseTool }, expectedStatus: ToolCallStatus.Pending, expectedName: baseTool.displayName, expectedDescription: baseTool.getDescription(baseRequest.args), }, { name: 'executing no live output', status: 'executing', extraProps: { tool: baseTool }, expectedStatus: ToolCallStatus.Executing, expectedName: baseTool.displayName, expectedDescription: baseTool.getDescription(baseRequest.args), }, { name: 'executing with live output', status: 'executing', extraProps: { tool: baseTool, liveOutput: 'Live test output' }, expectedStatus: ToolCallStatus.Executing, expectedResultDisplay: 'Live test output', expectedName: baseTool.displayName, expectedDescription: baseTool.getDescription(baseRequest.args), }, { name: 'success', status: 'success', extraProps: { tool: baseTool, response: baseResponse }, expectedStatus: ToolCallStatus.Success, expectedResultDisplay: baseResponse.resultDisplay as any, expectedName: baseTool.displayName, expectedDescription: baseTool.getDescription(baseRequest.args), }, { name: 'error tool not found', status: 'error', extraProps: { response: { ...baseResponse, error: new Error('Test error tool not found'), resultDisplay: 'Error display tool not found', }, }, expectedStatus: ToolCallStatus.Error, expectedResultDisplay: 'Error display tool not found', expectedName: baseRequest.name, expectedDescription: '', }, { name: 'error tool execution failed', status: 'error', extraProps: { tool: baseTool, response: { ...baseResponse, error: new Error('Tool execution failed'), resultDisplay: 'Execution failed display', }, }, expectedStatus: ToolCallStatus.Error, expectedResultDisplay: 'Execution failed display', expectedName: baseTool.displayName, // Changed from baseTool.name expectedDescription: '', }, { name: 'cancelled', status: 'cancelled', extraProps: { tool: baseTool, response: { ...baseResponse, resultDisplay: 'Cancelled display', }, }, expectedStatus: ToolCallStatus.Canceled, expectedResultDisplay: 'Cancelled display', expectedName: baseTool.displayName, expectedDescription: baseTool.getDescription(baseRequest.args), }, ]; testCases.forEach( ({ name: testName, status, extraProps, expectedStatus, expectedResultDisplay, expectedName, expectedDescription, }) => { it(`should map ToolCall with status '${status}' (${testName}) correctly`, () => { const toolCall: ToolCall = { request: baseRequest, status, ...(extraProps || {}), } as ToolCall; const display = mapToDisplay(toolCall); expect(display.type).toBe('tool_group'); expect(display.tools.length).toBe(1); const toolDisplay = display.tools[0]; expect(toolDisplay.callId).toBe(baseRequest.callId); expect(toolDisplay.status).toBe(expectedStatus); expect(toolDisplay.resultDisplay).toBe(expectedResultDisplay); expect(toolDisplay.name).toBe(expectedName); if (status === 'error' && !extraProps?.tool) { expect(toolDisplay.description).toBe(''); } else { expect(toolDisplay.description).toBe( expectedDescription ?? baseTool.getDescription(baseRequest.args), ); } expect(toolDisplay.renderOutputAsMarkdown).toBe( extraProps?.tool?.isOutputMarkdown ?? false, ); if (status === 'awaiting_approval') { expect(toolDisplay.confirmationDetails).toBe( extraProps!.confirmationDetails, ); } else { expect(toolDisplay.confirmationDetails).toBeUndefined(); } }); }, ); it('should map an array of ToolCalls correctly', () => { const toolCall1: ToolCall = { request: { ...baseRequest, callId: 'call1' }, status: 'success', tool: baseTool, response: { ...baseResponse, callId: 'call1' }, } as ToolCall; const toolCall2: ToolCall = { request: { ...baseRequest, callId: 'call2' }, status: 'executing', tool: { ...baseTool, isOutputMarkdown: true }, liveOutput: 'markdown output', } as ToolCall; const display = mapToDisplay([toolCall1, toolCall2]); expect(display.tools.length).toBe(2); expect(display.tools[0].callId).toBe('call1'); expect(display.tools[0].status).toBe(ToolCallStatus.Success); expect(display.tools[0].renderOutputAsMarkdown).toBe(false); expect(display.tools[1].callId).toBe('call2'); expect(display.tools[1].status).toBe(ToolCallStatus.Executing); expect(display.tools[1].resultDisplay).toBe('markdown output'); expect(display.tools[1].renderOutputAsMarkdown).toBe(true); }); });