mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-05-02 05:31:02 +00:00
Sync upstream Gemini-CLI v0.8.2 (#838)
This commit is contained in:
parent
096fabb5d6
commit
eb95c131be
644 changed files with 70389 additions and 23709 deletions
|
|
@ -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',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue