directus-task-management/tests/api/ai.integration.test.ts

536 lines
15 KiB
TypeScript

import 'reflect-metadata';
import request from 'supertest';
import express, { Express } from 'express';
// Mock services
const mockAITaskService = {
createTaskFromNaturalLanguage: jest.fn(),
updateTaskContext: jest.fn(),
getTaskSuggestions: jest.fn(),
breakdownTask: jest.fn(),
getHealthStatus: jest.fn(),
getUsageStats: jest.fn(),
warmCache: jest.fn(),
};
const mockOpenAIService = {
testConnection: jest.fn(),
};
const mockLangChainService = {
testConnection: jest.fn(),
};
// Mock DI container before importing routes
jest.mock('tsyringe', () => ({
container: {
resolve: jest.fn((token: string) => {
if (token === 'AITaskService') return mockAITaskService;
if (token === 'OpenAIService') return mockOpenAIService;
if (token === 'LangChainService') return mockLangChainService;
return null;
}),
register: jest.fn(),
},
injectable: jest.fn(() => (target: any) => target),
inject: jest.fn(() => (target: any, key: any, index: any) => {}),
}));
// Import routes after mocking
const aiRoutes = require('../../src/api/routes/ai.routes').default;
describe('AI API Integration Tests', () => {
let app: Express;
beforeEach(() => {
jest.clearAllMocks();
// Create Express app
app = express();
app.use(express.json());
app.use('/api/ai', aiRoutes);
// Add error handler
app.use((err: any, req: any, res: any, next: any) => {
res.status(err.status || 500).json({
success: false,
error: err.message || 'Internal server error',
});
});
});
describe('POST /api/ai/create-task', () => {
it('should create a task from natural language successfully', async () => {
const mockTask = {
title: 'Implement user authentication',
description: 'Add JWT-based authentication to the application',
priority: 'high',
complexity: 'major',
estimatedHours: 16,
acceptanceCriteria: [
'Users can register',
'Users can login',
'JWT tokens are secure',
],
tags: ['auth', 'security'],
};
mockAITaskService.createTaskFromNaturalLanguage.mockResolvedValue(mockTask);
const response = await request(app)
.post('/api/ai/create-task')
.send({
prompt: 'Implement user authentication with JWT',
projectId: 'project-123',
context: {
projectDescription: 'E-commerce platform',
currentTasks: ['Database setup', 'API structure'],
},
})
.expect(201);
expect(response.body).toEqual({
success: true,
data: mockTask,
message: 'Task created successfully from natural language',
});
expect(mockAITaskService.createTaskFromNaturalLanguage).toHaveBeenCalledWith({
prompt: 'Implement user authentication with JWT',
projectId: 'project-123',
context: {
projectDescription: 'E-commerce platform',
currentTasks: ['Database setup', 'API structure'],
},
});
});
it('should return 400 for invalid request', async () => {
const response = await request(app)
.post('/api/ai/create-task')
.send({
// Missing required 'prompt' field
projectId: 'project-123',
})
.expect(400);
expect(response.body).toMatchObject({
success: false,
error: 'Validation error',
details: expect.any(Array),
});
expect(mockAITaskService.createTaskFromNaturalLanguage).not.toHaveBeenCalled();
});
it('should handle rate limiting', async () => {
mockAITaskService.createTaskFromNaturalLanguage.mockRejectedValue(
new Error('Rate limit exceeded for task_creation')
);
const response = await request(app)
.post('/api/ai/create-task')
.send({
prompt: 'Create a task',
})
.expect(429);
expect(response.body).toEqual({
success: false,
error: 'Rate limit exceeded',
message: 'Too many requests. Please try again later.',
});
});
});
describe('PUT /api/ai/update-context/:taskId', () => {
it('should update task context with feedback', async () => {
mockAITaskService.updateTaskContext.mockResolvedValue(undefined);
const response = await request(app)
.put('/api/ai/update-context/task-123')
.send({
feedback: 'The task needs more specific acceptance criteria',
})
.expect(200);
expect(response.body).toEqual({
success: true,
message: 'Task context updated successfully',
});
expect(mockAITaskService.updateTaskContext).toHaveBeenCalledWith(
'task-123',
'The task needs more specific acceptance criteria'
);
});
it('should validate feedback is provided', async () => {
const response = await request(app)
.put('/api/ai/update-context/task-123')
.send({})
.expect(400);
expect(response.body).toMatchObject({
success: false,
error: 'Validation error',
});
});
});
describe('GET /api/ai/suggestions/:projectId', () => {
it('should get task suggestions for a project', async () => {
const mockSuggestions = [
{
id: 'suggestion-1',
title: 'Add error handling',
description: 'Implement comprehensive error handling',
rationale: 'Improves user experience and debugging',
priority: 'medium',
confidence: 0.85,
},
{
id: 'suggestion-2',
title: 'Setup logging',
description: 'Add structured logging system',
rationale: 'Essential for production monitoring',
priority: 'high',
confidence: 0.92,
},
];
mockAITaskService.getTaskSuggestions.mockResolvedValue(mockSuggestions);
const response = await request(app)
.get('/api/ai/suggestions/project-123')
.query({
projectDescription: 'E-commerce platform',
completedTasks: 'Setup database,Create API structure',
currentTasks: 'Build UI components',
goals: 'Launch MVP,Scale to 1000 users',
})
.expect(200);
expect(response.body).toEqual({
success: true,
data: mockSuggestions,
projectId: 'project-123',
});
expect(mockAITaskService.getTaskSuggestions).toHaveBeenCalledWith(
'project-123',
{
projectDescription: 'E-commerce platform',
completedTasks: ['Setup database', 'Create API structure'],
currentTasks: ['Build UI components'],
goals: ['Launch MVP', 'Scale to 1000 users'],
}
);
});
it('should handle missing query parameters', async () => {
const response = await request(app)
.get('/api/ai/suggestions/project-123')
.expect(400);
expect(response.body).toMatchObject({
success: false,
error: 'Validation error',
});
});
});
describe('POST /api/ai/breakdown/:taskId', () => {
it('should break down a task into subtasks', async () => {
const mockBreakdown = {
title: 'Implement authentication',
description: 'Create secure authentication system',
subtasks: [
{
id: '1',
title: 'Setup database schema',
description: 'Create user tables and indexes',
estimatedHours: 2,
},
{
id: '2',
title: 'Implement JWT logic',
description: 'Create token generation and validation',
estimatedHours: 4,
},
{
id: '3',
title: 'Create API endpoints',
description: 'Build login, register, and refresh endpoints',
estimatedHours: 3,
},
],
complexity: 'major',
estimatedTotalHours: 9,
suggestedApproach: 'Use industry-standard JWT with refresh tokens',
};
mockAITaskService.breakdownTask.mockResolvedValue(mockBreakdown);
const response = await request(app)
.post('/api/ai/breakdown/task-456')
.send({
taskDescription: 'Implement user authentication with JWT tokens',
context: 'Node.js Express application with PostgreSQL database',
})
.expect(200);
expect(response.body).toEqual({
success: true,
data: mockBreakdown,
taskId: 'task-456',
});
expect(mockAITaskService.breakdownTask).toHaveBeenCalledWith(
'task-456',
'Implement user authentication with JWT tokens',
'Node.js Express application with PostgreSQL database'
);
});
});
describe('GET /api/ai/health', () => {
it('should return healthy status when all services are operational', async () => {
mockAITaskService.getHealthStatus.mockResolvedValue({
openai: true,
langchain: true,
redis: true,
rateLimiter: true,
});
const response = await request(app)
.get('/api/ai/health')
.expect(200);
expect(response.body).toMatchObject({
success: true,
healthy: true,
services: {
openai: true,
langchain: true,
redis: true,
rateLimiter: true,
},
timestamp: expect.any(String),
});
});
it('should return 503 when services are degraded', async () => {
mockAITaskService.getHealthStatus.mockResolvedValue({
openai: false,
langchain: true,
redis: true,
rateLimiter: true,
});
const response = await request(app)
.get('/api/ai/health')
.expect(503);
expect(response.body).toMatchObject({
success: true,
healthy: false,
services: expect.any(Object),
});
});
});
describe('GET /api/ai/usage', () => {
it('should return usage statistics', async () => {
const mockStats = {
openai: {
requestCount: 150,
totalTokensUsed: 75000,
estimatedCost: 1.5,
},
totalContextsSaved: 300,
cacheHitRate: 0.68,
};
mockAITaskService.getUsageStats.mockResolvedValue(mockStats);
const response = await request(app)
.get('/api/ai/usage')
.expect(200);
expect(response.body).toEqual({
success: true,
data: mockStats,
timestamp: expect.any(String),
});
});
});
describe('POST /api/ai/warm-cache', () => {
it('should initiate cache warming for a project', async () => {
mockAITaskService.warmCache.mockResolvedValue(undefined);
const response = await request(app)
.post('/api/ai/warm-cache')
.send({
projectId: 'project-789',
})
.expect(202);
expect(response.body).toEqual({
success: true,
message: 'Cache warming initiated',
projectId: 'project-789',
});
expect(mockAITaskService.warmCache).toHaveBeenCalledWith('project-789');
});
it('should require projectId', async () => {
const response = await request(app)
.post('/api/ai/warm-cache')
.send({})
.expect(400);
expect(response.body).toEqual({
success: false,
error: 'Project ID is required',
});
});
});
describe('POST /api/ai/test-connection', () => {
it('should test connections to all AI services', async () => {
mockOpenAIService.testConnection.mockResolvedValue(true);
mockLangChainService.testConnection.mockResolvedValue(true);
const response = await request(app)
.post('/api/ai/test-connection')
.expect(200);
expect(response.body).toEqual({
success: true,
connections: {
openai: true,
langchain: true,
},
message: 'All AI services are connected',
});
});
it('should report partial connectivity', async () => {
mockOpenAIService.testConnection.mockResolvedValue(true);
mockLangChainService.testConnection.mockResolvedValue(false);
const response = await request(app)
.post('/api/ai/test-connection')
.expect(200);
expect(response.body).toEqual({
success: true,
connections: {
openai: true,
langchain: false,
},
message: 'Some AI services are not available',
});
});
});
describe('Error handling', () => {
it('should handle internal server errors gracefully', async () => {
mockAITaskService.createTaskFromNaturalLanguage.mockRejectedValue(
new Error('Database connection failed')
);
const response = await request(app)
.post('/api/ai/create-task')
.send({
prompt: 'Create a task',
})
.expect(500);
expect(response.body).toMatchObject({
success: false,
error: 'Database connection failed',
});
});
it('should validate request data types', async () => {
const response = await request(app)
.post('/api/ai/create-task')
.send({
prompt: 123, // Should be string
})
.expect(400);
expect(response.body).toMatchObject({
success: false,
error: 'Validation error',
});
});
});
describe('End-to-end workflow', () => {
it('should complete full task creation and breakdown flow', async () => {
// Step 1: Create task from natural language
const createdTask = {
id: 'task-new-123',
title: 'Build user dashboard',
description: 'Create interactive dashboard for users',
priority: 'high',
complexity: 'major',
};
mockAITaskService.createTaskFromNaturalLanguage.mockResolvedValue(createdTask);
const createResponse = await request(app)
.post('/api/ai/create-task')
.send({
prompt: 'Build a user dashboard with charts and metrics',
})
.expect(201);
expect(createResponse.body.success).toBe(true);
expect(createResponse.body.data).toMatchObject(createdTask);
// Step 2: Break down the created task
const breakdown = {
title: createdTask.title,
description: createdTask.description,
subtasks: [
{ id: '1', title: 'Design dashboard layout' },
{ id: '2', title: 'Implement chart components' },
{ id: '3', title: 'Add metrics calculations' },
],
complexity: 'major',
estimatedTotalHours: 24,
};
mockAITaskService.breakdownTask.mockResolvedValue(breakdown);
const breakdownResponse = await request(app)
.post(`/api/ai/breakdown/${createdTask.id}`)
.send({
taskDescription: createdTask.description,
context: 'React application with D3.js for charts',
})
.expect(200);
expect(breakdownResponse.body.success).toBe(true);
expect(breakdownResponse.body.data.subtasks).toHaveLength(3);
// Step 3: Update context with feedback
mockAITaskService.updateTaskContext.mockResolvedValue(undefined);
const updateResponse = await request(app)
.put(`/api/ai/update-context/${createdTask.id}`)
.send({
feedback: 'Focus on real-time data updates for the dashboard',
})
.expect(200);
expect(updateResponse.body.success).toBe(true);
});
});
});