Merge tag 'v0.1.15' into feature/yiheng/sync-gemini-cli-0.1.15

This commit is contained in:
奕桁
2025-08-01 23:06:11 +08:00
340 changed files with 36528 additions and 22931 deletions

View File

@@ -16,7 +16,12 @@ import {
TrackedExecutingToolCall,
TrackedCancelledToolCall,
} from './useReactToolScheduler.js';
import { Config, EditorType, AuthType } from '@qwen-code/qwen-code-core';
import {
Config,
EditorType,
AuthType,
GeminiEventType as ServerGeminiEventType,
} from '@qwen-code/qwen-code-core';
import { Part, PartListUnion } from '@google/genai';
import { UseHistoryManagerReturn } from './useHistoryManager.js';
import {
@@ -1053,6 +1058,65 @@ describe('useGeminiStream', () => {
expect(mockSendMessageStream).not.toHaveBeenCalled(); // No LLM call made
});
});
it('should call Gemini with prompt content when slash command returns a `submit_prompt` action', async () => {
const customCommandResult: SlashCommandProcessorResult = {
type: 'submit_prompt',
content: 'This is the actual prompt from the command file.',
};
mockHandleSlashCommand.mockResolvedValue(customCommandResult);
const { result, mockSendMessageStream: localMockSendMessageStream } =
renderTestHook();
await act(async () => {
await result.current.submitQuery('/my-custom-command');
});
await waitFor(() => {
expect(mockHandleSlashCommand).toHaveBeenCalledWith(
'/my-custom-command',
);
expect(localMockSendMessageStream).not.toHaveBeenCalledWith(
'/my-custom-command',
expect.anything(),
expect.anything(),
);
expect(localMockSendMessageStream).toHaveBeenCalledWith(
'This is the actual prompt from the command file.',
expect.any(AbortSignal),
expect.any(String),
);
expect(mockScheduleToolCalls).not.toHaveBeenCalled();
});
});
it('should correctly handle a submit_prompt action with empty content', async () => {
const emptyPromptResult: SlashCommandProcessorResult = {
type: 'submit_prompt',
content: '',
};
mockHandleSlashCommand.mockResolvedValue(emptyPromptResult);
const { result, mockSendMessageStream: localMockSendMessageStream } =
renderTestHook();
await act(async () => {
await result.current.submitQuery('/emptycmd');
});
await waitFor(() => {
expect(mockHandleSlashCommand).toHaveBeenCalledWith('/emptycmd');
expect(localMockSendMessageStream).toHaveBeenCalledWith(
'',
expect.any(AbortSignal),
expect.any(String),
);
});
});
});
describe('Memory Refresh on save_memory', () => {
@@ -1178,4 +1242,235 @@ describe('useGeminiStream', () => {
});
});
});
describe('handleFinishedEvent', () => {
it('should add info message for MAX_TOKENS finish reason', async () => {
// Setup mock to return a stream with MAX_TOKENS finish reason
mockSendMessageStream.mockReturnValue(
(async function* () {
yield {
type: ServerGeminiEventType.Content,
value: 'This is a truncated response...',
};
yield { type: ServerGeminiEventType.Finished, value: 'MAX_TOKENS' };
})(),
);
const { result } = renderHook(() =>
useGeminiStream(
new MockedGeminiClientClass(mockConfig),
[],
mockAddItem,
mockSetShowHelp,
mockConfig,
mockOnDebugMessage,
mockHandleSlashCommand,
false,
() => 'vscode' as EditorType,
() => {},
() => Promise.resolve(),
false,
() => {},
),
);
// Submit a query
await act(async () => {
await result.current.submitQuery('Generate long text');
});
// Check that the info message was added
await waitFor(() => {
expect(mockAddItem).toHaveBeenCalledWith(
{
type: 'info',
text: '⚠️ Response truncated due to token limits.',
},
expect.any(Number),
);
});
});
it('should not add message for STOP finish reason', async () => {
// Setup mock to return a stream with STOP finish reason
mockSendMessageStream.mockReturnValue(
(async function* () {
yield {
type: ServerGeminiEventType.Content,
value: 'Complete response',
};
yield { type: ServerGeminiEventType.Finished, value: 'STOP' };
})(),
);
const { result } = renderHook(() =>
useGeminiStream(
new MockedGeminiClientClass(mockConfig),
[],
mockAddItem,
mockSetShowHelp,
mockConfig,
mockOnDebugMessage,
mockHandleSlashCommand,
false,
() => 'vscode' as EditorType,
() => {},
() => Promise.resolve(),
false,
() => {},
),
);
// Submit a query
await act(async () => {
await result.current.submitQuery('Test normal completion');
});
// Wait a bit to ensure no message is added
await new Promise((resolve) => setTimeout(resolve, 100));
// Check that no info message was added for STOP
const infoMessages = mockAddItem.mock.calls.filter(
(call) => call[0].type === 'info',
);
expect(infoMessages).toHaveLength(0);
});
it('should not add message for FINISH_REASON_UNSPECIFIED', async () => {
// Setup mock to return a stream with FINISH_REASON_UNSPECIFIED
mockSendMessageStream.mockReturnValue(
(async function* () {
yield {
type: ServerGeminiEventType.Content,
value: 'Response with unspecified finish',
};
yield {
type: ServerGeminiEventType.Finished,
value: 'FINISH_REASON_UNSPECIFIED',
};
})(),
);
const { result } = renderHook(() =>
useGeminiStream(
new MockedGeminiClientClass(mockConfig),
[],
mockAddItem,
mockSetShowHelp,
mockConfig,
mockOnDebugMessage,
mockHandleSlashCommand,
false,
() => 'vscode' as EditorType,
() => {},
() => Promise.resolve(),
false,
() => {},
),
);
// Submit a query
await act(async () => {
await result.current.submitQuery('Test unspecified finish');
});
// Wait a bit to ensure no message is added
await new Promise((resolve) => setTimeout(resolve, 100));
// Check that no info message was added
const infoMessages = mockAddItem.mock.calls.filter(
(call) => call[0].type === 'info',
);
expect(infoMessages).toHaveLength(0);
});
it('should add appropriate messages for other finish reasons', async () => {
const testCases = [
{
reason: 'SAFETY',
message: '⚠️ Response stopped due to safety reasons.',
},
{
reason: 'RECITATION',
message: '⚠️ Response stopped due to recitation policy.',
},
{
reason: 'LANGUAGE',
message: '⚠️ Response stopped due to unsupported language.',
},
{
reason: 'BLOCKLIST',
message: '⚠️ Response stopped due to forbidden terms.',
},
{
reason: 'PROHIBITED_CONTENT',
message: '⚠️ Response stopped due to prohibited content.',
},
{
reason: 'SPII',
message:
'⚠️ Response stopped due to sensitive personally identifiable information.',
},
{ reason: 'OTHER', message: '⚠️ Response stopped for other reasons.' },
{
reason: 'MALFORMED_FUNCTION_CALL',
message: '⚠️ Response stopped due to malformed function call.',
},
{
reason: 'IMAGE_SAFETY',
message: '⚠️ Response stopped due to image safety violations.',
},
{
reason: 'UNEXPECTED_TOOL_CALL',
message: '⚠️ Response stopped due to unexpected tool call.',
},
];
for (const { reason, message } of testCases) {
// Reset mocks for each test case
mockAddItem.mockClear();
mockSendMessageStream.mockReturnValue(
(async function* () {
yield {
type: ServerGeminiEventType.Content,
value: `Response for ${reason}`,
};
yield { type: ServerGeminiEventType.Finished, value: reason };
})(),
);
const { result } = renderHook(() =>
useGeminiStream(
new MockedGeminiClientClass(mockConfig),
[],
mockAddItem,
mockSetShowHelp,
mockConfig,
mockOnDebugMessage,
mockHandleSlashCommand,
false,
() => 'vscode' as EditorType,
() => {},
() => Promise.resolve(),
false,
() => {},
),
);
await act(async () => {
await result.current.submitQuery(`Test ${reason}`);
});
await waitFor(() => {
expect(mockAddItem).toHaveBeenCalledWith(
{
type: 'info',
text: message,
},
expect.any(Number),
);
});
}
});
});
});