352 lines
14 KiB
JavaScript
352 lines
14 KiB
JavaScript
"use strict";
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
require("reflect-metadata");
|
|
// Mock dependencies before importing controller
|
|
jest.mock('tsyringe', () => ({
|
|
container: {
|
|
resolve: jest.fn((token) => {
|
|
if (token === 'AITaskService')
|
|
return mockAITaskService;
|
|
if (token === 'OpenAIService')
|
|
return mockOpenAIService;
|
|
if (token === 'LangChainService')
|
|
return mockLangChainService;
|
|
return null;
|
|
}),
|
|
},
|
|
injectable: jest.fn(() => (target) => target),
|
|
inject: jest.fn(() => (target, key, index) => { }),
|
|
}));
|
|
// 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(),
|
|
};
|
|
const ai_controller_1 = require("../../../src/api/controllers/ai.controller");
|
|
describe('AIController', () => {
|
|
let controller;
|
|
let mockReq;
|
|
let mockRes;
|
|
let mockNext;
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
controller = new ai_controller_1.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, mockRes, 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, mockRes, 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, mockRes, 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, mockRes, 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, mockRes, 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, mockRes, 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, mockRes, 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, mockRes, 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, mockRes, 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, mockRes, 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, mockRes, 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, mockRes, 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, mockRes, 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, mockRes, 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, mockRes, 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, mockRes, 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, mockRes, 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),
|
|
}),
|
|
]),
|
|
});
|
|
});
|
|
});
|
|
});
|
|
//# sourceMappingURL=ai.controller.test.js.map
|