mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-03 00:14:28 -07:00
feat(core, cli): Implement sequential approval. (#11593)
This commit is contained in:
@@ -37,7 +37,7 @@ import {
|
||||
} from '@google/gemini-cli-core';
|
||||
import type { Part, PartListUnion } from '@google/genai';
|
||||
import type { UseHistoryManagerReturn } from './useHistoryManager.js';
|
||||
import type { HistoryItem, SlashCommandProcessorResult } from '../types.js';
|
||||
import type { SlashCommandProcessorResult } from '../types.js';
|
||||
import { MessageType, StreamingState } from '../types.js';
|
||||
import type { LoadedSettings } from '../../config/settings.js';
|
||||
|
||||
@@ -231,8 +231,9 @@ describe('useGeminiStream', () => {
|
||||
mockUseReactToolScheduler.mockReturnValue([
|
||||
[], // Default to empty array for toolCalls
|
||||
mockScheduleToolCalls,
|
||||
mockCancelAllToolCalls,
|
||||
mockMarkToolsAsSubmitted,
|
||||
vi.fn(), // setToolCallsForDisplay
|
||||
mockCancelAllToolCalls,
|
||||
]);
|
||||
|
||||
// Reset mocks for GeminiClient instance methods (startChat and sendMessageStream)
|
||||
@@ -259,38 +260,71 @@ describe('useGeminiStream', () => {
|
||||
initialToolCalls: TrackedToolCall[] = [],
|
||||
geminiClient?: any,
|
||||
) => {
|
||||
let currentToolCalls = initialToolCalls;
|
||||
const setToolCalls = (newToolCalls: TrackedToolCall[]) => {
|
||||
currentToolCalls = newToolCalls;
|
||||
};
|
||||
|
||||
mockUseReactToolScheduler.mockImplementation(() => [
|
||||
currentToolCalls,
|
||||
mockScheduleToolCalls,
|
||||
mockCancelAllToolCalls,
|
||||
mockMarkToolsAsSubmitted,
|
||||
]);
|
||||
|
||||
const client = geminiClient || mockConfig.getGeminiClient();
|
||||
|
||||
const initialProps = {
|
||||
client,
|
||||
history: [],
|
||||
addItem: mockAddItem as unknown as UseHistoryManagerReturn['addItem'],
|
||||
config: mockConfig,
|
||||
onDebugMessage: mockOnDebugMessage,
|
||||
handleSlashCommand: mockHandleSlashCommand as unknown as (
|
||||
cmd: PartListUnion,
|
||||
) => Promise<SlashCommandProcessorResult | false>,
|
||||
shellModeActive: false,
|
||||
loadedSettings: mockLoadedSettings,
|
||||
toolCalls: initialToolCalls,
|
||||
};
|
||||
|
||||
const { result, rerender } = renderHook(
|
||||
(props: {
|
||||
client: any;
|
||||
history: HistoryItem[];
|
||||
addItem: UseHistoryManagerReturn['addItem'];
|
||||
config: Config;
|
||||
onDebugMessage: (message: string) => void;
|
||||
handleSlashCommand: (
|
||||
cmd: PartListUnion,
|
||||
) => Promise<SlashCommandProcessorResult | false>;
|
||||
shellModeActive: boolean;
|
||||
loadedSettings: LoadedSettings;
|
||||
toolCalls?: TrackedToolCall[]; // Allow passing updated toolCalls
|
||||
}) => {
|
||||
// Update the mock's return value if new toolCalls are passed in props
|
||||
if (props.toolCalls) {
|
||||
setToolCalls(props.toolCalls);
|
||||
}
|
||||
(props: typeof initialProps) => {
|
||||
// This mock needs to be stateful. When setToolCallsForDisplay is called,
|
||||
// it should trigger a rerender with the new state.
|
||||
const mockSetToolCallsForDisplay = vi.fn((updater) => {
|
||||
const newToolCalls =
|
||||
typeof updater === 'function' ? updater(props.toolCalls) : updater;
|
||||
rerender({ ...props, toolCalls: newToolCalls });
|
||||
});
|
||||
|
||||
// Create a stateful mock for cancellation that updates the toolCalls state.
|
||||
const statefulCancelAllToolCalls = vi.fn((...args) => {
|
||||
// Call the original spy so `toHaveBeenCalled` checks still work.
|
||||
mockCancelAllToolCalls(...args);
|
||||
|
||||
const newToolCalls = props.toolCalls.map((tc) => {
|
||||
// Only cancel tools that are in a cancellable state.
|
||||
if (
|
||||
tc.status === 'awaiting_approval' ||
|
||||
tc.status === 'executing' ||
|
||||
tc.status === 'scheduled' ||
|
||||
tc.status === 'validating'
|
||||
) {
|
||||
// A real cancelled tool call has a response object.
|
||||
// We need to simulate this to avoid type errors downstream.
|
||||
return {
|
||||
...tc,
|
||||
status: 'cancelled',
|
||||
response: {
|
||||
callId: tc.request.callId,
|
||||
responseParts: [],
|
||||
resultDisplay: 'Request cancelled.',
|
||||
},
|
||||
responseSubmittedToGemini: true, // Mark as "processed"
|
||||
} as any as TrackedCancelledToolCall;
|
||||
}
|
||||
return tc;
|
||||
});
|
||||
rerender({ ...props, toolCalls: newToolCalls });
|
||||
});
|
||||
|
||||
mockUseReactToolScheduler.mockImplementation(() => [
|
||||
props.toolCalls,
|
||||
mockScheduleToolCalls,
|
||||
mockMarkToolsAsSubmitted,
|
||||
mockSetToolCallsForDisplay,
|
||||
statefulCancelAllToolCalls, // Use the stateful mock
|
||||
]);
|
||||
|
||||
return useGeminiStream(
|
||||
props.client,
|
||||
props.history,
|
||||
@@ -313,19 +347,7 @@ describe('useGeminiStream', () => {
|
||||
);
|
||||
},
|
||||
{
|
||||
initialProps: {
|
||||
client,
|
||||
history: [],
|
||||
addItem: mockAddItem as unknown as UseHistoryManagerReturn['addItem'],
|
||||
config: mockConfig,
|
||||
onDebugMessage: mockOnDebugMessage,
|
||||
handleSlashCommand: mockHandleSlashCommand as unknown as (
|
||||
cmd: PartListUnion,
|
||||
) => Promise<SlashCommandProcessorResult | false>,
|
||||
shellModeActive: false,
|
||||
loadedSettings: mockLoadedSettings,
|
||||
toolCalls: initialToolCalls,
|
||||
},
|
||||
initialProps,
|
||||
},
|
||||
);
|
||||
return {
|
||||
@@ -452,7 +474,7 @@ describe('useGeminiStream', () => {
|
||||
|
||||
mockUseReactToolScheduler.mockImplementation((onComplete) => {
|
||||
capturedOnComplete = onComplete;
|
||||
return [[], mockScheduleToolCalls, mockMarkToolsAsSubmitted];
|
||||
return [[], mockScheduleToolCalls, mockMarkToolsAsSubmitted, vi.fn()];
|
||||
});
|
||||
|
||||
renderHook(() =>
|
||||
@@ -535,7 +557,7 @@ describe('useGeminiStream', () => {
|
||||
|
||||
mockUseReactToolScheduler.mockImplementation((onComplete) => {
|
||||
capturedOnComplete = onComplete;
|
||||
return [[], mockScheduleToolCalls, mockMarkToolsAsSubmitted];
|
||||
return [[], mockScheduleToolCalls, mockMarkToolsAsSubmitted, vi.fn()];
|
||||
});
|
||||
|
||||
renderHook(() =>
|
||||
@@ -647,7 +669,7 @@ describe('useGeminiStream', () => {
|
||||
|
||||
mockUseReactToolScheduler.mockImplementation((onComplete) => {
|
||||
capturedOnComplete = onComplete;
|
||||
return [[], mockScheduleToolCalls, mockMarkToolsAsSubmitted];
|
||||
return [[], mockScheduleToolCalls, mockMarkToolsAsSubmitted, vi.fn()];
|
||||
});
|
||||
|
||||
renderHook(() =>
|
||||
@@ -760,6 +782,7 @@ describe('useGeminiStream', () => {
|
||||
currentToolCalls,
|
||||
mockScheduleToolCalls,
|
||||
mockMarkToolsAsSubmitted,
|
||||
vi.fn(), // setToolCallsForDisplay
|
||||
];
|
||||
});
|
||||
|
||||
@@ -797,6 +820,7 @@ describe('useGeminiStream', () => {
|
||||
completedToolCalls,
|
||||
mockScheduleToolCalls,
|
||||
mockMarkToolsAsSubmitted,
|
||||
vi.fn(), // setToolCallsForDisplay
|
||||
];
|
||||
});
|
||||
|
||||
@@ -1031,7 +1055,7 @@ describe('useGeminiStream', () => {
|
||||
expect(result.current.streamingState).toBe(StreamingState.Idle);
|
||||
});
|
||||
|
||||
it('should not cancel if a tool call is in progress (not just responding)', async () => {
|
||||
it('should cancel if a tool call is in progress', async () => {
|
||||
const toolCalls: TrackedToolCall[] = [
|
||||
{
|
||||
request: { callId: 'call1', name: 'tool1', args: {} },
|
||||
@@ -1052,7 +1076,6 @@ describe('useGeminiStream', () => {
|
||||
} as TrackedExecutingToolCall,
|
||||
];
|
||||
|
||||
const abortSpy = vi.spyOn(AbortController.prototype, 'abort');
|
||||
const { result } = renderTestHook(toolCalls);
|
||||
|
||||
// State is `Responding` because a tool is running
|
||||
@@ -1061,8 +1084,71 @@ describe('useGeminiStream', () => {
|
||||
// Try to cancel
|
||||
simulateEscapeKeyPress();
|
||||
|
||||
// Nothing should happen because the state is not `Responding`
|
||||
expect(abortSpy).not.toHaveBeenCalled();
|
||||
// The cancel function should be called
|
||||
expect(mockCancelAllToolCalls).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should cancel a request when a tool is awaiting confirmation', async () => {
|
||||
const mockOnConfirm = vi.fn().mockResolvedValue(undefined);
|
||||
const toolCalls: TrackedToolCall[] = [
|
||||
{
|
||||
request: {
|
||||
callId: 'confirm-call',
|
||||
name: 'some_tool',
|
||||
args: {},
|
||||
isClientInitiated: false,
|
||||
prompt_id: 'prompt-id-1',
|
||||
},
|
||||
status: 'awaiting_approval',
|
||||
responseSubmittedToGemini: false,
|
||||
tool: {
|
||||
name: 'some_tool',
|
||||
description: 'a tool',
|
||||
build: vi.fn().mockImplementation((_) => ({
|
||||
getDescription: () => `Mock description`,
|
||||
})),
|
||||
} as any,
|
||||
invocation: {
|
||||
getDescription: () => `Mock description`,
|
||||
} as unknown as AnyToolInvocation,
|
||||
confirmationDetails: {
|
||||
type: 'edit',
|
||||
title: 'Confirm Edit',
|
||||
onConfirm: mockOnConfirm,
|
||||
fileName: 'file.txt',
|
||||
filePath: '/test/file.txt',
|
||||
fileDiff: 'fake diff',
|
||||
originalContent: 'old',
|
||||
newContent: 'new',
|
||||
},
|
||||
} as TrackedWaitingToolCall,
|
||||
];
|
||||
|
||||
const { result } = renderTestHook(toolCalls);
|
||||
|
||||
// State is `WaitingForConfirmation` because a tool is awaiting approval
|
||||
expect(result.current.streamingState).toBe(
|
||||
StreamingState.WaitingForConfirmation,
|
||||
);
|
||||
|
||||
// Try to cancel
|
||||
simulateEscapeKeyPress();
|
||||
|
||||
// The imperative cancel function should be called on the scheduler
|
||||
expect(mockCancelAllToolCalls).toHaveBeenCalled();
|
||||
|
||||
// A cancellation message should be added to history
|
||||
await waitFor(() => {
|
||||
expect(mockAddItem).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
text: 'Request cancelled.',
|
||||
}),
|
||||
expect.any(Number),
|
||||
);
|
||||
});
|
||||
|
||||
// The final state should be idle
|
||||
expect(result.current.streamingState).toBe(StreamingState.Idle);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1282,7 +1368,7 @@ describe('useGeminiStream', () => {
|
||||
|
||||
mockUseReactToolScheduler.mockImplementation((onComplete) => {
|
||||
capturedOnComplete = onComplete;
|
||||
return [[], mockScheduleToolCalls, mockMarkToolsAsSubmitted];
|
||||
return [[], mockScheduleToolCalls, mockMarkToolsAsSubmitted, vi.fn()];
|
||||
});
|
||||
|
||||
renderHook(() =>
|
||||
|
||||
Reference in New Issue
Block a user