/** * Task Manager module tests */ import { jest } from '@jest/globals'; // Mock implementations const mockReadFileSync = jest.fn(); const mockExistsSync = jest.fn(); const mockMkdirSync = jest.fn(); const mockDirname = jest.fn(); const mockCallClaude = jest.fn(); const mockWriteJSON = jest.fn(); const mockGenerateTaskFiles = jest.fn(); // Mock fs module jest.mock('fs', () => ({ readFileSync: mockReadFileSync, existsSync: mockExistsSync, mkdirSync: mockMkdirSync })); // Mock path module jest.mock('path', () => ({ dirname: mockDirname })); // Mock AI services jest.mock('../../scripts/modules/ai-services.js', () => ({ callClaude: mockCallClaude })); // Mock utils jest.mock('../../scripts/modules/utils.js', () => ({ writeJSON: mockWriteJSON, log: jest.fn() })); // Create a simplified version of parsePRD for testing const testParsePRD = async (prdPath, outputPath, numTasks) => { try { const prdContent = mockReadFileSync(prdPath, 'utf8'); const tasks = await mockCallClaude(prdContent, prdPath, numTasks); const dir = mockDirname(outputPath); if (!mockExistsSync(dir)) { mockMkdirSync(dir, { recursive: true }); } mockWriteJSON(outputPath, tasks); await mockGenerateTaskFiles(outputPath, dir); return tasks; } catch (error) { console.error(`Error parsing PRD: ${error.message}`); process.exit(1); } }; // Import after mocks import { findNextTask } from '../../scripts/modules/task-manager.js'; import { sampleClaudeResponse } from '../fixtures/sample-claude-response.js'; describe('Task Manager Module', () => { beforeEach(() => { jest.clearAllMocks(); }); describe('findNextTask function', () => { test('should return the highest priority task with all dependencies satisfied', () => { const tasks = [ { id: 1, title: 'Setup Project', status: 'done', dependencies: [], priority: 'high' }, { id: 2, title: 'Implement Core Features', status: 'pending', dependencies: [1], priority: 'high' }, { id: 3, title: 'Create Documentation', status: 'pending', dependencies: [1], priority: 'medium' }, { id: 4, title: 'Deploy Application', status: 'pending', dependencies: [2, 3], priority: 'high' } ]; const nextTask = findNextTask(tasks); expect(nextTask).toBeDefined(); expect(nextTask.id).toBe(2); expect(nextTask.title).toBe('Implement Core Features'); }); test('should prioritize by priority level when dependencies are equal', () => { const tasks = [ { id: 1, title: 'Setup Project', status: 'done', dependencies: [], priority: 'high' }, { id: 2, title: 'Low Priority Task', status: 'pending', dependencies: [1], priority: 'low' }, { id: 3, title: 'Medium Priority Task', status: 'pending', dependencies: [1], priority: 'medium' }, { id: 4, title: 'High Priority Task', status: 'pending', dependencies: [1], priority: 'high' } ]; const nextTask = findNextTask(tasks); expect(nextTask.id).toBe(4); expect(nextTask.priority).toBe('high'); }); test('should return null when all tasks are completed', () => { const tasks = [ { id: 1, title: 'Setup Project', status: 'done', dependencies: [], priority: 'high' }, { id: 2, title: 'Implement Features', status: 'done', dependencies: [1], priority: 'high' } ]; const nextTask = findNextTask(tasks); expect(nextTask).toBeNull(); }); test('should return null when all pending tasks have unsatisfied dependencies', () => { const tasks = [ { id: 1, title: 'Setup Project', status: 'pending', dependencies: [2], priority: 'high' }, { id: 2, title: 'Implement Features', status: 'pending', dependencies: [1], priority: 'high' } ]; const nextTask = findNextTask(tasks); expect(nextTask).toBeNull(); }); test('should handle empty tasks array', () => { const nextTask = findNextTask([]); expect(nextTask).toBeNull(); }); }); // Skipped tests for analyzeTaskComplexity describe.skip('analyzeTaskComplexity function', () => { // These tests are skipped because they require complex mocking // but document what should be tested test('should handle valid JSON response from LLM', async () => { // This test would verify that: // 1. The function properly calls the AI model // 2. It correctly parses a valid JSON response // 3. It generates a properly formatted complexity report // 4. The report includes all analyzed tasks with their complexity scores expect(true).toBe(true); }); test('should handle and fix malformed JSON with unterminated strings', async () => { // This test would verify that: // 1. The function can handle JSON with unterminated strings // 2. It applies regex fixes to repair the malformed JSON // 3. It still produces a valid report despite receiving bad JSON expect(true).toBe(true); }); test('should handle missing tasks in the response', async () => { // This test would verify that: // 1. When the AI response is missing some tasks // 2. The function detects the missing tasks // 3. It attempts to analyze just those missing tasks // 4. The final report includes all tasks that could be analyzed expect(true).toBe(true); }); }); describe('parsePRD function', () => { // Mock the sample PRD content const samplePRDContent = '# Sample PRD for Testing'; beforeEach(() => { // Reset all mocks jest.clearAllMocks(); // Set up mocks for fs, path and other modules mockReadFileSync.mockReturnValue(samplePRDContent); mockExistsSync.mockReturnValue(true); mockDirname.mockReturnValue('tasks'); mockCallClaude.mockResolvedValue(sampleClaudeResponse); mockGenerateTaskFiles.mockResolvedValue(undefined); }); test('should parse a PRD file and generate tasks', async () => { // Call the test version of parsePRD await testParsePRD('path/to/prd.txt', 'tasks/tasks.json', 3); // Verify fs.readFileSync was called with the correct arguments expect(mockReadFileSync).toHaveBeenCalledWith('path/to/prd.txt', 'utf8'); // Verify callClaude was called with the correct arguments expect(mockCallClaude).toHaveBeenCalledWith(samplePRDContent, 'path/to/prd.txt', 3); // Verify directory check expect(mockExistsSync).toHaveBeenCalledWith('tasks'); // Verify writeJSON was called with the correct arguments expect(mockWriteJSON).toHaveBeenCalledWith('tasks/tasks.json', sampleClaudeResponse); // Verify generateTaskFiles was called expect(mockGenerateTaskFiles).toHaveBeenCalledWith('tasks/tasks.json', 'tasks'); }); test('should create the tasks directory if it does not exist', async () => { // Mock existsSync to return false to simulate directory doesn't exist mockExistsSync.mockReturnValueOnce(false); // Call the function await testParsePRD('path/to/prd.txt', 'tasks/tasks.json', 3); // Verify mkdir was called expect(mockMkdirSync).toHaveBeenCalledWith('tasks', { recursive: true }); }); test('should handle errors in the PRD parsing process', async () => { // Mock an error in callClaude const testError = new Error('Test error in Claude API call'); mockCallClaude.mockRejectedValueOnce(testError); // Mock console.error and process.exit const mockConsoleError = jest.spyOn(console, 'error').mockImplementation(() => {}); const mockProcessExit = jest.spyOn(process, 'exit').mockImplementation(() => {}); // Call the function await testParsePRD('path/to/prd.txt', 'tasks/tasks.json', 3); // Verify error handling expect(mockConsoleError).toHaveBeenCalled(); expect(mockProcessExit).toHaveBeenCalledWith(1); // Restore mocks mockConsoleError.mockRestore(); mockProcessExit.mockRestore(); }); test('should generate individual task files after creating tasks.json', async () => { // Call the function await testParsePRD('path/to/prd.txt', 'tasks/tasks.json', 3); // Verify generateTaskFiles was called expect(mockGenerateTaskFiles).toHaveBeenCalledWith('tasks/tasks.json', 'tasks'); }); }); });