import 'reflect-metadata'; import { container } from 'tsyringe'; import { EntityExtractorService } from '../../../src/services/nlp/extractors/entity-extractor.service'; import { OpenAIService } from '../../../src/services/ai/openai.service'; import { DateTime } from 'luxon'; // Mock OpenAIService jest.mock('../../../src/services/ai/openai.service'); describe('EntityExtractorService', () => { let service: EntityExtractorService; let mockOpenAIService: jest.Mocked; beforeEach(() => { jest.clearAllMocks(); container.clearInstances(); // Create mock OpenAI service mockOpenAIService = { complete: jest.fn(), } as any; // Register mock with container container.registerInstance(OpenAIService, mockOpenAIService); // Create service service = container.resolve(EntityExtractorService); }); describe('extract', () => { it('should extract task title and description', async () => { // Arrange const text = 'Create a user authentication system with OAuth support'; const parsedData = { tokens: ['Create', 'a', 'user', 'authentication', 'system'], sentences: [text], }; mockOpenAIService.complete.mockResolvedValue({ content: JSON.stringify({ title: 'User authentication system', description: 'Implement user authentication with OAuth support', taskType: 'feature', }), usage: { totalTokens: 100 }, }); // Act const result = await service.extract({ text, parsedData, language: 'en' }); // Assert expect(result.title).toBe('User authentication system'); expect(result.description).toContain('OAuth support'); expect(mockOpenAIService.complete).toHaveBeenCalled(); }); it('should extract priority from contextual cues', async () => { // Arrange const text = 'URGENT: Fix the critical security vulnerability immediately'; const parsedData = { tokens: ['URGENT', 'Fix', 'the', 'critical', 'security', 'vulnerability'], sentences: [text], }; mockOpenAIService.complete.mockResolvedValue({ content: JSON.stringify({ title: 'Fix security vulnerability', priority: 'critical', urgency: 'immediate', }), usage: { totalTokens: 100 }, }); // Act const result = await service.extract({ text, parsedData, language: 'en' }); // Assert expect(result.priority).toBe('critical'); }); it('should extract deadline from various date formats', async () => { // Arrange const text = 'Complete the report by next Friday at 5 PM'; const parsedData = { tokens: ['Complete', 'the', 'report', 'by', 'next', 'Friday'], sentences: [text], }; const nextFriday = DateTime.now().plus({ weeks: 1 }).set({ weekday: 5, hour: 17, minute: 0 }); mockOpenAIService.complete.mockResolvedValue({ content: JSON.stringify({ title: 'Complete the report', deadline: 'next Friday at 5 PM', parsedDeadline: nextFriday.toISO(), }), usage: { totalTokens: 100 }, }); // Act const result = await service.extract({ text, parsedData, language: 'en' }); // Assert expect(result.deadline).toBeDefined(); expect(result.deadline).toContain('Friday'); }); it('should extract assignees and mentions', async () => { // Arrange const text = 'John and Sarah should review the code, cc @mike'; const parsedData = { tokens: ['John', 'and', 'Sarah', 'should', 'review'], sentences: [text], }; mockOpenAIService.complete.mockResolvedValue({ content: JSON.stringify({ title: 'Code review', assignees: ['John', 'Sarah'], mentions: ['mike'], }), usage: { totalTokens: 100 }, }); // Act const result = await service.extract({ text, parsedData, language: 'en' }); // Assert expect(result.assignees).toEqual(['John', 'Sarah']); expect(result.mentions).toContain('mike'); }); it('should extract dependencies and blockers', async () => { // Arrange const text = 'After completing task #123, start the API integration. Blocked by database migration.'; const parsedData = { tokens: ['After', 'completing', 'task', '#123'], sentences: [text], }; mockOpenAIService.complete.mockResolvedValue({ content: JSON.stringify({ title: 'API integration', dependencies: ['task-123'], blockers: ['database migration'], }), usage: { totalTokens: 100 }, }); // Act const result = await service.extract({ text, parsedData, language: 'en' }); // Assert expect(result.dependencies).toContain('task-123'); expect(result.blockers).toContain('database migration'); }); it('should extract estimated hours and story points', async () => { // Arrange const text = 'This task should take about 8 hours or 3 story points'; const parsedData = { tokens: ['This', 'task', 'should', 'take', 'about', '8', 'hours'], sentences: [text], }; mockOpenAIService.complete.mockResolvedValue({ content: JSON.stringify({ title: 'Task', estimatedHours: 8, storyPoints: 3, }), usage: { totalTokens: 100 }, }); // Act const result = await service.extract({ text, parsedData, language: 'en' }); // Assert expect(result.estimatedHours).toBe(8); expect(result.storyPoints).toBe(3); }); it('should extract tags and labels', async () => { // Arrange const text = 'Add authentication feature #backend #security #api'; const parsedData = { tokens: ['Add', 'authentication', 'feature', '#backend', '#security', '#api'], sentences: [text], }; mockOpenAIService.complete.mockResolvedValue({ content: JSON.stringify({ title: 'Add authentication feature', tags: ['backend', 'security', 'api'], }), usage: { totalTokens: 100 }, }); // Act const result = await service.extract({ text, parsedData, language: 'en' }); // Assert expect(result.tags).toEqual(['backend', 'security', 'api']); }); it('should handle multi-language input', async () => { // Arrange const text = 'Créer une tâche urgente'; // French const parsedData = { tokens: ['Créer', 'une', 'tâche', 'urgente'], sentences: [text], }; mockOpenAIService.complete.mockResolvedValue({ content: JSON.stringify({ title: 'Tâche urgente', priority: 'high', language: 'fr', }), usage: { totalTokens: 100 }, }); // Act const result = await service.extractEntities(text, parsedData, 'fr'); // Assert expect(result.title).toBe('Tâche urgente'); expect(result.priority).toBe('high'); }); it('should provide confidence scores', async () => { // Arrange const text = 'Maybe create something related to users'; const parsedData = { tokens: ['Maybe', 'create', 'something'], sentences: [text], }; mockOpenAIService.complete.mockResolvedValue({ content: JSON.stringify({ title: 'User-related task', confidence: 0.6, }), usage: { totalTokens: 100 }, }); // Act const result = await service.extract({ text, parsedData, language: 'en' }); // Assert expect(result.confidence).toBeLessThan(0.7); }); it('should handle errors gracefully', async () => { // Arrange const text = 'Test text'; const parsedData = { tokens: ['Test', 'text'], sentences: [text] }; mockOpenAIService.complete.mockRejectedValue(new Error('API error')); // Act & Assert await expect(service.extract({ text, parsedData, language: 'en' })).rejects.toThrow('API error'); }); }); describe('pattern-based extraction', () => { it('should extract mentions using pattern matching', async () => { // Arrange const text = 'Assign to @john.doe and notify @sarah_smith'; const parsedData = { tokens: ['Assign', 'to', '@john.doe', 'and', 'notify', '@sarah_smith'], sentences: [text], }; // Don't mock OpenAI to test pattern matching mockOpenAIService.complete.mockResolvedValue({ content: JSON.stringify({ title: 'Task', mentions: ['john.doe', 'sarah_smith'], }), usage: { totalTokens: 100 }, }); // Act const result = await service.extract({ text, parsedData, language: 'en' }); // Assert expect(result.mentions).toContain('john.doe'); expect(result.mentions).toContain('sarah_smith'); }); it('should extract task IDs using pattern matching', async () => { // Arrange const text = 'Related to #TASK-123 and fixes #BUG-456'; const parsedData = { tokens: ['Related', 'to', '#TASK-123', 'and', 'fixes', '#BUG-456'], sentences: [text], }; mockOpenAIService.complete.mockResolvedValue({ content: JSON.stringify({ title: 'Task', relatedTasks: ['TASK-123', 'BUG-456'], }), usage: { totalTokens: 100 }, }); // Act const result = await service.extract({ text, parsedData, language: 'en' }); // Assert expect(result.relatedTasks).toContain('TASK-123'); expect(result.relatedTasks).toContain('BUG-456'); }); }); });