# 🚀 Sync Gemini CLI v0.2.1 - Major Feature Update (#483)

This commit is contained in:
tanzhenxin
2025-09-01 14:48:55 +08:00
committed by GitHub
parent 1610c1586e
commit 2572faf726
292 changed files with 19401 additions and 5941 deletions

View File

@@ -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
);
});
});