442 lines
13 KiB
TypeScript
442 lines
13 KiB
TypeScript
import 'reflect-metadata';
|
|
import { Request, Response, NextFunction } from 'express';
|
|
import { z } from 'zod';
|
|
|
|
// Mock dependencies before importing controller
|
|
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;
|
|
}),
|
|
},
|
|
injectable: jest.fn(() => (target: any) => target),
|
|
inject: jest.fn(() => (target: any, key: any, index: any) => {}),
|
|
}));
|
|
|
|
// 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(),
|
|
};
|
|
|
|
import { AIController } from '../../../src/api/controllers/ai.controller';
|
|
|
|
describe('AIController', () => {
|
|
let controller: AIController;
|
|
let mockReq: Partial<Request>;
|
|
let mockRes: Partial<Response>;
|
|
let mockNext: NextFunction;
|
|
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
|
|
controller = new AIController();
|
|
|
|
mockReq = {
|
|
body: {},
|
|
params: {},
|
|
query: {},
|
|
};
|
|
|
|
mockRes = {
|
|
status: jest.fn().mockReturnThis(),
|
|
json: jest.fn().mockReturnThis(),
|
|
};
|
|
|
|
mockNext = jest.fn();
|
|
});
|
|
|
|
describe('createTask', () => {
|
|
it('should create a task from natural language', async () => {
|
|
const mockTask = {
|
|
title: 'Implement login',
|
|
description: 'Add user authentication',
|
|
priority: 'high',
|
|
complexity: 'major',
|
|
};
|
|
|
|
mockAITaskService.createTaskFromNaturalLanguage.mockResolvedValue(mockTask);
|
|
|
|
mockReq.body = {
|
|
prompt: 'Create a login feature',
|
|
projectId: 'project-123',
|
|
};
|
|
|
|
await controller.createTask(mockReq as Request, mockRes as Response, mockNext);
|
|
|
|
expect(mockAITaskService.createTaskFromNaturalLanguage).toHaveBeenCalledWith({
|
|
prompt: 'Create a login feature',
|
|
projectId: 'project-123',
|
|
context: undefined,
|
|
});
|
|
|
|
expect(mockRes.status).toHaveBeenCalledWith(201);
|
|
expect(mockRes.json).toHaveBeenCalledWith({
|
|
success: true,
|
|
data: mockTask,
|
|
message: 'Task created successfully from natural language',
|
|
});
|
|
});
|
|
|
|
it('should handle validation errors', async () => {
|
|
mockReq.body = {
|
|
// Missing required 'prompt' field
|
|
projectId: 'project-123',
|
|
};
|
|
|
|
await controller.createTask(mockReq as Request, mockRes as Response, mockNext);
|
|
|
|
expect(mockRes.status).toHaveBeenCalledWith(400);
|
|
expect(mockRes.json).toHaveBeenCalledWith({
|
|
success: false,
|
|
error: 'Validation error',
|
|
details: expect.any(Array),
|
|
});
|
|
});
|
|
|
|
it('should handle rate limiting errors', async () => {
|
|
mockAITaskService.createTaskFromNaturalLanguage.mockRejectedValue(
|
|
new Error('Rate limit exceeded for task_creation')
|
|
);
|
|
|
|
mockReq.body = {
|
|
prompt: 'Create a task',
|
|
};
|
|
|
|
await controller.createTask(mockReq as Request, mockRes as Response, mockNext);
|
|
|
|
expect(mockRes.status).toHaveBeenCalledWith(429);
|
|
expect(mockRes.json).toHaveBeenCalledWith({
|
|
success: false,
|
|
error: 'Rate limit exceeded',
|
|
message: 'Too many requests. Please try again later.',
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('updateTaskContext', () => {
|
|
it('should update task context with feedback', async () => {
|
|
mockAITaskService.updateTaskContext.mockResolvedValue(undefined);
|
|
|
|
mockReq.params = { taskId: 'task-123' };
|
|
mockReq.body = { feedback: 'Task needs more detail' };
|
|
|
|
await controller.updateTaskContext(mockReq as Request, mockRes as Response, mockNext);
|
|
|
|
expect(mockAITaskService.updateTaskContext).toHaveBeenCalledWith(
|
|
'task-123',
|
|
'Task needs more detail'
|
|
);
|
|
|
|
expect(mockRes.status).toHaveBeenCalledWith(200);
|
|
expect(mockRes.json).toHaveBeenCalledWith({
|
|
success: true,
|
|
message: 'Task context updated successfully',
|
|
});
|
|
});
|
|
|
|
it('should validate feedback is provided', async () => {
|
|
mockReq.params = { taskId: 'task-123' };
|
|
mockReq.body = {}; // Missing feedback
|
|
|
|
await controller.updateTaskContext(mockReq as Request, mockRes as Response, mockNext);
|
|
|
|
expect(mockRes.status).toHaveBeenCalledWith(400);
|
|
expect(mockRes.json).toHaveBeenCalledWith({
|
|
success: false,
|
|
error: 'Validation error',
|
|
details: expect.any(Array),
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('getTaskSuggestions', () => {
|
|
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',
|
|
priority: 'medium',
|
|
confidence: 0.85,
|
|
},
|
|
];
|
|
|
|
mockAITaskService.getTaskSuggestions.mockResolvedValue(mockSuggestions);
|
|
|
|
mockReq.params = { projectId: 'project-123' };
|
|
mockReq.query = {
|
|
projectDescription: 'E-commerce platform',
|
|
completedTasks: 'Setup database,Create API',
|
|
currentTasks: 'Build UI',
|
|
goals: 'Launch MVP',
|
|
};
|
|
|
|
await controller.getTaskSuggestions(mockReq as Request, mockRes as Response, mockNext);
|
|
|
|
expect(mockAITaskService.getTaskSuggestions).toHaveBeenCalledWith(
|
|
'project-123',
|
|
{
|
|
projectDescription: 'E-commerce platform',
|
|
completedTasks: ['Setup database', 'Create API'],
|
|
currentTasks: ['Build UI'],
|
|
goals: ['Launch MVP'],
|
|
}
|
|
);
|
|
|
|
expect(mockRes.status).toHaveBeenCalledWith(200);
|
|
expect(mockRes.json).toHaveBeenCalledWith({
|
|
success: true,
|
|
data: mockSuggestions,
|
|
projectId: 'project-123',
|
|
});
|
|
});
|
|
|
|
it('should handle empty arrays for task lists', async () => {
|
|
mockAITaskService.getTaskSuggestions.mockResolvedValue([]);
|
|
|
|
mockReq.params = { projectId: 'project-456' };
|
|
mockReq.query = {
|
|
projectDescription: 'New project',
|
|
// No completed/current tasks or goals
|
|
};
|
|
|
|
await controller.getTaskSuggestions(mockReq as Request, mockRes as Response, mockNext);
|
|
|
|
expect(mockAITaskService.getTaskSuggestions).toHaveBeenCalledWith(
|
|
'project-456',
|
|
{
|
|
projectDescription: 'New project',
|
|
completedTasks: [],
|
|
currentTasks: [],
|
|
goals: [],
|
|
}
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('breakdownTask', () => {
|
|
it('should break down a task into subtasks', async () => {
|
|
const mockBreakdown = {
|
|
title: 'Main task',
|
|
description: 'Task description',
|
|
subtasks: [
|
|
{ id: '1', title: 'Subtask 1', description: 'First part' },
|
|
{ id: '2', title: 'Subtask 2', description: 'Second part' },
|
|
],
|
|
complexity: 'major',
|
|
estimatedTotalHours: 8,
|
|
};
|
|
|
|
mockAITaskService.breakdownTask.mockResolvedValue(mockBreakdown);
|
|
|
|
mockReq.params = { taskId: 'task-789' };
|
|
mockReq.body = {
|
|
taskDescription: 'Implement authentication system',
|
|
context: 'Web application using JWT',
|
|
};
|
|
|
|
await controller.breakdownTask(mockReq as Request, mockRes as Response, mockNext);
|
|
|
|
expect(mockAITaskService.breakdownTask).toHaveBeenCalledWith(
|
|
'task-789',
|
|
'Implement authentication system',
|
|
'Web application using JWT'
|
|
);
|
|
|
|
expect(mockRes.status).toHaveBeenCalledWith(200);
|
|
expect(mockRes.json).toHaveBeenCalledWith({
|
|
success: true,
|
|
data: mockBreakdown,
|
|
taskId: 'task-789',
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('getHealthStatus', () => {
|
|
it('should return healthy status when all services are up', async () => {
|
|
mockAITaskService.getHealthStatus.mockResolvedValue({
|
|
openai: true,
|
|
langchain: true,
|
|
redis: true,
|
|
rateLimiter: true,
|
|
});
|
|
|
|
await controller.getHealthStatus(mockReq as Request, mockRes as Response, mockNext);
|
|
|
|
expect(mockRes.status).toHaveBeenCalledWith(200);
|
|
expect(mockRes.json).toHaveBeenCalledWith({
|
|
success: true,
|
|
healthy: true,
|
|
services: {
|
|
openai: true,
|
|
langchain: true,
|
|
redis: true,
|
|
rateLimiter: true,
|
|
},
|
|
timestamp: expect.any(String),
|
|
});
|
|
});
|
|
|
|
it('should return 503 when services are down', async () => {
|
|
mockAITaskService.getHealthStatus.mockResolvedValue({
|
|
openai: false,
|
|
langchain: true,
|
|
redis: true,
|
|
rateLimiter: true,
|
|
});
|
|
|
|
await controller.getHealthStatus(mockReq as Request, mockRes as Response, mockNext);
|
|
|
|
expect(mockRes.status).toHaveBeenCalledWith(503);
|
|
expect(mockRes.json).toHaveBeenCalledWith({
|
|
success: true,
|
|
healthy: false,
|
|
services: expect.any(Object),
|
|
timestamp: expect.any(String),
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('getUsageStats', () => {
|
|
it('should return usage statistics', async () => {
|
|
const mockStats = {
|
|
openai: {
|
|
requestCount: 100,
|
|
totalTokensUsed: 50000,
|
|
estimatedCost: 1.0,
|
|
},
|
|
totalContextsSaved: 250,
|
|
cacheHitRate: 0.75,
|
|
};
|
|
|
|
mockAITaskService.getUsageStats.mockResolvedValue(mockStats);
|
|
|
|
await controller.getUsageStats(mockReq as Request, mockRes as Response, mockNext);
|
|
|
|
expect(mockRes.status).toHaveBeenCalledWith(200);
|
|
expect(mockRes.json).toHaveBeenCalledWith({
|
|
success: true,
|
|
data: mockStats,
|
|
timestamp: expect.any(String),
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('warmCache', () => {
|
|
it('should initiate cache warming', async () => {
|
|
mockAITaskService.warmCache.mockResolvedValue(undefined);
|
|
|
|
mockReq.body = { projectId: 'project-123' };
|
|
|
|
await controller.warmCache(mockReq as Request, mockRes as Response, mockNext);
|
|
|
|
expect(mockAITaskService.warmCache).toHaveBeenCalledWith('project-123');
|
|
|
|
expect(mockRes.status).toHaveBeenCalledWith(202);
|
|
expect(mockRes.json).toHaveBeenCalledWith({
|
|
success: true,
|
|
message: 'Cache warming initiated',
|
|
projectId: 'project-123',
|
|
});
|
|
});
|
|
|
|
it('should require projectId', async () => {
|
|
mockReq.body = {}; // Missing projectId
|
|
|
|
await controller.warmCache(mockReq as Request, mockRes as Response, mockNext);
|
|
|
|
expect(mockRes.status).toHaveBeenCalledWith(400);
|
|
expect(mockRes.json).toHaveBeenCalledWith({
|
|
success: false,
|
|
error: 'Project ID is required',
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('testConnection', () => {
|
|
it('should test connections to AI services', async () => {
|
|
mockOpenAIService.testConnection.mockResolvedValue(true);
|
|
mockLangChainService.testConnection.mockResolvedValue(true);
|
|
|
|
await controller.testConnection(mockReq as Request, mockRes as Response, mockNext);
|
|
|
|
expect(mockRes.status).toHaveBeenCalledWith(200);
|
|
expect(mockRes.json).toHaveBeenCalledWith({
|
|
success: true,
|
|
connections: {
|
|
openai: true,
|
|
langchain: true,
|
|
},
|
|
message: 'All AI services are connected',
|
|
});
|
|
});
|
|
|
|
it('should report when some services are unavailable', async () => {
|
|
mockOpenAIService.testConnection.mockResolvedValue(true);
|
|
mockLangChainService.testConnection.mockResolvedValue(false);
|
|
|
|
await controller.testConnection(mockReq as Request, mockRes as Response, mockNext);
|
|
|
|
expect(mockRes.status).toHaveBeenCalledWith(200);
|
|
expect(mockRes.json).toHaveBeenCalledWith({
|
|
success: true,
|
|
connections: {
|
|
openai: true,
|
|
langchain: false,
|
|
},
|
|
message: 'Some AI services are not available',
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('error handling', () => {
|
|
it('should call next with non-validation errors', async () => {
|
|
const error = new Error('Database error');
|
|
mockAITaskService.createTaskFromNaturalLanguage.mockRejectedValue(error);
|
|
|
|
mockReq.body = { prompt: 'Create task' };
|
|
|
|
await controller.createTask(mockReq as Request, mockRes as Response, mockNext);
|
|
|
|
expect(mockNext).toHaveBeenCalledWith(error);
|
|
});
|
|
|
|
it('should handle Zod validation errors with details', async () => {
|
|
mockReq.body = {
|
|
prompt: '', // Empty string should fail validation
|
|
};
|
|
|
|
await controller.createTask(mockReq as Request, mockRes as Response, mockNext);
|
|
|
|
expect(mockRes.status).toHaveBeenCalledWith(400);
|
|
expect(mockRes.json).toHaveBeenCalledWith({
|
|
success: false,
|
|
error: 'Validation error',
|
|
details: expect.arrayContaining([
|
|
expect.objectContaining({
|
|
path: expect.any(Array),
|
|
message: expect.any(String),
|
|
}),
|
|
]),
|
|
});
|
|
});
|
|
});
|
|
}); |