import { describe, it, expect, jest, beforeEach } from '@jest/globals'; import { chatDuckTool } from '../../src/tools/chat-duck.js'; import { ProviderManager } from '../../src/providers/manager.js'; import { ConversationManager } from '../../src/services/conversation.js'; // Mock dependencies jest.mock('../../src/utils/logger'); jest.mock('../../src/providers/manager.js'); jest.mock('../../src/services/conversation.js'); describe('chatDuckTool', () => { let mockProviderManager: jest.Mocked; let mockConversationManager: jest.Mocked; const mockResponse = { provider: 'openai', nickname: 'OpenAI Duck', content: 'This is a test response.', model: 'gpt-4', latency: 150, }; const mockConversation = { id: 'test-conv', provider: 'openai', messages: [], createdAt: new Date(), updatedAt: new Date(), }; beforeEach(() => { mockProviderManager = { askDuck: jest.fn().mockResolvedValue(mockResponse), getProviderNames: jest.fn().mockReturnValue(['openai', 'groq']), } as unknown as jest.Mocked; mockConversationManager = { getConversation: jest.fn().mockReturnValue(mockConversation), createConversation: jest.fn().mockReturnValue(mockConversation), switchProvider: jest.fn().mockReturnValue({ ...mockConversation, provider: 'groq' }), addMessage: jest.fn().mockReturnValue(mockConversation), getConversationContext: jest.fn().mockReturnValue([]), } as unknown as jest.Mocked; }); it('should throw error when conversation_id is missing', async () => { await expect( chatDuckTool(mockProviderManager, mockConversationManager, { message: 'Hello' }) ).rejects.toThrow('conversation_id and message are required'); }); it('should throw error when message is missing', async () => { await expect( chatDuckTool(mockProviderManager, mockConversationManager, { conversation_id: 'test' }) ).rejects.toThrow('conversation_id and message are required'); }); it('should create new conversation if not exists', async () => { mockConversationManager.getConversation.mockReturnValue(undefined); await chatDuckTool(mockProviderManager, mockConversationManager, { conversation_id: 'new-conv', message: 'Hello', }); expect(mockConversationManager.createConversation).toHaveBeenCalledWith('new-conv', 'openai'); }); it('should use existing conversation', async () => { await chatDuckTool(mockProviderManager, mockConversationManager, { conversation_id: 'test-conv', message: 'Hello', }); expect(mockConversationManager.createConversation).not.toHaveBeenCalled(); }); it('should switch provider when requested', async () => { await chatDuckTool(mockProviderManager, mockConversationManager, { conversation_id: 'test-conv', message: 'Hello', provider: 'groq', }); expect(mockConversationManager.switchProvider).toHaveBeenCalledWith('test-conv', 'groq'); }); it('should not switch provider if same as current', async () => { await chatDuckTool(mockProviderManager, mockConversationManager, { conversation_id: 'test-conv', message: 'Hello', provider: 'openai', // Same as mockConversation.provider }); expect(mockConversationManager.switchProvider).not.toHaveBeenCalled(); }); it('should add user message to conversation', async () => { await chatDuckTool(mockProviderManager, mockConversationManager, { conversation_id: 'test-conv', message: 'What is TypeScript?', }); expect(mockConversationManager.addMessage).toHaveBeenCalledWith( 'test-conv', expect.objectContaining({ role: 'user', content: 'What is TypeScript?', }) ); }); it('should add assistant response to conversation', async () => { await chatDuckTool(mockProviderManager, mockConversationManager, { conversation_id: 'test-conv', message: 'Hello', }); expect(mockConversationManager.addMessage).toHaveBeenCalledWith( 'test-conv', expect.objectContaining({ role: 'assistant', content: 'This is a test response.', }) ); }); it('should pass conversation context to provider', async () => { const contextMessages = [ { role: 'user' as const, content: 'Previous', timestamp: new Date() }, ]; mockConversationManager.getConversationContext.mockReturnValue(contextMessages); await chatDuckTool(mockProviderManager, mockConversationManager, { conversation_id: 'test-conv', message: 'Hello', }); expect(mockProviderManager.askDuck).toHaveBeenCalledWith( 'openai', '', expect.objectContaining({ messages: contextMessages, }) ); }); it('should pass model option to provider', async () => { await chatDuckTool(mockProviderManager, mockConversationManager, { conversation_id: 'test-conv', message: 'Hello', model: 'gpt-3.5-turbo', }); expect(mockProviderManager.askDuck).toHaveBeenCalledWith( 'openai', '', expect.objectContaining({ model: 'gpt-3.5-turbo', }) ); }); it('should format response with conversation info', async () => { mockConversationManager.getConversationContext.mockReturnValue([ { role: 'user' as const, content: 'Hello', timestamp: new Date() }, ]); const result = await chatDuckTool(mockProviderManager, mockConversationManager, { conversation_id: 'test-conv', message: 'Hello', }); expect(result.content[0].text).toContain('OpenAI Duck'); expect(result.content[0].text).toContain('test-conv'); expect(result.content[0].text).toContain('Messages: 2'); // context (1) + 1 new expect(result.content[0].text).toContain('150ms'); }); it('should use specified provider for new conversation', async () => { mockConversationManager.getConversation.mockReturnValue(undefined); await chatDuckTool(mockProviderManager, mockConversationManager, { conversation_id: 'new-conv', message: 'Hello', provider: 'groq', }); expect(mockConversationManager.createConversation).toHaveBeenCalledWith('new-conv', 'groq'); }); });