UNPKG

lamplighter-mcp

Version:

An intelligent context engine for AI-assisted software development

196 lines (167 loc) 8.97 kB
import * as fs from 'fs/promises'; import * as path from 'path'; import { TaskManager, TaskStatus } from '../../src/modules/taskManager'; // Mock fs/promises jest.mock('fs/promises'); const mockedFs = jest.mocked(fs); // Save original environment variables const originalEnv = process.env; describe('TaskManager', () => { const defaultContextDir = './lamplighter_context'; const featureTasksDir = path.join(defaultContextDir, 'feature_tasks'); let taskManager: TaskManager; const featureId = 'test-feature'; const mockFilePath = path.join(featureTasksDir, `feature_${featureId}_tasks.md`); beforeEach(() => { jest.clearAllMocks(); // Mock environment variables process.env = { ...originalEnv, LAMPLIGHTER_CONTEXT_DIR: defaultContextDir }; // Default fs mocks mockedFs.access.mockResolvedValue(undefined); // Assume file exists mockedFs.readFile.mockResolvedValue(''); // Default empty content mockedFs.writeFile.mockResolvedValue(undefined); taskManager = new TaskManager(); }); afterAll(() => { process.env = originalEnv; }); describe('parseTasks', () => { // Access private method for testing const parseTasks = (content: string) => (taskManager as any).parseTasks(content); it('should parse ToDo and Done tasks', () => { const content = '# Title\n- [ ] Task 1\n- [x] Task 2\nSome other text\n- [ ] Task 3'; const tasks = parseTasks(content); expect(tasks).toHaveLength(3); expect(tasks[0]).toMatchObject({ text: 'Task 1', status: 'ToDo', lineNumber: 1 }); expect(tasks[1]).toMatchObject({ text: 'Task 2', status: 'Done', lineNumber: 2 }); expect(tasks[2]).toMatchObject({ text: 'Task 3', status: 'ToDo', lineNumber: 4 }); }); it('should handle variations in spacing and case for Done marker', () => { const content = '- [ ] Task A\n- [x] Task B \n- [X] Task C'; const tasks = parseTasks(content); expect(tasks).toHaveLength(3); expect(tasks[0].status).toBe('ToDo'); expect(tasks[1].status).toBe('Done'); expect(tasks[1].text).toBe('Task B'); // Check trimming expect(tasks[2].status).toBe('Done'); }); it('should return empty array for content with no tasks', () => { const content = 'No tasks here.'; expect(parseTasks(content)).toEqual([]); }); }); describe('updateTaskInContent', () => { // Access private method for testing const updateTaskInContent = (content: string, task: any, status: TaskStatus) => (taskManager as any).updateTaskInContent(content, task, status); const taskLine = '- [ ] Initial task'; const content = `# Header\n${taskLine}\n- [x] Another task`; const mockTask = { text: 'Initial task', status: 'ToDo', lineNumber: 1, originalLine: taskLine }; it('should update ToDo to Done', () => { const updated = updateTaskInContent(content, mockTask, 'Done'); expect(updated).toContain('- [x] Initial task'); expect(updated).toContain('- [x] Another task'); // Ensure other lines are preserved }); it('should update Done to ToDo', () => { const doneTaskLine = '- [x] Done task'; const doneContent = `# Header\n${doneTaskLine}`; const doneTask = { text: 'Done task', status: 'Done', lineNumber: 1, originalLine: doneTaskLine }; const updated = updateTaskInContent(doneContent, doneTask, 'ToDo'); expect(updated).toContain('- [ ] Done task'); }); it('should update ToDo to InProgress (treated as ToDo)', () => { const updated = updateTaskInContent(content, mockTask, 'InProgress'); expect(updated).toContain('- [ ] Initial task'); // Should remain [ ] }); }); describe('updateTaskStatus', () => { const task1 = 'First Task'; const task2 = 'Second Task'; const initialContent = `# Tasks\n- [ ] ${task1}\n- [ ] ${task2}\n- [x] Already Done`; beforeEach(() => { mockedFs.readFile.mockResolvedValue(initialContent); }); it('should update task status from ToDo to Done by exact match', async () => { await taskManager.updateTaskStatus(featureId, task1, 'Done'); expect(mockedFs.writeFile).toHaveBeenCalledTimes(1); expect(mockedFs.writeFile).toHaveBeenCalledWith(mockFilePath, expect.stringContaining(`- [x] ${task1}`), 'utf-8'); expect(mockedFs.writeFile).toHaveBeenCalledWith(mockFilePath, expect.not.stringContaining(`- [ ] ${task1}`), 'utf-8'); expect(mockedFs.writeFile).toHaveBeenCalledWith(mockFilePath, expect.stringContaining(`- [ ] ${task2}`), 'utf-8'); // Check other task unchanged }); it('should update task status from ToDo to Done by partial match', async () => { const partialIdentifier = 'Second'; // Partial match for task2 await taskManager.updateTaskStatus(featureId, partialIdentifier, 'Done'); expect(mockedFs.writeFile).toHaveBeenCalledTimes(1); expect(mockedFs.writeFile).toHaveBeenCalledWith(mockFilePath, expect.stringContaining(`- [x] ${task2}`), 'utf-8'); expect(mockedFs.writeFile).toHaveBeenCalledWith(mockFilePath, expect.not.stringContaining(`- [ ] ${task2}`), 'utf-8'); expect(mockedFs.writeFile).toHaveBeenCalledWith(mockFilePath, expect.stringContaining(`- [ ] ${task1}`), 'utf-8'); }); it('should update task status from Done to ToDo', async () => { const taskToUpdate = 'Already Done'; await taskManager.updateTaskStatus(featureId, taskToUpdate, 'ToDo'); expect(mockedFs.writeFile).toHaveBeenCalledTimes(1); expect(mockedFs.writeFile).toHaveBeenCalledWith(mockFilePath, expect.stringContaining(`- [ ] ${taskToUpdate}`), 'utf-8'); expect(mockedFs.writeFile).toHaveBeenCalledWith(mockFilePath, expect.not.stringContaining(`- [x] ${taskToUpdate}`), 'utf-8'); }); it('should throw error if task file not found', async () => { mockedFs.access.mockRejectedValue(new Error('File not found')); await expect(taskManager.updateTaskStatus(featureId, task1, 'Done')) .rejects.toThrow(`Task file for feature "${featureId}" not found.`); expect(mockedFs.readFile).not.toHaveBeenCalled(); expect(mockedFs.writeFile).not.toHaveBeenCalled(); }); it('should throw error if task identifier not found', async () => { const nonExistentTask = 'Missing Task'; await expect(taskManager.updateTaskStatus(featureId, nonExistentTask, 'Done')) .rejects.toThrow(`Task "${nonExistentTask}" not found in feature "${featureId}".`); expect(mockedFs.writeFile).not.toHaveBeenCalled(); }); it('should handle readFile error', async () => { const readError = new Error('Read permission denied'); mockedFs.readFile.mockRejectedValue(readError); await expect(taskManager.updateTaskStatus(featureId, task1, 'Done')) .rejects.toThrow(`Failed to update task status: ${readError.message}`); }); it('should handle writeFile error', async () => { const writeError = new Error('Write permission denied'); mockedFs.writeFile.mockRejectedValue(writeError); await expect(taskManager.updateTaskStatus(featureId, task1, 'Done')) .rejects.toThrow(`Failed to update task status: ${writeError.message}`); }); }); describe('suggestNextTask', () => { it('should suggest the first ToDo task', async () => { const content = '- [x] Done 1\n- [ ] First ToDo\n- [ ] Second ToDo'; mockedFs.readFile.mockResolvedValue(content); const nextTask = await taskManager.suggestNextTask(featureId); expect(nextTask).toBe('First ToDo'); expect(mockedFs.readFile).toHaveBeenCalledWith(mockFilePath, 'utf-8'); }); it('should return null if no ToDo tasks are found', async () => { const content = '- [x] Done 1\n- [x] Done 2'; mockedFs.readFile.mockResolvedValue(content); const nextTask = await taskManager.suggestNextTask(featureId); expect(nextTask).toBeNull(); }); it('should return null if the file is empty or has no tasks', async () => { mockedFs.readFile.mockResolvedValue('# Empty File'); const nextTask = await taskManager.suggestNextTask(featureId); expect(nextTask).toBeNull(); }); it('should throw error if task file not found', async () => { mockedFs.access.mockRejectedValue(new Error('File not found')); await expect(taskManager.suggestNextTask(featureId)) .rejects.toThrow(`Task file for feature "${featureId}" not found.`); expect(mockedFs.readFile).not.toHaveBeenCalled(); }); it('should handle readFile error', async () => { const readError = new Error('Read permission denied'); mockedFs.readFile.mockRejectedValue(readError); await expect(taskManager.suggestNextTask(featureId)) .rejects.toThrow(`Failed to suggest next task: ${readError.message}`); }); }); });