import { describe, it, expect, jest, beforeEach } from '@jest/globals'; // Mock OpenAI BEFORE importing const mockCreate = jest.fn(); jest.mock('openai', () => { const MockOpenAI = jest.fn().mockImplementation(() => ({ chat: { completions: { create: mockCreate } }, })); return { __esModule: true, default: MockOpenAI }; }); jest.mock('../../src/config/config'); jest.mock('../../src/utils/logger'); import { duckVoteTool } from '../../src/tools/duck-vote'; import { ProviderManager } from '../../src/providers/manager'; import { ConfigManager } from '../../src/config/config'; describe('duckVoteTool structured JSON', () => { let mockProviderManager: ProviderManager; beforeEach(() => { jest.clearAllMocks(); const mockConfigManager = { getConfig: jest.fn().mockReturnValue({ providers: { openai: { api_key: 'key1', base_url: 'https://api.openai.com/v1', default_model: 'gpt-4', nickname: 'GPT-4', models: ['gpt-4'], }, google: { api_key: 'key2', base_url: 'https://api.gemini.com/v1', default_model: 'gemini-pro', nickname: 'Gemini', models: ['gemini-pro'], }, }, default_provider: 'openai', enable_failover: true, default_temperature: 0.7, }), } as unknown as jest.Mocked; mockProviderManager = new ProviderManager(mockConfigManager); const provider1 = mockProviderManager.getProvider('openai'); const provider2 = mockProviderManager.getProvider('google'); provider1['client'].chat.completions.create = mockCreate; provider2['client'].chat.completions.create = mockCreate; }); it('should return two content items: text and JSON', async () => { mockCreate .mockResolvedValueOnce({ choices: [{ message: { content: '{"choice": "A", "confidence": 85, "reasoning": "Solid"}' }, finish_reason: 'stop' }], usage: { prompt_tokens: 10, completion_tokens: 20, total_tokens: 30 }, model: 'gpt-4', }) .mockResolvedValueOnce({ choices: [{ message: { content: '{"choice": "A", "confidence": 75, "reasoning": "Good"}' }, finish_reason: 'stop' }], usage: { prompt_tokens: 10, completion_tokens: 20, total_tokens: 30 }, model: 'gemini-pro', }); const result = await duckVoteTool(mockProviderManager, { question: 'Best?', options: ['A', 'B'], }); expect(result.content).toHaveLength(2); expect(result.content[0].type).toBe('text'); expect(result.content[1].type).toBe('text'); }); it('should include vote data in JSON', async () => { mockCreate .mockResolvedValueOnce({ choices: [{ message: { content: '{"choice": "A", "confidence": 85, "reasoning": "Best"}' }, finish_reason: 'stop' }], usage: { prompt_tokens: 10, completion_tokens: 20, total_tokens: 30 }, model: 'gpt-4', }) .mockResolvedValueOnce({ choices: [{ message: { content: '{"choice": "A", "confidence": 75, "reasoning": "Good"}' }, finish_reason: 'stop' }], usage: { prompt_tokens: 10, completion_tokens: 20, total_tokens: 30 }, model: 'gemini-pro', }); const result = await duckVoteTool(mockProviderManager, { question: 'Best approach?', options: ['A', 'B'], }); const data = JSON.parse(result.content[1].text) as { question: string; options: string[]; winner: string | null; isTie: boolean; tally: Record; confidenceByOption: Record; votes: { voter: string; nickname: string; choice: string; confidence: number; reasoning: string }[]; totalVoters: number; validVotes: number; consensusLevel: string; }; expect(data.question).toBe('Best approach?'); expect(data.options).toEqual(['A', 'B']); expect(data.winner).toBe('A'); expect(data.consensusLevel).toBe('unanimous'); expect(data.tally['A']).toBe(2); expect(data.tally['B']).toBe(0); expect(data.votes).toHaveLength(2); expect(data.votes[0].confidence).toBe(85); }); it('should reflect tie in JSON data', async () => { mockCreate .mockResolvedValueOnce({ choices: [{ message: { content: '{"choice": "A", "confidence": 60}' }, finish_reason: 'stop' }], usage: { prompt_tokens: 10, completion_tokens: 20, total_tokens: 30 }, model: 'gpt-4', }) .mockResolvedValueOnce({ choices: [{ message: { content: '{"choice": "B", "confidence": 90}' }, finish_reason: 'stop' }], usage: { prompt_tokens: 10, completion_tokens: 20, total_tokens: 30 }, model: 'gemini-pro', }); const result = await duckVoteTool(mockProviderManager, { question: 'Which?', options: ['A', 'B'], }); const data = JSON.parse(result.content[1].text) as { isTie: boolean; winner: string | null; consensusLevel: string; }; expect(data.isTie).toBe(true); expect(data.consensusLevel).toBe('split'); // Tie-break by confidence: B has 90 vs A has 60 expect(data.winner).toBe('B'); }); it('should not include rawResponse in JSON votes', async () => { mockCreate.mockResolvedValueOnce({ choices: [{ message: { content: '{"choice": "A", "confidence": 80}' }, finish_reason: 'stop' }], usage: { prompt_tokens: 10, completion_tokens: 20, total_tokens: 30 }, model: 'gpt-4', }); const result = await duckVoteTool(mockProviderManager, { question: 'Test?', options: ['A', 'B'], voters: ['openai'], }); const data = JSON.parse(result.content[1].text) as { votes: Record[]; }; // rawResponse should not be in the structured output expect(data.votes[0]).not.toHaveProperty('rawResponse'); }); });