diff --git a/packages/cli/src/ui/commands/btwCommand.test.ts b/packages/cli/src/ui/commands/btwCommand.test.ts index af08d84e9..5d2b42572 100644 --- a/packages/cli/src/ui/commands/btwCommand.test.ts +++ b/packages/cli/src/ui/commands/btwCommand.test.ts @@ -52,6 +52,12 @@ describe('btwCommand', () => { beforeEach(() => { vi.clearAllMocks(); + mockGetCacheSafeParams.mockReturnValue({ + generationConfig: {}, + history: [], + model: 'test-model', + version: 1, + }); mockContext = createMockCommandContext({ services: { config: createConfig(), @@ -157,6 +163,107 @@ describe('btwCommand', () => { ); }); + it('should fall back to live Gemini client context when no saved cache params exist', async () => { + mockGetCacheSafeParams.mockReturnValue(null); + mockRunForkedAgent.mockResolvedValue({ + text: 'answer', + usage: { inputTokens: 5, outputTokens: 2, cacheHitTokens: 0 }, + }); + + const geminiClient = { + getHistory: vi + .fn() + .mockReturnValue([ + { role: 'user', parts: [{ text: '杭州天气如何?' }] }, + ]), + getChat: vi.fn().mockReturnValue({ + getGenerationConfig: vi.fn().mockReturnValue({ + systemInstruction: 'You are helpful', + tools: [], + }), + }), + }; + + const liveContext = createMockCommandContext({ + services: { + config: createConfig({ + getGeminiClient: () => geminiClient, + }), + }, + }); + + await btwCommand.action!(liveContext, 'how ?'); + await flushPromises(); + + expect(mockRunForkedAgent).toHaveBeenCalledWith( + expect.objectContaining({ + cacheSafeParams: expect.objectContaining({ + generationConfig: expect.objectContaining({ + systemInstruction: 'You are helpful', + }), + history: [{ role: 'user', parts: [{ text: '杭州天气如何?' }] }], + model: 'test-model', + }), + userMessage: expect.stringContaining('how ?'), + }), + ); + }); + + it('should prefer live Gemini client history over a stale saved cache snapshot', async () => { + mockGetCacheSafeParams.mockReturnValue({ + generationConfig: { + systemInstruction: 'stale system prompt', + tools: [], + }, + history: [{ role: 'user', parts: [{ text: '旧问题' }] }], + model: 'stale-model', + version: 99, + }); + mockRunForkedAgent.mockResolvedValue({ + text: 'answer', + usage: { inputTokens: 5, outputTokens: 2, cacheHitTokens: 0 }, + }); + + const geminiClient = { + getHistory: vi.fn().mockReturnValue([ + { role: 'user', parts: [{ text: '杭州天气如何?' }] }, + { role: 'user', parts: [{ text: '请顺便解释一下湿度怎么看' }] }, + ]), + getChat: vi.fn().mockReturnValue({ + getGenerationConfig: vi.fn().mockReturnValue({ + systemInstruction: 'live system prompt', + tools: [], + }), + }), + }; + + const liveContext = createMockCommandContext({ + services: { + config: createConfig({ + getGeminiClient: () => geminiClient, + }), + }, + }); + + await btwCommand.action!(liveContext, 'how ?'); + await flushPromises(); + + expect(mockRunForkedAgent).toHaveBeenCalledWith( + expect.objectContaining({ + cacheSafeParams: expect.objectContaining({ + generationConfig: expect.objectContaining({ + systemInstruction: 'live system prompt', + }), + history: [ + { role: 'user', parts: [{ text: '杭州天气如何?' }] }, + { role: 'user', parts: [{ text: '请顺便解释一下湿度怎么看' }] }, + ], + model: 'test-model', + }), + }), + ); + }); + it('should add error item on failure and clear btwItem', async () => { mockRunForkedAgent.mockRejectedValue(new Error('API error')); diff --git a/packages/cli/src/ui/commands/btwCommand.ts b/packages/cli/src/ui/commands/btwCommand.ts index 615182d86..8514a857a 100644 --- a/packages/cli/src/ui/commands/btwCommand.ts +++ b/packages/cli/src/ui/commands/btwCommand.ts @@ -47,6 +47,44 @@ function buildBtwPrompt(question: string): string { ].join('\n'); } +function getBtwCacheSafeParams( + context: CommandContext, +): ReturnType { + const geminiClient = context.services.config?.getGeminiClient(); + if ( + geminiClient && + typeof geminiClient === 'object' && + typeof geminiClient.getChat === 'function' && + typeof geminiClient.getHistory === 'function' + ) { + const chat = geminiClient.getChat(); + if ( + chat && + typeof chat === 'object' && + typeof chat.getGenerationConfig === 'function' + ) { + const generationConfig = chat.getGenerationConfig(); + if (generationConfig) { + const fullHistory = geminiClient.getHistory(true); + const maxHistoryEntries = 40; + const history = + fullHistory.length > maxHistoryEntries + ? fullHistory.slice(-maxHistoryEntries) + : fullHistory; + + return { + generationConfig, + history, + model: context.services.config?.getModel() ?? '', + version: 0, + }; + } + } + } + + return getCacheSafeParams(); +} + /** * Run a side question using runForkedAgent (cache path). * @@ -63,7 +101,7 @@ async function askBtw( const { config } = context.services; if (!config) throw new Error('Config not loaded'); - const cacheSafeParams = getCacheSafeParams(); + const cacheSafeParams = getBtwCacheSafeParams(context); if (!cacheSafeParams) throw new Error(t('No conversation context available for /btw'));