import { describe, it, expect, jest, beforeEach } from '@jest/globals'; import { compareDucksTool } from '../../src/tools/compare-ducks.js'; import { ProviderManager } from '../../src/providers/manager.js'; // Mock dependencies jest.mock('../../src/utils/logger'); jest.mock('../../src/providers/manager.js'); describe('compareDucksTool', () => { let mockProviderManager: jest.Mocked; const mockResponses = [ { provider: 'openai', nickname: 'OpenAI Duck', content: 'OpenAI says: TypeScript is great!', model: 'gpt-4', latency: 150, usage: { prompt_tokens: 10, completion_tokens: 20, total_tokens: 30, }, }, { provider: 'groq', nickname: 'Groq Duck', content: 'Groq says: TypeScript rocks!', model: 'llama-3.1-70b', latency: 80, usage: { prompt_tokens: 10, completion_tokens: 15, total_tokens: 25, }, }, ]; beforeEach(() => { mockProviderManager = { compareDucks: jest.fn().mockResolvedValue(mockResponses), } as unknown as jest.Mocked; }); it('should throw error when prompt is missing', async () => { await expect(compareDucksTool(mockProviderManager, {})).rejects.toThrow('Prompt is required'); }); it('should compare ducks with basic prompt', async () => { const result = await compareDucksTool(mockProviderManager, { prompt: 'What is TypeScript?', }); expect(mockProviderManager.compareDucks).toHaveBeenCalledWith( 'What is TypeScript?', undefined, { model: undefined } ); expect(result.content).toHaveLength(2); expect(result.content[0].type).toBe('text'); expect(result.content[0].text).toContain('Asked:'); expect(result.content[0].text).toContain('What is TypeScript?'); expect(result.content[1].type).toBe('text'); expect(() => JSON.parse(result.content[1].text)).not.toThrow(); }); it('should display all duck responses', async () => { const result = await compareDucksTool(mockProviderManager, { prompt: 'Test', }); expect(result.content[0].text).toContain('OpenAI Duck'); expect(result.content[0].text).toContain('Groq Duck'); expect(result.content[0].text).toContain('OpenAI says'); expect(result.content[0].text).toContain('Groq says'); }); it('should display model and token usage', async () => { const result = await compareDucksTool(mockProviderManager, { prompt: 'Test', }); expect(result.content[0].text).toContain('gpt-4'); expect(result.content[0].text).toContain('llama-3.1-70b'); expect(result.content[0].text).toContain('Tokens: 30'); expect(result.content[0].text).toContain('Tokens: 25'); }); it('should display latency info', async () => { const result = await compareDucksTool(mockProviderManager, { prompt: 'Test', }); expect(result.content[0].text).toContain('150ms'); expect(result.content[0].text).toContain('80ms'); }); it('should show success count in summary', async () => { const result = await compareDucksTool(mockProviderManager, { prompt: 'Test', }); expect(result.content[0].text).toContain('2/2 ducks responded successfully'); }); it('should handle error responses', async () => { mockProviderManager.compareDucks.mockResolvedValue([ mockResponses[0], { provider: 'groq', nickname: 'Groq Duck', content: 'Error: API key invalid', model: '', latency: 0, }, ]); const result = await compareDucksTool(mockProviderManager, { prompt: 'Test', }); expect(result.content[0].text).toContain('Error: API key invalid'); expect(result.content[0].text).toContain('1/2 ducks responded successfully'); }); it('should pass specific providers when provided', async () => { await compareDucksTool(mockProviderManager, { prompt: 'Test', providers: ['openai', 'groq'], }); expect(mockProviderManager.compareDucks).toHaveBeenCalledWith('Test', ['openai', 'groq'], { model: undefined, }); }); it('should pass model option', async () => { await compareDucksTool(mockProviderManager, { prompt: 'Test', model: 'gpt-3.5-turbo', }); expect(mockProviderManager.compareDucks).toHaveBeenCalledWith('Test', undefined, { model: 'gpt-3.5-turbo', }); }); it('should handle empty response list', async () => { mockProviderManager.compareDucks.mockResolvedValue([]); const result = await compareDucksTool(mockProviderManager, { prompt: 'Test', }); expect(result.content[0].text).toContain('0/0 ducks responded successfully'); }); it('should not show latency when zero', async () => { mockProviderManager.compareDucks.mockResolvedValue([ { ...mockResponses[0], latency: 0, }, ]); const result = await compareDucksTool(mockProviderManager, { prompt: 'Test', }); // When latency is 0, the ms indicator should not be shown for that response // (the code checks latency > 0) expect(result.content[0].text).not.toContain('0ms'); }); it('should use compareDucksWithProgress when progress is provided', async () => { mockProviderManager.compareDucksWithProgress = jest.fn().mockResolvedValue(mockResponses) as any; const mockProgress = { enabled: true, report: jest.fn<() => Promise>().mockResolvedValue(undefined), }; await compareDucksTool(mockProviderManager, { prompt: 'Test' }, mockProgress); expect(mockProviderManager.compareDucksWithProgress).toHaveBeenCalled(); expect(mockProviderManager.compareDucks).not.toHaveBeenCalled(); }); it('should use compareDucks when no progress is provided', async () => { await compareDucksTool(mockProviderManager, { prompt: 'Test' }); expect(mockProviderManager.compareDucks).toHaveBeenCalled(); }); describe('CLI provider support', () => { const mockCLIResponses = [ { provider: 'cli-claude', nickname: 'Claude Agent', content: 'CLI Claude response', model: 'cli', latency: 5000, usage: { prompt_tokens: 5, completion_tokens: 10, total_tokens: 15, }, }, { provider: 'cli-gemini', nickname: 'Gemini Agent', content: 'CLI Gemini response', model: 'cli', latency: 8000, }, ]; it('should handle CLI-only providers', async () => { mockProviderManager.compareDucks.mockResolvedValue(mockCLIResponses); const result = await compareDucksTool(mockProviderManager, { prompt: 'Test CLI', providers: ['cli-claude', 'cli-gemini'], }); expect(mockProviderManager.compareDucks).toHaveBeenCalledWith( 'Test CLI', ['cli-claude', 'cli-gemini'], { model: undefined } ); expect(result.content[0].text).toContain('Claude Agent'); expect(result.content[0].text).toContain('Gemini Agent'); expect(result.content[0].text).toContain('cli-claude'); expect(result.content[0].text).toContain('cli-gemini'); }); it('should display CLI responses with model as "cli"', async () => { mockProviderManager.compareDucks.mockResolvedValue(mockCLIResponses); const result = await compareDucksTool(mockProviderManager, { prompt: 'Test', providers: ['cli-claude'], }); expect(result.content[0].text).toContain('Model: cli'); }); it('should handle CLI response without usage info', async () => { mockProviderManager.compareDucks.mockResolvedValue([ { provider: 'cli-gemini', nickname: 'Gemini Agent', content: 'Response without usage', model: 'cli', latency: 8000, // no usage field }, ]); const result = await compareDucksTool(mockProviderManager, { prompt: 'Test', }); // Should not crash, should display response expect(result.content[0].text).toContain('Gemini Agent'); expect(result.content[0].text).toContain('Response without usage'); // Should not contain "Tokens:" since no usage expect(result.content[0].text).not.toMatch(/Tokens:\s*\|/); }); it('should handle mixed HTTP and CLI providers', async () => { mockProviderManager.compareDucks.mockResolvedValue([ { provider: 'openai', nickname: 'GPT Duck', content: 'HTTP response', model: 'gpt-4', latency: 500, usage: { prompt_tokens: 100, completion_tokens: 50, total_tokens: 150 }, }, { provider: 'cli-claude', nickname: 'Claude Agent', content: 'CLI response', model: 'cli', latency: 5000, usage: { prompt_tokens: 5, completion_tokens: 10, total_tokens: 15 }, }, ]); const result = await compareDucksTool(mockProviderManager, { prompt: 'Mixed test', providers: ['openai', 'cli-claude'], }); expect(result.content[0].text).toContain('GPT Duck'); expect(result.content[0].text).toContain('Claude Agent'); expect(result.content[0].text).toContain('gpt-4'); expect(result.content[0].text).toContain('Model: cli'); expect(result.content[0].text).toContain('2/2 ducks responded successfully'); }); it('should include CLI responses in structured JSON output', async () => { mockProviderManager.compareDucks.mockResolvedValue(mockCLIResponses); const result = await compareDucksTool(mockProviderManager, { prompt: 'Test', providers: ['cli-claude', 'cli-gemini'], }); const structuredData = JSON.parse(result.content[1].text); expect(structuredData).toHaveLength(2); expect(structuredData[0].provider).toBe('cli-claude'); expect(structuredData[0].model).toBe('cli'); expect(structuredData[1].provider).toBe('cli-gemini'); expect(structuredData[1].tokens).toBeNull(); // gemini has no usage }); }); });