import { describe, it, expect, vi, beforeEach } from 'vitest'; const mockEnsureReady = vi.fn(async () => {}); const mockPrompt = vi.fn(async () => 'mock response'); const mockDispose = vi.fn(); vi.mock('../src/providers/acp-client.js', () => ({ AcpClient: vi.fn(function (this: Record) { this.ensureReady = mockEnsureReady; this.prompt = mockPrompt; this.dispose = mockDispose; }), })); // Must import after mock setup const { GeminiAcpProvider } = await import('../src/providers/gemini-acp.js'); describe('GeminiAcpProvider', () => { let provider: InstanceType; beforeEach(() => { vi.clearAllMocks(); mockPrompt.mockResolvedValue('mock response'); provider = new GeminiAcpProvider({ binaryPath: '/usr/bin/gemini', defaultModel: 'gemini-2.5-flash' }); }); describe('complete', () => { it('builds prompt from messages and returns CompletionResult', async () => { mockPrompt.mockResolvedValueOnce('The answer is 42.'); const result = await provider.complete({ messages: [ { role: 'system', content: 'You are helpful.' }, { role: 'user', content: 'What is the answer?' }, ], }); expect(result.content).toBe('The answer is 42.'); expect(result.toolCalls).toEqual([]); expect(result.finishReason).toBe('stop'); const promptText = mockPrompt.mock.calls[0][0] as string; expect(promptText).toContain('System: You are helpful.'); expect(promptText).toContain('What is the answer?'); }); it('formats assistant messages with prefix', async () => { mockPrompt.mockResolvedValueOnce('ok'); await provider.complete({ messages: [ { role: 'user', content: 'Hello' }, { role: 'assistant', content: 'Hi there' }, { role: 'user', content: 'How are you?' }, ], }); const promptText = mockPrompt.mock.calls[0][0] as string; expect(promptText).toContain('Assistant: Hi there'); }); it('trims response content', async () => { mockPrompt.mockResolvedValueOnce(' padded response \n'); const result = await provider.complete({ messages: [{ role: 'user', content: 'test' }], }); expect(result.content).toBe('padded response'); }); it('serializes concurrent calls', async () => { const callOrder: number[] = []; let callCount = 0; mockPrompt.mockImplementation(async () => { const myCall = ++callCount; callOrder.push(myCall); await new Promise((r) => setTimeout(r, 10)); return `response-${myCall}`; }); const [r1, r2, r3] = await Promise.all([ provider.complete({ messages: [{ role: 'user', content: 'a' }] }), provider.complete({ messages: [{ role: 'user', content: 'b' }] }), provider.complete({ messages: [{ role: 'user', content: 'c' }] }), ]); expect(r1.content).toBe('response-1'); expect(r2.content).toBe('response-2'); expect(r3.content).toBe('response-3'); expect(callOrder).toEqual([1, 2, 3]); }); it('continues queue after error', async () => { mockPrompt .mockRejectedValueOnce(new Error('first fails')) .mockResolvedValueOnce('second works'); const results = await Promise.allSettled([ provider.complete({ messages: [{ role: 'user', content: 'a' }] }), provider.complete({ messages: [{ role: 'user', content: 'b' }] }), ]); expect(results[0].status).toBe('rejected'); expect(results[1].status).toBe('fulfilled'); if (results[1].status === 'fulfilled') { expect(results[1].value.content).toBe('second works'); } }); }); describe('listModels', () => { it('returns static model list', async () => { const models = await provider.listModels(); expect(models).toContain('gemini-2.5-flash'); expect(models).toContain('gemini-2.5-pro'); expect(models).toContain('gemini-2.0-flash'); }); }); describe('dispose', () => { it('delegates to AcpClient', () => { provider.dispose(); expect(mockDispose).toHaveBeenCalled(); }); }); describe('name', () => { it('is gemini-cli for config compatibility', () => { expect(provider.name).toBe('gemini-cli'); }); }); });