135 lines
4.3 KiB
TypeScript
135 lines
4.3 KiB
TypeScript
|
|
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<string, unknown>) {
|
||
|
|
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<typeof GeminiAcpProvider>;
|
||
|
|
|
||
|
|
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');
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|