import { LangChainService, TaskBreakdownSchema } from '../../../src/services/ai/langchain.service'; import { AIConfig } from '../../../src/config/ai.config'; import { ChatOpenAI } from '@langchain/openai'; // Mock dependencies jest.mock('@langchain/openai'); jest.mock('langchain/memory'); describe('LangChainService', () => { let service: LangChainService; let mockConfig: AIConfig; let mockLLM: jest.Mocked; beforeEach(() => { jest.clearAllMocks(); mockConfig = { openai: { apiKey: 'test-api-key', model: 'gpt-4-turbo', maxTokens: 4096, temperature: 0.7, retryAttempts: 3, retryDelayMs: 100, timeout: 30000, }, langchain: { verbose: false, cacheEnabled: true, cacheTTL: 3600, maxConcurrency: 5, memoryBufferSize: 10, }, rateLimit: { maxRequestsPerMinute: 60, maxTokensPerMinute: 90000, maxRequestsPerDay: 10000, }, monitoring: { trackTokenUsage: true, logLevel: 'info', metricsEnabled: true, }, }; service = new LangChainService(mockConfig); mockLLM = (ChatOpenAI as jest.MockedClass).mock.instances[0] as jest.Mocked; }); describe('createCompletionChain', () => { it('should create a basic completion chain', () => { const systemPrompt = 'You are a helpful assistant'; const chain = service.createCompletionChain(systemPrompt); expect(chain).toBeDefined(); expect(chain.invoke).toBeDefined(); }); it('should create a chain with custom options', () => { const systemPrompt = 'You are a code reviewer'; const options = { temperature: 0.3, maxTokens: 2000, }; const chain = service.createCompletionChain(systemPrompt, options); expect(chain).toBeDefined(); }); }); describe('createConversationChain', () => { it('should create a conversation chain with memory', () => { const systemPrompt = 'You are a project assistant'; const sessionId = 'test-session-123'; const chain = service.createConversationChain(systemPrompt, sessionId); expect(chain).toBeDefined(); expect(chain.invoke).toBeDefined(); }); it('should reuse memory for same session', () => { const systemPrompt = 'You are a helper'; const sessionId = 'same-session'; const chain1 = service.createConversationChain(systemPrompt, sessionId); const chain2 = service.createConversationChain(systemPrompt, sessionId); // Both should use the same memory instance expect(chain1).toBeDefined(); expect(chain2).toBeDefined(); }); }); describe('createTaskBreakdownAgent', () => { it('should create a task breakdown agent', () => { const agent = service.createTaskBreakdownAgent(); expect(agent).toBeDefined(); expect(agent.invoke).toBeDefined(); }); it('should create agent with custom options', () => { const options = { temperature: 0.2, maxTokens: 3000, }; const agent = service.createTaskBreakdownAgent(options); expect(agent).toBeDefined(); }); }); describe('breakdownTask', () => { it('should breakdown a task successfully', async () => { const mockBreakdown = { title: 'Implement User Authentication', description: 'Create a secure authentication system', subtasks: [ { id: '1', title: 'Setup database schema', description: 'Create user tables', estimatedHours: 2, dependencies: [], }, { id: '2', title: 'Implement JWT tokens', description: 'Create JWT generation and validation', estimatedHours: 3, dependencies: ['1'], }, ], estimatedTotalHours: 5, complexity: 'major' as const, suggestedApproach: 'Use JWT with refresh tokens', }; // Mock the chain invoke method const mockChain = { invoke: jest.fn().mockResolvedValue(mockBreakdown), }; jest.spyOn(service, 'createTaskBreakdownAgent').mockReturnValue(mockChain as any); const result = await service.breakdownTask( 'Implement user authentication', 'Web application context', ); expect(result).toEqual(mockBreakdown); expect(mockChain.invoke).toHaveBeenCalledWith({ taskDescription: 'Implement user authentication', context: 'Web application context', }); }); it('should handle breakdown errors', async () => { const mockChain = { invoke: jest.fn().mockRejectedValue(new Error('AI error')), }; jest.spyOn(service, 'createTaskBreakdownAgent').mockReturnValue(mockChain as any); await expect(service.breakdownTask('Test task')).rejects.toThrow('Failed to breakdown task'); }); it('should validate breakdown schema', async () => { const invalidBreakdown = { title: 'Test Task', // Missing required fields }; const mockChain = { invoke: jest.fn().mockResolvedValue(invalidBreakdown), }; jest.spyOn(service, 'createTaskBreakdownAgent').mockReturnValue(mockChain as any); await expect(service.breakdownTask('Test task')).rejects.toThrow(); }); }); describe('createTaskFromNaturalLanguage', () => { it('should create task from natural language', async () => { const mockTask = { title: 'Fix login bug', description: 'Users cannot login with email', priority: 'high', complexity: 'minor', acceptanceCriteria: ['Login works with email', 'Error messages shown'], tags: ['bug', 'auth'], estimatedHours: 2, }; const mockChain = { invoke: jest.fn().mockResolvedValue(mockTask), }; jest.spyOn(service, 'createTaskCreationChain').mockReturnValue(mockChain as any); const result = await service.createTaskFromNaturalLanguage( 'Fix the bug where users cant login with their email', 'Authentication module', ); expect(result).toMatchObject(mockTask); expect(result.executionTimeMs).toBeGreaterThanOrEqual(0); }); }); describe('getTaskSuggestions', () => { it('should generate task suggestions', async () => { const mockSuggestions = ` 1. Implement user profile page - Users need to view and edit profiles 2. Add password reset - Security requirement for production 3. Setup email notifications - Keep users informed `; const mockChain = { invoke: jest.fn().mockResolvedValue(mockSuggestions), }; jest.spyOn(service, 'createTaskSuggestionChain').mockReturnValue(mockChain as any); const result = await service.getTaskSuggestions({ projectDescription: 'User management system', completedTasks: ['User registration', 'Login'], currentTasks: ['Dashboard'], goals: ['Complete MVP', 'Launch beta'], }); expect(result).toContain('user profile'); expect(result).toContain('password reset'); }); }); describe('memory management', () => { it('should add messages to memory', async () => { const sessionId = 'test-session'; // Mock the memory chatHistory const mockMemory = { chatHistory: { addMessage: jest.fn(), getMessages: jest.fn().mockResolvedValue([ { _getType: () => 'human', content: 'Hello' }, { _getType: () => 'ai', content: 'Hi there!' }, ]), }, clear: jest.fn(), }; // Store mock in service's memory map for getMemorySummary to work (service as any).memories.set(sessionId, mockMemory); await service.addToMemory(sessionId, 'Hello', 'human'); await service.addToMemory(sessionId, 'Hi there!', 'ai'); expect(mockMemory.chatHistory.addMessage).toHaveBeenCalledTimes(2); const summary = await service.getMemorySummary(sessionId); expect(summary).toContain('Hello'); expect(summary).toContain('Hi there!'); }); it('should clear memory for session', async () => { const sessionId = 'test-clear'; const mockMemory = { chatHistory: { addMessage: jest.fn(), getMessages: jest.fn().mockResolvedValue([]), }, clear: jest.fn(), }; jest.spyOn(service as any, 'getOrCreateMemory').mockReturnValue(mockMemory); await service.addToMemory(sessionId, 'Test message', 'human'); service.clearMemory(sessionId); const summary = await service.getMemorySummary(sessionId); expect(summary).toBeNull(); }); it('should clear all memories', async () => { const mockMemory1 = { chatHistory: { addMessage: jest.fn(), getMessages: jest.fn().mockResolvedValue([]), }, clear: jest.fn(), }; const mockMemory2 = { chatHistory: { addMessage: jest.fn(), getMessages: jest.fn().mockResolvedValue([]), }, clear: jest.fn(), }; jest.spyOn(service as any, 'getOrCreateMemory') .mockReturnValueOnce(mockMemory1) .mockReturnValueOnce(mockMemory2); await service.addToMemory('session1', 'Message 1', 'human'); await service.addToMemory('session2', 'Message 2', 'human'); service.clearAllMemories(); const summary1 = await service.getMemorySummary('session1'); const summary2 = await service.getMemorySummary('session2'); expect(summary1).toBeNull(); expect(summary2).toBeNull(); }); }); describe('streaming', () => { it('should handle streaming responses', async () => { const mockChain = { stream: jest.fn().mockImplementation(async function* () { yield 'Hello'; yield ' '; yield 'world'; }), }; const chunks: string[] = []; const result = await service.streamChain( mockChain as any, { input: 'test' }, (chunk) => chunks.push(chunk), ); expect(result).toBe('Hello world'); expect(chunks).toEqual(['Hello', ' ', 'world']); }); it('should handle streaming with objects', async () => { const mockChain = { stream: jest.fn().mockImplementation(async function* () { yield { content: 'Part 1' }; yield { content: ' Part 2' }; }), }; const chunks: string[] = []; const result = await service.streamChain( mockChain as any, { input: 'test' }, (chunk) => chunks.push(chunk), ); expect(result).toBe('Part 1 Part 2'); expect(chunks).toEqual(['Part 1', ' Part 2']); }); }); describe('testConnection', () => { it('should return true when connection succeeds', async () => { const mockChain = { invoke: jest.fn().mockResolvedValue('OK, I can hear you!'), }; jest.spyOn(service, 'createCompletionChain').mockReturnValue(mockChain as any); const result = await service.testConnection(); expect(result).toBe(true); }); it('should return false when connection fails', async () => { const mockChain = { invoke: jest.fn().mockRejectedValue(new Error('Connection failed')), }; jest.spyOn(service, 'createCompletionChain').mockReturnValue(mockChain as any); const result = await service.testConnection(); expect(result).toBe(false); }); }); describe('TaskBreakdownSchema validation', () => { it('should validate correct task breakdown', () => { const validBreakdown = { title: 'Main Task', description: 'Task description', subtasks: [ { id: '1', title: 'Subtask 1', description: 'Subtask description', estimatedHours: 2, dependencies: [], }, ], estimatedTotalHours: 2, complexity: 'minor' as const, suggestedApproach: 'Step by step', }; const result = TaskBreakdownSchema.parse(validBreakdown); expect(result).toEqual(validBreakdown); }); it('should reject invalid complexity', () => { const invalidBreakdown = { title: 'Task', description: 'Description', subtasks: [], complexity: 'invalid', }; expect(() => TaskBreakdownSchema.parse(invalidBreakdown)).toThrow(); }); it('should allow optional fields', () => { const minimalBreakdown = { title: 'Task', description: 'Description', subtasks: [], complexity: 'trivial' as const, }; const result = TaskBreakdownSchema.parse(minimalBreakdown); expect(result).toEqual(minimalBreakdown); }); }); });