refactor(core): adopt CoreToolCallStatus enum for type safety (#18998)

This commit is contained in:
Jerop Kipruto
2026-02-13 11:27:20 -05:00
committed by GitHub
parent d0c6a56c65
commit 60be42f095
22 changed files with 631 additions and 431 deletions

View File

@@ -18,6 +18,7 @@ import {
type ExecutingToolCall,
type WaitingToolCall,
type CancelledToolCall,
CoreToolCallStatus,
} from '@google/gemini-cli-core';
import { ToolCallStatus } from '../types.js';
@@ -28,13 +29,13 @@ describe('toolMapping', () => {
describe('mapCoreStatusToDisplayStatus', () => {
it.each([
['validating', ToolCallStatus.Pending],
['awaiting_approval', ToolCallStatus.Confirming],
['executing', ToolCallStatus.Executing],
['success', ToolCallStatus.Success],
['cancelled', ToolCallStatus.Canceled],
['error', ToolCallStatus.Error],
['scheduled', ToolCallStatus.Pending],
[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,
@@ -77,7 +78,7 @@ describe('toolMapping', () => {
it('handles a single tool call input', () => {
const toolCall: ScheduledToolCall = {
status: 'scheduled',
status: CoreToolCallStatus.Scheduled,
request: mockRequest,
tool: mockTool,
invocation: mockInvocation,
@@ -91,13 +92,13 @@ describe('toolMapping', () => {
it('handles an array of tool calls', () => {
const toolCall1: ScheduledToolCall = {
status: 'scheduled',
status: CoreToolCallStatus.Scheduled,
request: mockRequest,
tool: mockTool,
invocation: mockInvocation,
};
const toolCall2: ScheduledToolCall = {
status: 'scheduled',
status: CoreToolCallStatus.Scheduled,
request: { ...mockRequest, callId: 'call-2' },
tool: mockTool,
invocation: mockInvocation,
@@ -111,7 +112,7 @@ describe('toolMapping', () => {
it('maps successful tool call properties correctly', () => {
const toolCall: SuccessfulToolCall = {
status: 'success',
status: CoreToolCallStatus.Success,
request: mockRequest,
tool: mockTool,
invocation: mockInvocation,
@@ -139,7 +140,7 @@ describe('toolMapping', () => {
it('maps executing tool call properties correctly with live output and ptyId', () => {
const toolCall: ExecutingToolCall = {
status: 'executing',
status: CoreToolCallStatus.Executing,
request: mockRequest,
tool: mockTool,
invocation: mockInvocation,
@@ -166,7 +167,7 @@ describe('toolMapping', () => {
};
const toolCall: WaitingToolCall = {
status: 'awaiting_approval',
status: CoreToolCallStatus.AwaitingApproval,
request: mockRequest,
tool: mockTool,
invocation: mockInvocation,
@@ -193,7 +194,7 @@ describe('toolMapping', () => {
};
const toolCall: WaitingToolCall = {
status: 'awaiting_approval',
status: CoreToolCallStatus.AwaitingApproval,
request: mockRequest,
tool: mockTool,
invocation: mockInvocation,
@@ -211,7 +212,7 @@ describe('toolMapping', () => {
it('maps error tool call missing tool definition', () => {
// e.g. "TOOL_NOT_REGISTERED" errors
const toolCall: ToolCall = {
status: 'error',
status: CoreToolCallStatus.Error,
request: mockRequest, // name: 'test_tool'
response: { ...mockResponse, resultDisplay: 'Tool not found' },
// notice: no `tool` or `invocation` defined here
@@ -229,7 +230,7 @@ describe('toolMapping', () => {
it('maps cancelled tool call properties correctly', () => {
const toolCall: CancelledToolCall = {
status: 'cancelled',
status: CoreToolCallStatus.Cancelled,
request: mockRequest,
tool: mockTool,
invocation: mockInvocation,
@@ -248,7 +249,7 @@ describe('toolMapping', () => {
it('propagates borderTop and borderBottom options correctly', () => {
const toolCall: ScheduledToolCall = {
status: 'scheduled',
status: CoreToolCallStatus.Scheduled,
request: mockRequest,
tool: mockTool,
invocation: mockInvocation,
@@ -264,7 +265,7 @@ describe('toolMapping', () => {
it('sets resultDisplay to undefined for pre-execution statuses', () => {
const toolCall: ScheduledToolCall = {
status: 'scheduled',
status: CoreToolCallStatus.Scheduled,
request: mockRequest,
tool: mockTool,
invocation: mockInvocation,

View File

@@ -10,6 +10,8 @@ import {
type SerializableConfirmationDetails,
type ToolResultDisplay,
debugLogger,
CoreToolCallStatus,
checkExhaustive,
} from '@google/gemini-cli-core';
import {
ToolCallStatus,
@@ -17,25 +19,23 @@ import {
type IndividualToolCallDisplay,
} from '../types.js';
import { checkExhaustive } from '@google/gemini-cli-core';
export function mapCoreStatusToDisplayStatus(
coreStatus: CoreStatus,
): ToolCallStatus {
switch (coreStatus) {
case 'validating':
case CoreToolCallStatus.Validating:
return ToolCallStatus.Pending;
case 'awaiting_approval':
case CoreToolCallStatus.AwaitingApproval:
return ToolCallStatus.Confirming;
case 'executing':
case CoreToolCallStatus.Executing:
return ToolCallStatus.Executing;
case 'success':
case CoreToolCallStatus.Success:
return ToolCallStatus.Success;
case 'cancelled':
case CoreToolCallStatus.Cancelled:
return ToolCallStatus.Canceled;
case 'error':
case CoreToolCallStatus.Error:
return ToolCallStatus.Error;
case 'scheduled':
case CoreToolCallStatus.Scheduled:
return ToolCallStatus.Pending;
default:
return checkExhaustive(coreStatus);
@@ -60,7 +60,7 @@ export function mapToDisplay(
const displayName = call.tool?.displayName ?? call.request.name;
if (call.status === 'error') {
if (call.status === CoreToolCallStatus.Error) {
description = JSON.stringify(call.request.args);
} else {
description = call.invocation.getDescription();
@@ -82,25 +82,25 @@ export function mapToDisplay(
let correlationId: string | undefined = undefined;
switch (call.status) {
case 'success':
case CoreToolCallStatus.Success:
resultDisplay = call.response.resultDisplay;
outputFile = call.response.outputFile;
break;
case 'error':
case 'cancelled':
case CoreToolCallStatus.Error:
case CoreToolCallStatus.Cancelled:
resultDisplay = call.response.resultDisplay;
break;
case 'awaiting_approval':
case CoreToolCallStatus.AwaitingApproval:
correlationId = call.correlationId;
// Pass through details. Context handles dispatch (callback vs bus).
confirmationDetails = call.confirmationDetails;
break;
case 'executing':
case CoreToolCallStatus.Executing:
resultDisplay = call.liveOutput;
ptyId = call.pid;
break;
case 'scheduled':
case 'validating':
case CoreToolCallStatus.Scheduled:
case CoreToolCallStatus.Validating:
break;
default: {
const exhaustiveCheck: never = call;

View File

@@ -27,6 +27,7 @@ import type {
AnyToolInvocation,
} from '@google/gemini-cli-core';
import {
CoreToolCallStatus,
ApprovalMode,
AuthType,
GeminiEventType as ServerGeminiEventType,
@@ -343,14 +344,14 @@ describe('useGeminiStream', () => {
mockCancelAllToolCalls(...args);
lastToolCalls = lastToolCalls.map((tc) => {
if (
tc.status === 'awaiting_approval' ||
tc.status === 'executing' ||
tc.status === 'scheduled' ||
tc.status === 'validating'
tc.status === CoreToolCallStatus.AwaitingApproval ||
tc.status === CoreToolCallStatus.Executing ||
tc.status === CoreToolCallStatus.Scheduled ||
tc.status === CoreToolCallStatus.Validating
) {
return {
...tc,
status: 'cancelled',
status: CoreToolCallStatus.Cancelled,
response: {
callId: tc.request.callId,
responseParts: [],
@@ -406,7 +407,8 @@ describe('useGeminiStream', () => {
toolName: string,
callId: string,
confirmationType: 'edit' | 'info',
status: TrackedToolCall['status'] = 'awaiting_approval',
status: TrackedToolCall['status'] = CoreToolCallStatus.AwaitingApproval,
mockOnConfirm: Mock = vi.fn(),
): TrackedWaitingToolCall => ({
request: {
callId,
@@ -415,7 +417,7 @@ describe('useGeminiStream', () => {
isClientInitiated: false,
prompt_id: 'prompt-id-1',
},
status: status as 'awaiting_approval',
status: status as CoreToolCallStatus.AwaitingApproval,
responseSubmittedToGemini: false,
confirmationDetails:
confirmationType === 'edit'
@@ -427,11 +429,13 @@ describe('useGeminiStream', () => {
fileDiff: 'fake diff',
originalContent: 'old',
newContent: 'new',
onConfirm: mockOnConfirm,
}
: {
type: 'info',
title: `${toolName} confirmation`,
prompt: `Execute ${toolName}?`,
onConfirm: mockOnConfirm,
},
tool: {
name: toolName,
@@ -500,7 +504,7 @@ describe('useGeminiStream', () => {
isClientInitiated: false,
prompt_id: 'prompt-id-1',
},
status: 'success',
status: CoreToolCallStatus.Success,
responseSubmittedToGemini: false,
response: {
callId: 'call1',
@@ -528,7 +532,7 @@ describe('useGeminiStream', () => {
args: {},
prompt_id: 'prompt-id-1',
},
status: 'executing',
status: CoreToolCallStatus.Executing,
responseSubmittedToGemini: false,
tool: {
name: 'tool2',
@@ -566,7 +570,7 @@ describe('useGeminiStream', () => {
isClientInitiated: false,
prompt_id: 'prompt-id-2',
},
status: 'success',
status: CoreToolCallStatus.Success,
responseSubmittedToGemini: false,
response: {
callId: 'call1',
@@ -588,7 +592,7 @@ describe('useGeminiStream', () => {
isClientInitiated: false,
prompt_id: 'prompt-id-2',
},
status: 'error',
status: CoreToolCallStatus.Error,
responseSubmittedToGemini: false,
response: {
callId: 'call2',
@@ -675,10 +679,10 @@ describe('useGeminiStream', () => {
isClientInitiated: false,
prompt_id: 'prompt-id-3',
},
status: 'cancelled',
status: CoreToolCallStatus.Cancelled,
response: {
callId: '1',
responseParts: [{ text: 'cancelled' }],
responseParts: [{ text: CoreToolCallStatus.Cancelled }],
errorType: undefined, // FIX: Added missing property
},
responseSubmittedToGemini: false,
@@ -744,7 +748,7 @@ describe('useGeminiStream', () => {
expect(mockMarkToolsAsSubmitted).toHaveBeenCalledWith(['1']);
expect(client.addHistory).toHaveBeenCalledWith({
role: 'user',
parts: [{ text: 'cancelled' }],
parts: [{ text: CoreToolCallStatus.Cancelled }],
});
// Ensure we do NOT call back to the API
expect(mockSendMessageStream).not.toHaveBeenCalled();
@@ -761,7 +765,7 @@ describe('useGeminiStream', () => {
isClientInitiated: false,
prompt_id: 'prompt-id-stop',
},
status: 'error',
status: CoreToolCallStatus.Error,
response: {
callId: 'stop-call',
responseParts: [{ text: 'error occurred' }],
@@ -825,7 +829,7 @@ describe('useGeminiStream', () => {
invocation: {
getDescription: () => `Mock description`,
} as unknown as AnyToolInvocation,
status: 'cancelled',
status: CoreToolCallStatus.Cancelled,
response: {
callId: 'cancel-1',
responseParts: [
@@ -854,7 +858,7 @@ describe('useGeminiStream', () => {
invocation: {
getDescription: () => `Mock description`,
} as unknown as AnyToolInvocation,
status: 'cancelled',
status: CoreToolCallStatus.Cancelled,
response: {
callId: 'cancel-2',
responseParts: [
@@ -954,7 +958,7 @@ describe('useGeminiStream', () => {
isClientInitiated: false,
prompt_id: 'prompt-id-4',
},
status: 'executing',
status: CoreToolCallStatus.Executing,
responseSubmittedToGemini: false,
tool: {
name: 'tool1',
@@ -972,7 +976,7 @@ describe('useGeminiStream', () => {
const completedToolCalls: TrackedToolCall[] = [
{
...(initialToolCalls[0] as TrackedExecutingToolCall),
status: 'success',
status: CoreToolCallStatus.Success,
response: {
callId: 'call1',
responseParts: toolCallResponseParts,
@@ -1278,7 +1282,7 @@ describe('useGeminiStream', () => {
const toolCalls: TrackedToolCall[] = [
{
request: { callId: 'call1', name: 'tool1', args: {} },
status: 'executing',
status: CoreToolCallStatus.Executing,
responseSubmittedToGemini: false,
tool: {
name: 'tool1',
@@ -1318,7 +1322,7 @@ describe('useGeminiStream', () => {
isClientInitiated: false,
prompt_id: 'prompt-id-1',
},
status: 'awaiting_approval',
status: CoreToolCallStatus.AwaitingApproval,
responseSubmittedToGemini: false,
tool: {
name: 'some_tool',
@@ -1630,7 +1634,7 @@ describe('useGeminiStream', () => {
isClientInitiated: true,
prompt_id: 'prompt-id-6',
},
status: 'success',
status: CoreToolCallStatus.Success,
responseSubmittedToGemini: false,
response: {
callId: 'save-mem-call-1',
@@ -1875,7 +1879,7 @@ describe('useGeminiStream', () => {
isClientInitiated: false,
prompt_id: 'prompt-id-1',
},
status: 'awaiting_approval',
status: CoreToolCallStatus.AwaitingApproval,
responseSubmittedToGemini: false,
// No confirmationDetails
tool: {
@@ -1900,8 +1904,15 @@ describe('useGeminiStream', () => {
});
it('should only process tool calls with awaiting_approval status', async () => {
const mockOnConfirmAwaiting = vi.fn().mockResolvedValue(undefined);
const mixedStatusToolCalls: TrackedToolCall[] = [
createMockToolCall('replace', 'call1', 'edit'),
createMockToolCall(
'replace',
'call1',
'edit',
CoreToolCallStatus.AwaitingApproval,
mockOnConfirmAwaiting,
),
{
request: {
callId: 'call2',
@@ -1910,7 +1921,7 @@ describe('useGeminiStream', () => {
isClientInitiated: false,
prompt_id: 'prompt-id-1',
},
status: 'executing',
status: CoreToolCallStatus.Executing,
responseSubmittedToGemini: false,
tool: {
name: 'write_file',
@@ -2206,7 +2217,7 @@ describe('useGeminiStream', () => {
// were added to history during the await scheduleToolCalls(...) block.
const tools = requests.map((r: any) => ({
request: r,
status: 'success',
status: CoreToolCallStatus.Success,
tool: { displayName: r.name, name: r.name },
invocation: { getDescription: () => 'desc' },
response: { responseParts: [], resultDisplay: 'done' },
@@ -2681,7 +2692,7 @@ describe('useGeminiStream', () => {
const newToolCalls: TrackedToolCall[] = [
{
request: { callId: 'call1', name: 'tool1', args: {} },
status: 'executing',
status: CoreToolCallStatus.Executing,
tool: {
name: 'tool1',
displayName: 'tool1',
@@ -2809,7 +2820,7 @@ describe('useGeminiStream', () => {
await waitFor(() => {
expect(mockAddItem).toHaveBeenCalledWith(
expect.objectContaining({
type: 'error',
type: CoreToolCallStatus.Error,
}),
expect.any(Number),
);