directus-task-management/tests/services/nlp/entity-extractor.service.te...

316 lines
9.6 KiB
TypeScript

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<OpenAIService>;
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');
});
});
});