316 lines
9.6 KiB
TypeScript
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');
|
|
});
|
|
});
|
|
}); |