/** * Integration test for direct function imports in MCP server */ import { jest } from '@jest/globals'; import path, { dirname } from 'path'; import { fileURLToPath } from 'url'; // Get the current module's directory const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); // Test file paths const testProjectRoot = path.join(__dirname, '../../fixtures'); const testTasksPath = path.join(testProjectRoot, 'test-tasks.json'); // Create explicit mock functions const mockExistsSync = jest.fn().mockReturnValue(true); const mockWriteFileSync = jest.fn(); const mockReadFileSync = jest.fn(); const mockUnlinkSync = jest.fn(); const mockMkdirSync = jest.fn(); const mockFindTasksJsonPath = jest.fn().mockReturnValue(testTasksPath); const mockReadJSON = jest.fn(); const mockWriteJSON = jest.fn(); const mockEnableSilentMode = jest.fn(); const mockDisableSilentMode = jest.fn(); const mockReadComplexityReport = jest.fn().mockReturnValue(null); const mockGetAnthropicClient = jest.fn().mockReturnValue({}); const mockGetConfiguredAnthropicClient = jest.fn().mockReturnValue({}); const mockHandleAnthropicStream = jest.fn().mockResolvedValue( JSON.stringify([ { id: 1, title: 'Mock Subtask 1', description: 'First mock subtask', dependencies: [], details: 'Implementation details for mock subtask 1' }, { id: 2, title: 'Mock Subtask 2', description: 'Second mock subtask', dependencies: [1], details: 'Implementation details for mock subtask 2' } ]) ); const mockParseSubtasksFromText = jest.fn().mockReturnValue([ { id: 1, title: 'Mock Subtask 1', description: 'First mock subtask', status: 'pending', dependencies: [] }, { id: 2, title: 'Mock Subtask 2', description: 'Second mock subtask', status: 'pending', dependencies: [1] } ]); // Create a mock for expandTask that returns predefined responses instead of making real calls const mockExpandTask = jest .fn() .mockImplementation( (taskId, numSubtasks, useResearch, additionalContext, options) => { const task = { ...(sampleTasks.tasks.find((t) => t.id === taskId) || {}), subtasks: useResearch ? [ { id: 1, title: 'Research-Backed Subtask 1', description: 'First research-backed subtask', status: 'pending', dependencies: [] }, { id: 2, title: 'Research-Backed Subtask 2', description: 'Second research-backed subtask', status: 'pending', dependencies: [1] } ] : [ { id: 1, title: 'Mock Subtask 1', description: 'First mock subtask', status: 'pending', dependencies: [] }, { id: 2, title: 'Mock Subtask 2', description: 'Second mock subtask', status: 'pending', dependencies: [1] } ] }; return Promise.resolve(task); } ); const mockGenerateTaskFiles = jest.fn().mockResolvedValue(true); const mockFindTaskById = jest.fn(); const mockTaskExists = jest.fn().mockReturnValue(true); // Mock fs module to avoid file system operations jest.mock('fs', () => ({ existsSync: mockExistsSync, writeFileSync: mockWriteFileSync, readFileSync: mockReadFileSync, unlinkSync: mockUnlinkSync, mkdirSync: mockMkdirSync })); // Mock utils functions to avoid actual file operations jest.mock('../../../scripts/modules/utils.js', () => ({ readJSON: mockReadJSON, writeJSON: mockWriteJSON, enableSilentMode: mockEnableSilentMode, disableSilentMode: mockDisableSilentMode, readComplexityReport: mockReadComplexityReport, CONFIG: { model: 'claude-3-7-sonnet-20250219', maxTokens: 8192, temperature: 0.2, defaultSubtasks: 5 } })); // Mock path-utils with findTasksJsonPath jest.mock('../../../mcp-server/src/core/utils/path-utils.js', () => ({ findTasksJsonPath: mockFindTasksJsonPath })); // Mock the AI module to prevent any real API calls jest.mock('../../../scripts/modules/ai-services-unified.js', () => ({ // Mock the functions exported by ai-services-unified.js as needed // For example, if you are testing a function that uses generateTextService: generateTextService: jest.fn().mockResolvedValue('Mock AI Response') // Add other mocks for generateObjectService, streamTextService if used })); // Mock task-manager.js to avoid real operations jest.mock('../../../scripts/modules/task-manager.js', () => ({ expandTask: mockExpandTask, generateTaskFiles: mockGenerateTaskFiles, findTaskById: mockFindTaskById, taskExists: mockTaskExists })); // Import dependencies after mocks are set up import { sampleTasks } from '../../fixtures/sample-tasks.js'; // Mock logger const mockLogger = { info: jest.fn(), error: jest.fn(), debug: jest.fn(), warn: jest.fn() }; // Mock session const mockSession = { env: { ANTHROPIC_API_KEY: 'mock-api-key', MODEL: 'claude-3-sonnet-20240229', MAX_TOKENS: 4000, TEMPERATURE: '0.2' } }; describe('MCP Server Direct Functions', () => { // Set up before each test beforeEach(() => { jest.clearAllMocks(); // Default mockReadJSON implementation mockReadJSON.mockReturnValue(JSON.parse(JSON.stringify(sampleTasks))); // Default mockFindTaskById implementation mockFindTaskById.mockImplementation((tasks, taskId) => { const id = parseInt(taskId, 10); return tasks.find((t) => t.id === id); }); // Default mockTaskExists implementation mockTaskExists.mockImplementation((tasks, taskId) => { const id = parseInt(taskId, 10); return tasks.some((t) => t.id === id); }); // Default findTasksJsonPath implementation mockFindTasksJsonPath.mockImplementation((args) => { // Mock returning null for non-existent files if (args.file === 'non-existent-file.json') { return null; } return testTasksPath; }); }); describe('listTasksDirect', () => { // Sample complexity report for testing const mockComplexityReport = { meta: { generatedAt: '2025-03-24T20:01:35.986Z', tasksAnalyzed: 3, thresholdScore: 5, projectName: 'Test Project', usedResearch: false }, complexityAnalysis: [ { taskId: 1, taskTitle: 'Initialize Project', complexityScore: 3, recommendedSubtasks: 2 }, { taskId: 2, taskTitle: 'Create Core Functionality', complexityScore: 8, recommendedSubtasks: 5 }, { taskId: 3, taskTitle: 'Implement UI Components', complexityScore: 6, recommendedSubtasks: 4 } ] }; // Test wrapper function that doesn't rely on the actual implementation async function testListTasks(args, mockLogger) { // File not found case if (args.file === 'non-existent-file.json') { mockLogger.error('Tasks file not found'); return { success: false, error: { code: 'FILE_NOT_FOUND_ERROR', message: 'Tasks file not found' } }; } // Check for complexity report const complexityReport = mockReadComplexityReport(); let tasksData = [...sampleTasks.tasks]; // Add complexity scores if report exists if (complexityReport && complexityReport.complexityAnalysis) { tasksData = tasksData.map((task) => { const analysis = complexityReport.complexityAnalysis.find( (a) => a.taskId === task.id ); if (analysis) { return { ...task, complexityScore: analysis.complexityScore }; } return task; }); } // Success case if (!args.status && !args.withSubtasks) { return { success: true, data: { tasks: tasksData, stats: { total: tasksData.length, completed: tasksData.filter((t) => t.status === 'done').length, inProgress: tasksData.filter((t) => t.status === 'in-progress') .length, pending: tasksData.filter((t) => t.status === 'pending').length } } }; } // Status filter case if (args.status) { const filteredTasks = tasksData.filter((t) => t.status === args.status); return { success: true, data: { tasks: filteredTasks, filter: args.status, stats: { total: tasksData.length, filtered: filteredTasks.length } } }; } // Include subtasks case if (args.withSubtasks) { return { success: true, data: { tasks: tasksData, includeSubtasks: true, stats: { total: tasksData.length } } }; } // Default case return { success: true, data: { tasks: [] } }; } test('should return all tasks when no filter is provided', async () => { // Arrange const args = { projectRoot: testProjectRoot, file: testTasksPath }; // Act const result = await testListTasks(args, mockLogger); // Assert expect(result.success).toBe(true); expect(result.data.tasks.length).toBe(sampleTasks.tasks.length); expect(result.data.stats.total).toBe(sampleTasks.tasks.length); }); test('should filter tasks by status', async () => { // Arrange const args = { projectRoot: testProjectRoot, file: testTasksPath, status: 'pending' }; // Act const result = await testListTasks(args, mockLogger); // Assert expect(result.success).toBe(true); expect(result.data.filter).toBe('pending'); // Should only include pending tasks result.data.tasks.forEach((task) => { expect(task.status).toBe('pending'); }); }); test('should include subtasks when requested', async () => { // Arrange const args = { projectRoot: testProjectRoot, file: testTasksPath, withSubtasks: true }; // Act const result = await testListTasks(args, mockLogger); // Assert expect(result.success).toBe(true); expect(result.data.includeSubtasks).toBe(true); // Verify subtasks are included for tasks that have them const tasksWithSubtasks = result.data.tasks.filter( (t) => t.subtasks && t.subtasks.length > 0 ); expect(tasksWithSubtasks.length).toBeGreaterThan(0); }); test('should handle file not found errors', async () => { // Arrange const args = { projectRoot: testProjectRoot, file: 'non-existent-file.json' }; // Act const result = await testListTasks(args, mockLogger); // Assert expect(result.success).toBe(false); expect(result.error.code).toBe('FILE_NOT_FOUND_ERROR'); expect(mockLogger.error).toHaveBeenCalled(); }); test('should include complexity scores when complexity report exists', async () => { // Arrange mockReadComplexityReport.mockReturnValueOnce(mockComplexityReport); const args = { projectRoot: testProjectRoot, file: testTasksPath, withSubtasks: true }; // Act const result = await testListTasks(args, mockLogger); // Assert expect(result.success).toBe(true); // Check that tasks have complexity scores from the report mockComplexityReport.complexityAnalysis.forEach((analysis) => { const task = result.data.tasks.find((t) => t.id === analysis.taskId); if (task) { expect(task.complexityScore).toBe(analysis.complexityScore); } }); }); }); describe('expandTaskDirect', () => { // Test wrapper function that returns appropriate results based on the test case async function testExpandTask(args, mockLogger, options = {}) { // Missing task ID case if (!args.id) { mockLogger.error('Task ID is required'); return { success: false, error: { code: 'INPUT_VALIDATION_ERROR', message: 'Task ID is required' } }; } // Non-existent task ID case if (args.id === '999') { mockLogger.error(`Task with ID ${args.id} not found`); return { success: false, error: { code: 'TASK_NOT_FOUND', message: `Task with ID ${args.id} not found` } }; } // Completed task case if (args.id === '1') { mockLogger.error( `Task ${args.id} is already marked as done and cannot be expanded` ); return { success: false, error: { code: 'TASK_COMPLETED', message: `Task ${args.id} is already marked as done and cannot be expanded` } }; } // For successful cases, record that functions were called but don't make real calls mockEnableSilentMode(); // This is just a mock call that won't make real API requests // We're using mockExpandTask which is already a mock function const expandedTask = await mockExpandTask( parseInt(args.id, 10), args.num, args.research || false, args.prompt || '', { mcpLog: mockLogger, session: options.session } ); mockDisableSilentMode(); return { success: true, data: { task: expandedTask, subtasksAdded: expandedTask.subtasks.length, hasExistingSubtasks: false } }; } test('should expand a task with subtasks', async () => { // Arrange const args = { projectRoot: testProjectRoot, file: testTasksPath, id: '3', // ID 3 exists in sampleTasks with status 'pending' num: 2 }; // Act const result = await testExpandTask(args, mockLogger, { session: mockSession }); // Assert expect(result.success).toBe(true); expect(result.data.task).toBeDefined(); expect(result.data.task.subtasks).toBeDefined(); expect(result.data.task.subtasks.length).toBe(2); expect(mockExpandTask).toHaveBeenCalledWith( 3, // Task ID as number 2, // num parameter false, // useResearch '', // prompt expect.objectContaining({ mcpLog: mockLogger, session: mockSession }) ); expect(mockEnableSilentMode).toHaveBeenCalled(); expect(mockDisableSilentMode).toHaveBeenCalled(); }); test('should handle missing task ID', async () => { // Arrange const args = { projectRoot: testProjectRoot, file: testTasksPath // id is intentionally missing }; // Act const result = await testExpandTask(args, mockLogger, { session: mockSession }); // Assert expect(result.success).toBe(false); expect(result.error.code).toBe('INPUT_VALIDATION_ERROR'); expect(mockLogger.error).toHaveBeenCalled(); // Make sure no real expand calls were made expect(mockExpandTask).not.toHaveBeenCalled(); }); test('should handle non-existent task ID', async () => { // Arrange const args = { projectRoot: testProjectRoot, file: testTasksPath, id: '999' // Non-existent task ID }; // Act const result = await testExpandTask(args, mockLogger, { session: mockSession }); // Assert expect(result.success).toBe(false); expect(result.error.code).toBe('TASK_NOT_FOUND'); expect(mockLogger.error).toHaveBeenCalled(); // Make sure no real expand calls were made expect(mockExpandTask).not.toHaveBeenCalled(); }); test('should handle completed tasks', async () => { // Arrange const args = { projectRoot: testProjectRoot, file: testTasksPath, id: '1' // Task with 'done' status in sampleTasks }; // Act const result = await testExpandTask(args, mockLogger, { session: mockSession }); // Assert expect(result.success).toBe(false); expect(result.error.code).toBe('TASK_COMPLETED'); expect(mockLogger.error).toHaveBeenCalled(); // Make sure no real expand calls were made expect(mockExpandTask).not.toHaveBeenCalled(); }); test('should use AI client when research flag is set', async () => { // Arrange const args = { projectRoot: testProjectRoot, file: testTasksPath, id: '3', research: true }; // Act const result = await testExpandTask(args, mockLogger, { session: mockSession }); // Assert expect(result.success).toBe(true); expect(mockExpandTask).toHaveBeenCalledWith( 3, // Task ID as number undefined, // args.num is undefined true, // useResearch should be true '', // prompt expect.objectContaining({ mcpLog: mockLogger, session: mockSession }) ); // Verify the result includes research-backed subtasks expect(result.data.task.subtasks[0].title).toContain('Research-Backed'); }); }); describe('expandAllTasksDirect', () => { // Test wrapper function that returns appropriate results based on the test case async function testExpandAllTasks(args, mockLogger, options = {}) { // For successful cases, record that functions were called but don't make real calls mockEnableSilentMode(); // Mock expandAllTasks - now returns a structured object instead of undefined const mockExpandAll = jest.fn().mockImplementation(async () => { // Return the new structured response that matches the actual implementation return { success: true, expandedCount: 2, failedCount: 0, skippedCount: 1, tasksToExpand: 3, telemetryData: { timestamp: new Date().toISOString(), commandName: 'expand-all-tasks', totalCost: 0.05, totalTokens: 1000, inputTokens: 600, outputTokens: 400 } }; }); // Call mock expandAllTasks with the correct signature const result = await mockExpandAll( args.file, // tasksPath args.num, // numSubtasks args.research || false, // useResearch args.prompt || '', // additionalContext args.force || false, // force { mcpLog: mockLogger, session: options.session, projectRoot: args.projectRoot } ); mockDisableSilentMode(); return { success: true, data: { message: `Expand all operation completed. Expanded: ${result.expandedCount}, Failed: ${result.failedCount}, Skipped: ${result.skippedCount}`, details: { expandedCount: result.expandedCount, failedCount: result.failedCount, skippedCount: result.skippedCount, tasksToExpand: result.tasksToExpand }, telemetryData: result.telemetryData } }; } test('should expand all pending tasks with subtasks', async () => { // Arrange const args = { projectRoot: testProjectRoot, file: testTasksPath, num: 3 }; // Act const result = await testExpandAllTasks(args, mockLogger, { session: mockSession }); // Assert expect(result.success).toBe(true); expect(result.data.message).toMatch(/Expand all operation completed/); expect(result.data.details.expandedCount).toBe(2); expect(result.data.details.failedCount).toBe(0); expect(result.data.details.skippedCount).toBe(1); expect(result.data.details.tasksToExpand).toBe(3); expect(result.data.telemetryData).toBeDefined(); expect(result.data.telemetryData.commandName).toBe('expand-all-tasks'); expect(mockEnableSilentMode).toHaveBeenCalled(); expect(mockDisableSilentMode).toHaveBeenCalled(); }); test('should handle research flag', async () => { // Arrange const args = { projectRoot: testProjectRoot, file: testTasksPath, research: true, num: 2 }; // Act const result = await testExpandAllTasks(args, mockLogger, { session: mockSession }); // Assert expect(result.success).toBe(true); expect(result.data.details.expandedCount).toBe(2); expect(result.data.telemetryData).toBeDefined(); expect(mockEnableSilentMode).toHaveBeenCalled(); expect(mockDisableSilentMode).toHaveBeenCalled(); }); test('should handle force flag', async () => { // Arrange const args = { projectRoot: testProjectRoot, file: testTasksPath, force: true }; // Act const result = await testExpandAllTasks(args, mockLogger, { session: mockSession }); // Assert expect(result.success).toBe(true); expect(result.data.details.expandedCount).toBe(2); expect(result.data.telemetryData).toBeDefined(); expect(mockEnableSilentMode).toHaveBeenCalled(); expect(mockDisableSilentMode).toHaveBeenCalled(); }); test('should handle additional context/prompt', async () => { // Arrange const args = { projectRoot: testProjectRoot, file: testTasksPath, prompt: 'Additional context for subtasks' }; // Act const result = await testExpandAllTasks(args, mockLogger, { session: mockSession }); // Assert expect(result.success).toBe(true); expect(result.data.details.expandedCount).toBe(2); expect(result.data.telemetryData).toBeDefined(); expect(mockEnableSilentMode).toHaveBeenCalled(); expect(mockDisableSilentMode).toHaveBeenCalled(); }); test('should handle case with no eligible tasks', async () => { // Arrange const args = { projectRoot: testProjectRoot, file: testTasksPath, num: 3 }; // Act - Mock the scenario where no tasks are eligible for expansion async function testNoEligibleTasks(args, mockLogger, options = {}) { mockEnableSilentMode(); const mockExpandAll = jest.fn().mockImplementation(async () => { return { success: true, expandedCount: 0, failedCount: 0, skippedCount: 0, tasksToExpand: 0, telemetryData: null, message: 'No tasks eligible for expansion.' }; }); const result = await mockExpandAll( args.file, args.num, false, '', false, { mcpLog: mockLogger, session: options.session, projectRoot: args.projectRoot }, 'json' ); mockDisableSilentMode(); return { success: true, data: { message: result.message, details: { expandedCount: result.expandedCount, failedCount: result.failedCount, skippedCount: result.skippedCount, tasksToExpand: result.tasksToExpand }, telemetryData: result.telemetryData } }; } const result = await testNoEligibleTasks(args, mockLogger, { session: mockSession }); // Assert expect(result.success).toBe(true); expect(result.data.message).toBe('No tasks eligible for expansion.'); expect(result.data.details.expandedCount).toBe(0); expect(result.data.details.tasksToExpand).toBe(0); expect(result.data.telemetryData).toBeNull(); }); }); });