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; let mockRes: Partial; 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), }), ]), }); }); }); });