mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-21 01:07:46 +00:00
# 🚀 Sync Gemini CLI v0.2.1 - Major Feature Update (#483)
This commit is contained in:
@@ -5,10 +5,19 @@
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { describe, it, expect, vi, beforeEach, Mock } from 'vitest';
|
||||
import {
|
||||
describe,
|
||||
it,
|
||||
expect,
|
||||
vi,
|
||||
beforeEach,
|
||||
Mock,
|
||||
MockInstance,
|
||||
} from 'vitest';
|
||||
import { renderHook, act, waitFor } from '@testing-library/react';
|
||||
import { useGeminiStream, mergePartListUnions } from './useGeminiStream.js';
|
||||
import { useKeypress } from './useKeypress.js';
|
||||
import * as atCommandProcessor from './atCommandProcessor.js';
|
||||
import {
|
||||
useReactToolScheduler,
|
||||
TrackedToolCall,
|
||||
@@ -20,8 +29,10 @@ import {
|
||||
Config,
|
||||
EditorType,
|
||||
AuthType,
|
||||
GeminiClient,
|
||||
GeminiEventType as ServerGeminiEventType,
|
||||
AnyToolInvocation,
|
||||
ToolErrorType,
|
||||
} from '@qwen-code/qwen-code-core';
|
||||
import { Part, PartListUnion } from '@google/genai';
|
||||
import { UseHistoryManagerReturn } from './useHistoryManager.js';
|
||||
@@ -83,11 +94,7 @@ vi.mock('./shellCommandProcessor.js', () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('./atCommandProcessor.js', () => ({
|
||||
handleAtCommand: vi
|
||||
.fn()
|
||||
.mockResolvedValue({ shouldProceed: true, processedQuery: 'mocked' }),
|
||||
}));
|
||||
vi.mock('./atCommandProcessor.js');
|
||||
|
||||
vi.mock('../utils/markdownUtilities.js', () => ({
|
||||
findLastSafeSplitPoint: vi.fn((s: string) => s.length),
|
||||
@@ -259,6 +266,7 @@ describe('useGeminiStream', () => {
|
||||
let mockScheduleToolCalls: Mock;
|
||||
let mockCancelAllToolCalls: Mock;
|
||||
let mockMarkToolsAsSubmitted: Mock;
|
||||
let handleAtCommandSpy: MockInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks(); // Clear mocks before each test
|
||||
@@ -342,6 +350,7 @@ describe('useGeminiStream', () => {
|
||||
mockSendMessageStream
|
||||
.mockClear()
|
||||
.mockReturnValue((async function* () {})());
|
||||
handleAtCommandSpy = vi.spyOn(atCommandProcessor, 'handleAtCommand');
|
||||
});
|
||||
|
||||
const mockLoadedSettings: LoadedSettings = {
|
||||
@@ -513,7 +522,11 @@ describe('useGeminiStream', () => {
|
||||
},
|
||||
status: 'success',
|
||||
responseSubmittedToGemini: false,
|
||||
response: { callId: 'call1', responseParts: toolCall1ResponseParts },
|
||||
response: {
|
||||
callId: 'call1',
|
||||
responseParts: toolCall1ResponseParts,
|
||||
errorType: undefined, // FIX: Added missing property
|
||||
},
|
||||
tool: {
|
||||
displayName: 'MockTool',
|
||||
},
|
||||
@@ -531,7 +544,11 @@ describe('useGeminiStream', () => {
|
||||
},
|
||||
status: 'error',
|
||||
responseSubmittedToGemini: false,
|
||||
response: { callId: 'call2', responseParts: toolCall2ResponseParts },
|
||||
response: {
|
||||
callId: 'call2',
|
||||
responseParts: toolCall2ResponseParts,
|
||||
errorType: ToolErrorType.UNHANDLED_EXCEPTION, // FIX: Added missing property
|
||||
},
|
||||
} as TrackedCompletedToolCall, // Treat error as a form of completion for submission
|
||||
];
|
||||
|
||||
@@ -598,7 +615,11 @@ describe('useGeminiStream', () => {
|
||||
prompt_id: 'prompt-id-3',
|
||||
},
|
||||
status: 'cancelled',
|
||||
response: { callId: '1', responseParts: [{ text: 'cancelled' }] },
|
||||
response: {
|
||||
callId: '1',
|
||||
responseParts: [{ text: 'cancelled' }],
|
||||
errorType: undefined, // FIX: Added missing property
|
||||
},
|
||||
responseSubmittedToGemini: false,
|
||||
tool: {
|
||||
displayName: 'mock tool',
|
||||
@@ -1902,173 +1923,76 @@ describe('useGeminiStream', () => {
|
||||
});
|
||||
|
||||
// Second call should work normally
|
||||
mockSendMessageStream.mockReturnValue(
|
||||
(async function* () {
|
||||
yield {
|
||||
type: ServerGeminiEventType.Content,
|
||||
value: 'Valid response',
|
||||
};
|
||||
yield { type: ServerGeminiEventType.Finished, value: 'STOP' };
|
||||
})(),
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await result.current.submitQuery('Valid query');
|
||||
});
|
||||
|
||||
// The second call should have been made
|
||||
expect(mockSendMessageStream).toHaveBeenCalledTimes(1);
|
||||
expect(mockSendMessageStream).toHaveBeenCalledWith(
|
||||
'Valid query',
|
||||
expect.any(AbortSignal),
|
||||
expect.any(String),
|
||||
);
|
||||
});
|
||||
|
||||
it('should reset execution flag when user cancels', async () => {
|
||||
let resolveCancelledStream!: () => void;
|
||||
const cancelledStreamPromise = new Promise<void>((resolve) => {
|
||||
resolveCancelledStream = resolve;
|
||||
});
|
||||
|
||||
// Mock a stream that can be cancelled
|
||||
const cancelledStream = (async function* () {
|
||||
yield {
|
||||
type: ServerGeminiEventType.Content,
|
||||
value: 'Cancelled content',
|
||||
};
|
||||
await cancelledStreamPromise;
|
||||
yield { type: ServerGeminiEventType.UserCancelled };
|
||||
})();
|
||||
|
||||
mockSendMessageStream.mockReturnValueOnce(cancelledStream);
|
||||
|
||||
const { result } = renderTestHook();
|
||||
|
||||
// Start first call
|
||||
const firstCallResult = act(async () => {
|
||||
await result.current.submitQuery('First query');
|
||||
});
|
||||
|
||||
// Wait a bit then resolve to trigger cancellation
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
resolveCancelledStream();
|
||||
await firstCallResult;
|
||||
|
||||
// Now try a second call - should work
|
||||
mockSendMessageStream.mockReturnValue(
|
||||
(async function* () {
|
||||
yield {
|
||||
type: ServerGeminiEventType.Content,
|
||||
value: 'Second response',
|
||||
};
|
||||
yield { type: ServerGeminiEventType.Finished, value: 'STOP' };
|
||||
})(),
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await result.current.submitQuery('Second query');
|
||||
});
|
||||
|
||||
// Both calls should have been made
|
||||
expect(mockSendMessageStream).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('should reset execution flag when an error occurs', async () => {
|
||||
// Mock a stream that throws an error
|
||||
mockSendMessageStream.mockReturnValueOnce(
|
||||
(async function* () {
|
||||
yield { type: ServerGeminiEventType.Content, value: 'Error content' };
|
||||
throw new Error('Stream error');
|
||||
})(),
|
||||
);
|
||||
|
||||
const { result } = renderTestHook();
|
||||
|
||||
// First call that will error
|
||||
await act(async () => {
|
||||
await result.current.submitQuery('Error query');
|
||||
});
|
||||
|
||||
// Second call should work normally
|
||||
mockSendMessageStream.mockReturnValue(
|
||||
(async function* () {
|
||||
yield {
|
||||
type: ServerGeminiEventType.Content,
|
||||
value: 'Success response',
|
||||
};
|
||||
yield { type: ServerGeminiEventType.Finished, value: 'STOP' };
|
||||
})(),
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await result.current.submitQuery('Success query');
|
||||
});
|
||||
|
||||
// Both calls should have been attempted
|
||||
expect(mockSendMessageStream).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('should handle rapid multiple concurrent calls correctly', async () => {
|
||||
let resolveStream!: () => void;
|
||||
const streamPromise = new Promise<void>((resolve) => {
|
||||
resolveStream = resolve;
|
||||
});
|
||||
|
||||
// Mock a long-running stream
|
||||
const longStream = (async function* () {
|
||||
yield {
|
||||
type: ServerGeminiEventType.Content,
|
||||
value: 'Long running content',
|
||||
};
|
||||
await streamPromise;
|
||||
yield { type: ServerGeminiEventType.Finished, value: 'STOP' };
|
||||
})();
|
||||
|
||||
mockSendMessageStream.mockReturnValue(longStream);
|
||||
|
||||
const { result } = renderTestHook();
|
||||
|
||||
// Start multiple concurrent calls
|
||||
const calls = [
|
||||
act(async () => {
|
||||
await result.current.submitQuery('Query 1');
|
||||
}),
|
||||
act(async () => {
|
||||
await result.current.submitQuery('Query 2');
|
||||
}),
|
||||
act(async () => {
|
||||
await result.current.submitQuery('Query 3');
|
||||
}),
|
||||
act(async () => {
|
||||
await result.current.submitQuery('Query 4');
|
||||
}),
|
||||
act(async () => {
|
||||
await result.current.submitQuery('Query 5');
|
||||
}),
|
||||
];
|
||||
|
||||
// Wait a bit then resolve the stream
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
resolveStream();
|
||||
|
||||
// Wait for all calls to complete
|
||||
await Promise.all(calls);
|
||||
|
||||
// Only the first call should have been made
|
||||
// Verify that only the second call was made (empty query is filtered out)
|
||||
expect(mockSendMessageStream).toHaveBeenCalledTimes(1);
|
||||
expect(mockSendMessageStream).toHaveBeenCalledWith(
|
||||
'Query 1',
|
||||
'Second query',
|
||||
expect.any(AbortSignal),
|
||||
expect.any(String),
|
||||
);
|
||||
|
||||
// Only one user message should have been added
|
||||
const userMessages = mockAddItem.mock.calls.filter(
|
||||
(call) => call[0].type === MessageType.USER,
|
||||
);
|
||||
expect(userMessages).toHaveLength(1);
|
||||
expect(userMessages[0][0].text).toBe('Query 1');
|
||||
});
|
||||
});
|
||||
|
||||
it('should process @include commands, adding user turn after processing to prevent race conditions', async () => {
|
||||
const rawQuery = '@include file.txt Summarize this.';
|
||||
const processedQueryParts = [
|
||||
{ text: 'Summarize this with content from @file.txt' },
|
||||
{ text: 'File content...' },
|
||||
];
|
||||
const userMessageTimestamp = Date.now();
|
||||
vi.spyOn(Date, 'now').mockReturnValue(userMessageTimestamp);
|
||||
|
||||
handleAtCommandSpy.mockResolvedValue({
|
||||
processedQuery: processedQueryParts,
|
||||
shouldProceed: true,
|
||||
});
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useGeminiStream(
|
||||
mockConfig.getGeminiClient() as GeminiClient,
|
||||
[],
|
||||
mockAddItem,
|
||||
mockConfig,
|
||||
mockOnDebugMessage,
|
||||
mockHandleSlashCommand,
|
||||
false,
|
||||
vi.fn(),
|
||||
vi.fn(),
|
||||
vi.fn(),
|
||||
false,
|
||||
vi.fn(),
|
||||
vi.fn(),
|
||||
vi.fn(),
|
||||
),
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await result.current.submitQuery(rawQuery);
|
||||
});
|
||||
|
||||
expect(handleAtCommandSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
query: rawQuery,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(mockAddItem).toHaveBeenCalledWith(
|
||||
{
|
||||
type: MessageType.USER,
|
||||
text: rawQuery,
|
||||
},
|
||||
userMessageTimestamp,
|
||||
);
|
||||
|
||||
// FIX: This expectation now correctly matches the actual function call signature.
|
||||
expect(mockSendMessageStream).toHaveBeenCalledWith(
|
||||
processedQueryParts, // Argument 1: The parts array directly
|
||||
expect.any(AbortSignal), // Argument 2: An AbortSignal
|
||||
expect.any(String), // Argument 3: The prompt_id string
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user