Sync upstream Gemini-CLI v0.8.2 (#838)

This commit is contained in:
tanzhenxin 2025-10-23 09:27:04 +08:00 committed by GitHub
parent 096fabb5d6
commit eb95c131be
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
644 changed files with 70389 additions and 23709 deletions

View file

@ -22,18 +22,15 @@ import type {
ToolCallResponseInfo,
ToolCall, // Import from core
Status as ToolCallStatusType,
ToolInvocation,
AnyDeclarativeTool,
AnyToolInvocation,
} from '@qwen-code/qwen-code-core';
import {
ToolConfirmationOutcome,
DEFAULT_TRUNCATE_TOOL_OUTPUT_LINES,
DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD,
ApprovalMode,
Kind,
BaseDeclarativeTool,
BaseToolInvocation,
MockTool,
} from '@qwen-code/qwen-code-core';
import type { HistoryItemWithoutId, HistoryItemToolGroup } from '../types.js';
import { ToolCallStatus } from '../types.js';
// Mocks
@ -57,103 +54,45 @@ const mockConfig = {
getSessionId: () => 'test-session-id',
getUsageStatisticsEnabled: () => true,
getDebugMode: () => false,
storage: {
getProjectTempDir: () => '/tmp',
},
getTruncateToolOutputThreshold: () => DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD,
getTruncateToolOutputLines: () => DEFAULT_TRUNCATE_TOOL_OUTPUT_LINES,
getAllowedTools: vi.fn(() => []),
getContentGeneratorConfig: () => ({
model: 'test-model',
authType: 'oauth-personal',
}),
getUseSmartEdit: () => false,
getUseModelRouter: () => false,
getGeminiClient: () => null, // No client needed for these tests
getShellExecutionConfig: () => ({ terminalWidth: 80, terminalHeight: 24 }),
} as unknown as Config;
class MockToolInvocation extends BaseToolInvocation<object, ToolResult> {
constructor(
private readonly tool: MockTool,
params: object,
) {
super(params);
}
getDescription(): string {
return JSON.stringify(this.params);
}
override shouldConfirmExecute(
abortSignal: AbortSignal,
): Promise<ToolCallConfirmationDetails | false> {
return this.tool.shouldConfirmExecute(this.params, abortSignal);
}
execute(
signal: AbortSignal,
updateOutput?: (output: string) => void,
terminalColumns?: number,
terminalRows?: number,
): Promise<ToolResult> {
return this.tool.execute(
this.params,
signal,
updateOutput,
terminalColumns,
terminalRows,
);
}
}
class MockTool extends BaseDeclarativeTool<object, ToolResult> {
constructor(
name: string,
displayName: string,
canUpdateOutput = false,
shouldConfirm = false,
isOutputMarkdown = false,
) {
super(
name,
displayName,
'A mock tool for testing',
Kind.Other,
{},
isOutputMarkdown,
canUpdateOutput,
);
if (shouldConfirm) {
this.shouldConfirmExecute.mockImplementation(
async (): Promise<ToolCallConfirmationDetails | false> => ({
type: 'edit',
title: 'Mock Tool Requires Confirmation',
onConfirm: mockOnUserConfirmForToolConfirmation,
filePath: 'mock',
fileName: 'mockToolRequiresConfirmation.ts',
fileDiff: 'Mock tool requires confirmation',
originalContent: 'Original content',
newContent: 'New content',
}),
);
}
}
execute = vi.fn();
shouldConfirmExecute = vi.fn();
protected createInvocation(
params: object,
): ToolInvocation<object, ToolResult> {
return new MockToolInvocation(this, params);
}
}
const mockTool = new MockTool('mockTool', 'Mock Tool');
const mockToolWithLiveOutput = new MockTool(
'mockToolWithLiveOutput',
'Mock Tool With Live Output',
true,
);
const mockTool = new MockTool({
name: 'mockTool',
displayName: 'Mock Tool',
execute: vi.fn(),
shouldConfirmExecute: vi.fn(),
});
const mockToolWithLiveOutput = new MockTool({
name: 'mockToolWithLiveOutput',
displayName: 'Mock Tool With Live Output',
description: 'A mock tool for testing',
params: {},
isOutputMarkdown: true,
canUpdateOutput: true,
execute: vi.fn(),
shouldConfirmExecute: vi.fn(),
});
let mockOnUserConfirmForToolConfirmation: Mock;
const mockToolRequiresConfirmation = new MockTool(
'mockToolRequiresConfirmation',
'Mock Tool Requires Confirmation',
false,
true,
);
const mockToolRequiresConfirmation = new MockTool({
name: 'mockToolRequiresConfirmation',
displayName: 'Mock Tool Requires Confirmation',
execute: vi.fn(),
shouldConfirmExecute: vi.fn(),
});
describe('useReactToolScheduler in YOLO Mode', () => {
let onComplete: Mock;
@ -185,7 +124,6 @@ describe('useReactToolScheduler in YOLO Mode', () => {
onComplete,
mockConfig as unknown as Config,
setPendingHistoryItem,
() => undefined,
() => {},
),
);
@ -223,10 +161,6 @@ describe('useReactToolScheduler in YOLO Mode', () => {
// Check that execute WAS called
expect(mockToolRequiresConfirmation.execute).toHaveBeenCalledWith(
request.args,
expect.any(AbortSignal),
undefined,
undefined,
undefined,
);
// Check that onComplete was called with success
@ -268,36 +202,10 @@ describe('useReactToolScheduler', () => {
// to find a robust way to test these scenarios.
let onComplete: Mock;
let setPendingHistoryItem: Mock;
let capturedOnConfirmForTest:
| ((outcome: ToolConfirmationOutcome) => void | Promise<void>)
| 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<HistoryItemToolGroup> = {
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;
}
});
setPendingHistoryItem = vi.fn();
mockToolRegistry.getTool.mockClear();
(mockTool.execute as Mock).mockClear();
@ -335,7 +243,6 @@ describe('useReactToolScheduler', () => {
onComplete,
mockConfig as unknown as Config,
setPendingHistoryItem,
() => undefined,
() => {},
),
);
@ -374,13 +281,7 @@ describe('useReactToolScheduler', () => {
await vi.runAllTimersAsync();
});
expect(mockTool.execute).toHaveBeenCalledWith(
request.args,
expect.any(AbortSignal),
undefined,
undefined,
undefined,
);
expect(mockTool.execute).toHaveBeenCalledWith(request.args);
expect(onComplete).toHaveBeenCalledWith([
expect.objectContaining({
status: 'success',
@ -516,220 +417,24 @@ describe('useReactToolScheduler', () => {
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' },
} as any;
act(() => {
schedule(request, new AbortController().signal);
});
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: {},
} as any;
act(() => {
schedule(request, new AbortController().signal);
});
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<ToolResult>((resolve) => {
resolveExecutePromise = resolve;
});
(mockToolWithLiveOutput.execute as Mock).mockImplementation(
async (
_args: Record<string, unknown>,
_signal: AbortSignal,
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: {},
} as any;
act(() => {
schedule(request, new AbortController().signal);
});
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('should schedule and execute multiple tool calls', async () => {
const tool1 = new MockTool('tool1', 'Tool 1');
tool1.execute.mockResolvedValue({
llmContent: 'Output 1',
returnDisplay: 'Display 1',
} as ToolResult);
tool1.shouldConfirmExecute.mockResolvedValue(null);
const tool1 = new MockTool({
name: 'tool1',
displayName: 'Tool 1',
execute: vi.fn().mockResolvedValue({
llmContent: 'Output 1',
returnDisplay: 'Display 1',
} as ToolResult),
});
const tool2 = new MockTool('tool2', 'Tool 2');
tool2.execute.mockResolvedValue({
llmContent: 'Output 2',
returnDisplay: 'Display 2',
} as ToolResult);
tool2.shouldConfirmExecute.mockResolvedValue(null);
const tool2 = new MockTool({
name: 'tool2',
displayName: 'Tool 2',
execute: vi.fn().mockResolvedValue({
llmContent: 'Output 2',
returnDisplay: 'Display 2',
} as ToolResult),
});
mockToolRegistry.getTool.mockImplementation((name) => {
if (name === 'tool1') return tool1;
@ -805,62 +510,6 @@ describe('useReactToolScheduler', () => {
});
expect(result.current[0]).toEqual([]);
});
it.skip('should throw error if scheduling while already running', async () => {
mockToolRegistry.getTool.mockReturnValue(mockTool);
const longExecutePromise = new Promise<ToolResult>((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: {},
} as any;
const request2: ToolCallRequestInfo = {
callId: 'run2',
name: 'mockTool',
args: {},
} as any;
act(() => {
schedule(request1, new AbortController().signal);
});
await act(async () => {
await vi.runAllTimersAsync();
});
expect(() => schedule(request2, new AbortController().signal)).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', () => {
@ -870,7 +519,12 @@ describe('mapToDisplay', () => {
args: { foo: 'bar' },
} as any;
const baseTool = new MockTool('testTool', 'Test Tool Display');
const baseTool = new MockTool({
name: 'testTool',
displayName: 'Test Tool Display',
execute: vi.fn(),
shouldConfirmExecute: vi.fn(),
});
const baseResponse: ToolCallResponseInfo = {
callId: 'testCallId',
@ -1028,7 +682,7 @@ describe('mapToDisplay', () => {
expectedStatus: ToolCallStatus.Error,
expectedResultDisplay: 'Execution failed display',
expectedName: baseTool.displayName, // Changed from baseTool.name
expectedDescription: baseInvocation.getDescription(),
expectedDescription: JSON.stringify(baseRequest.args),
},
{
name: 'cancelled',
@ -1099,13 +753,13 @@ describe('mapToDisplay', () => {
invocation: baseTool.build(baseRequest.args),
response: { ...baseResponse, callId: 'call1' },
} as ToolCall;
const toolForCall2 = new MockTool(
baseTool.name,
baseTool.displayName,
false,
false,
true,
);
const toolForCall2 = new MockTool({
name: baseTool.name,
displayName: baseTool.displayName,
isOutputMarkdown: true,
execute: vi.fn(),
shouldConfirmExecute: vi.fn(),
});
const toolCall2: ToolCall = {
request: { ...baseRequest, callId: 'call2' },
status: 'executing',