Files
gemini-cli/packages/cli/src/ui/hooks/toolMapping.test.ts
2026-03-07 23:17:10 +00:00

357 lines
11 KiB
TypeScript

/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { mapToDisplay } from './toolMapping.js';
import {
type AnyDeclarativeTool,
type AnyToolInvocation,
type ToolCallRequestInfo,
type ToolCallResponseInfo,
type Status,
type ToolCall,
type ScheduledToolCall,
type SuccessfulToolCall,
type ExecutingToolCall,
type WaitingToolCall,
type CancelledToolCall,
CoreToolCallStatus,
} from '@google/gemini-cli-core';
import { ToolCallStatus, mapCoreStatusToDisplayStatus } from '../types.js';
describe('toolMapping', () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe('mapCoreStatusToDisplayStatus', () => {
it.each([
[CoreToolCallStatus.Validating, ToolCallStatus.Pending],
[CoreToolCallStatus.AwaitingApproval, ToolCallStatus.Confirming],
[CoreToolCallStatus.Executing, ToolCallStatus.Executing],
[CoreToolCallStatus.Success, ToolCallStatus.Success],
[CoreToolCallStatus.Cancelled, ToolCallStatus.Canceled],
[CoreToolCallStatus.Error, ToolCallStatus.Error],
[CoreToolCallStatus.Scheduled, ToolCallStatus.Pending],
] as const)('maps %s to %s', (coreStatus, expectedDisplayStatus) => {
expect(mapCoreStatusToDisplayStatus(coreStatus)).toBe(
expectedDisplayStatus,
);
});
it('throws error for unknown status due to checkExhaustive', () => {
expect(() =>
mapCoreStatusToDisplayStatus('unknown_status' as Status),
).toThrow('unexpected value unknown_status!');
});
});
describe('mapToDisplay', () => {
const mockRequest: ToolCallRequestInfo = {
callId: 'call-1',
name: 'test_tool',
args: { arg1: 'val1' },
isClientInitiated: false,
prompt_id: 'p1',
};
const mockTool = {
name: 'test_tool',
displayName: 'Test Tool',
isOutputMarkdown: true,
} as unknown as AnyDeclarativeTool;
const mockInvocation = {
getDescription: () => 'Calling test_tool with args...',
} as unknown as AnyToolInvocation;
const mockResponse: ToolCallResponseInfo = {
callId: 'call-1',
responseParts: [],
resultDisplay: 'Success output',
error: undefined,
errorType: undefined,
};
it('handles a single tool call input', () => {
const toolCall: ScheduledToolCall = {
status: CoreToolCallStatus.Scheduled,
request: mockRequest,
tool: mockTool,
invocation: mockInvocation,
};
const result = mapToDisplay(toolCall);
expect(result.type).toBe('tool_group');
expect(result.tools).toHaveLength(1);
expect(result.tools[0]?.callId).toBe('call-1');
});
it('handles an array of tool calls', () => {
const toolCall1: ScheduledToolCall = {
status: CoreToolCallStatus.Scheduled,
request: mockRequest,
tool: mockTool,
invocation: mockInvocation,
};
const toolCall2: ScheduledToolCall = {
status: CoreToolCallStatus.Scheduled,
request: { ...mockRequest, callId: 'call-2' },
tool: mockTool,
invocation: mockInvocation,
};
const result = mapToDisplay([toolCall1, toolCall2]);
expect(result.tools).toHaveLength(2);
expect(result.tools[0]?.callId).toBe('call-1');
expect(result.tools[1]?.callId).toBe('call-2');
});
it('maps successful tool call properties correctly', () => {
const toolCall: SuccessfulToolCall = {
status: CoreToolCallStatus.Success,
request: mockRequest,
tool: mockTool,
invocation: mockInvocation,
response: {
...mockResponse,
outputFile: '/tmp/output.txt',
},
};
const result = mapToDisplay(toolCall);
const displayTool = result.tools[0];
expect(displayTool).toEqual(
expect.objectContaining({
callId: 'call-1',
name: 'Test Tool',
description: 'Calling test_tool with args...',
renderOutputAsMarkdown: true,
status: CoreToolCallStatus.Success,
resultDisplay: 'Success output',
outputFile: '/tmp/output.txt',
}),
);
});
it('maps executing tool call properties correctly with live output and ptyId', () => {
const toolCall: ExecutingToolCall = {
status: CoreToolCallStatus.Executing,
request: mockRequest,
tool: mockTool,
invocation: mockInvocation,
liveOutput: 'Loading...',
pid: 12345,
};
const result = mapToDisplay(toolCall);
const displayTool = result.tools[0];
expect(displayTool.status).toBe(CoreToolCallStatus.Executing);
expect(displayTool.resultDisplay).toBe('Loading...');
expect(displayTool.ptyId).toBe(12345);
});
it('maps awaiting_approval tool call properties with correlationId', () => {
const confirmationDetails = {
type: 'exec' as const,
title: 'Confirm Exec',
command: 'ls',
rootCommand: 'ls',
rootCommands: ['ls'],
onConfirm: vi.fn(),
};
const toolCall: WaitingToolCall = {
status: CoreToolCallStatus.AwaitingApproval,
request: mockRequest,
tool: mockTool,
invocation: mockInvocation,
confirmationDetails,
correlationId: 'corr-id-123',
};
const result = mapToDisplay(toolCall);
const displayTool = result.tools[0];
expect(displayTool.status).toBe(CoreToolCallStatus.AwaitingApproval);
expect(displayTool.confirmationDetails).toEqual(confirmationDetails);
});
it('maps correlationId and serializable confirmation details', () => {
const serializableDetails = {
type: 'edit' as const,
title: 'Confirm Edit',
fileName: 'file.txt',
filePath: '/path/file.txt',
fileDiff: 'diff',
originalContent: 'old',
newContent: 'new',
};
const toolCall: WaitingToolCall = {
status: CoreToolCallStatus.AwaitingApproval,
request: mockRequest,
tool: mockTool,
invocation: mockInvocation,
confirmationDetails: serializableDetails,
correlationId: 'corr-123',
};
const result = mapToDisplay(toolCall);
const displayTool = result.tools[0];
expect(displayTool.correlationId).toBe('corr-123');
expect(displayTool.confirmationDetails).toEqual(serializableDetails);
});
it('maps error tool call missing tool definition', () => {
// e.g. "TOOL_NOT_REGISTERED" errors
const toolCall: ToolCall = {
status: CoreToolCallStatus.Error,
request: mockRequest, // name: 'test_tool'
response: { ...mockResponse, resultDisplay: 'Tool not found' },
// notice: no `tool` or `invocation` defined here
};
const result = mapToDisplay(toolCall);
const displayTool = result.tools[0];
expect(displayTool.status).toBe(CoreToolCallStatus.Error);
expect(displayTool.name).toBe('test_tool'); // falls back to request.name
expect(displayTool.description).toBe('{"arg1":"val1"}'); // falls back to stringified args
expect(displayTool.resultDisplay).toBe('Tool not found');
expect(displayTool.renderOutputAsMarkdown).toBe(false);
});
it('maps cancelled tool call properties correctly', () => {
const toolCall: CancelledToolCall = {
status: CoreToolCallStatus.Cancelled,
request: mockRequest,
tool: mockTool,
invocation: mockInvocation,
response: {
...mockResponse,
resultDisplay: 'User cancelled', // Could be diff output for edits
},
};
const result = mapToDisplay(toolCall);
const displayTool = result.tools[0];
expect(displayTool.status).toBe(CoreToolCallStatus.Cancelled);
expect(displayTool.resultDisplay).toBe('User cancelled');
});
it('propagates borderTop and borderBottom options correctly', () => {
const toolCall: ScheduledToolCall = {
status: CoreToolCallStatus.Scheduled,
request: mockRequest,
tool: mockTool,
invocation: mockInvocation,
};
const result = mapToDisplay(toolCall, {
borderTop: true,
borderBottom: false,
});
expect(result.borderTop).toBe(true);
expect(result.borderBottom).toBe(false);
});
it('maps raw progress and progressTotal from Executing calls', () => {
const toolCall: ExecutingToolCall = {
status: CoreToolCallStatus.Executing,
request: mockRequest,
tool: mockTool,
invocation: mockInvocation,
progressMessage: 'Downloading...',
progress: 5,
progressTotal: 10,
};
const result = mapToDisplay(toolCall);
const displayTool = result.tools[0];
expect(displayTool.progress).toBe(5);
expect(displayTool.progressTotal).toBe(10);
expect(displayTool.progressMessage).toBe('Downloading...');
});
it('leaves progress fields undefined for non-Executing calls', () => {
const toolCall: SuccessfulToolCall = {
status: CoreToolCallStatus.Success,
request: mockRequest,
tool: mockTool,
invocation: mockInvocation,
response: mockResponse,
};
const result = mapToDisplay(toolCall);
const displayTool = result.tools[0];
expect(displayTool.progress).toBeUndefined();
expect(displayTool.progressTotal).toBeUndefined();
});
it('sets resultDisplay to undefined for pre-execution statuses', () => {
const toolCall: ScheduledToolCall = {
status: CoreToolCallStatus.Scheduled,
request: mockRequest,
tool: mockTool,
invocation: mockInvocation,
};
const result = mapToDisplay(toolCall);
expect(result.tools[0].resultDisplay).toBeUndefined();
expect(result.tools[0].status).toBe(CoreToolCallStatus.Scheduled);
});
it('propagates originalRequestName correctly', () => {
const toolCall: ScheduledToolCall = {
status: CoreToolCallStatus.Scheduled,
request: {
...mockRequest,
originalRequestName: 'original_tool',
},
tool: mockTool,
invocation: mockInvocation,
};
const result = mapToDisplay(toolCall);
expect(result.tools[0].originalRequestName).toBe('original_tool');
});
it('propagates isClientInitiated from tool request', () => {
const clientInitiatedTool: ScheduledToolCall = {
status: CoreToolCallStatus.Scheduled,
request: {
...mockRequest,
callId: 'call-client',
isClientInitiated: true,
},
tool: mockTool,
invocation: mockInvocation,
};
const modelInitiatedTool: ScheduledToolCall = {
status: CoreToolCallStatus.Scheduled,
request: {
...mockRequest,
callId: 'call-model',
isClientInitiated: false,
},
tool: mockTool,
invocation: mockInvocation,
};
const result = mapToDisplay([clientInitiatedTool, modelInitiatedTool]);
expect(result.tools[0].isClientInitiated).toBe(true);
expect(result.tools[1].isClientInitiated).toBe(false);
});
});
});