mirror of
https://github.com/eyaltoledano/claude-task-master.git
synced 2025-07-03 07:04:28 +00:00
310 lines
8.2 KiB
JavaScript
310 lines
8.2 KiB
JavaScript
![]() |
/**
|
||
|
* Tests for the addSubtask function
|
||
|
*/
|
||
|
import { jest } from '@jest/globals';
|
||
|
import path from 'path';
|
||
|
|
||
|
// Mock dependencies
|
||
|
const mockReadJSON = jest.fn();
|
||
|
const mockWriteJSON = jest.fn();
|
||
|
const mockGenerateTaskFiles = jest.fn();
|
||
|
const mockIsTaskDependentOn = jest.fn().mockReturnValue(false);
|
||
|
|
||
|
// Mock path module
|
||
|
jest.mock('path', () => ({
|
||
|
dirname: jest.fn()
|
||
|
}));
|
||
|
|
||
|
// Define test version of the addSubtask function
|
||
|
const testAddSubtask = (
|
||
|
tasksPath,
|
||
|
parentId,
|
||
|
existingTaskId,
|
||
|
newSubtaskData,
|
||
|
generateFiles = true
|
||
|
) => {
|
||
|
// Read the existing tasks
|
||
|
const data = mockReadJSON(tasksPath);
|
||
|
if (!data || !data.tasks) {
|
||
|
throw new Error(`Invalid or missing tasks file at ${tasksPath}`);
|
||
|
}
|
||
|
|
||
|
// Convert parent ID to number
|
||
|
const parentIdNum = parseInt(parentId, 10);
|
||
|
|
||
|
// Find the parent task
|
||
|
const parentTask = data.tasks.find((t) => t.id === parentIdNum);
|
||
|
if (!parentTask) {
|
||
|
throw new Error(`Parent task with ID ${parentIdNum} not found`);
|
||
|
}
|
||
|
|
||
|
// Initialize subtasks array if it doesn't exist
|
||
|
if (!parentTask.subtasks) {
|
||
|
parentTask.subtasks = [];
|
||
|
}
|
||
|
|
||
|
let newSubtask;
|
||
|
|
||
|
// Case 1: Convert an existing task to a subtask
|
||
|
if (existingTaskId !== null) {
|
||
|
const existingTaskIdNum = parseInt(existingTaskId, 10);
|
||
|
|
||
|
// Find the existing task
|
||
|
const existingTaskIndex = data.tasks.findIndex(
|
||
|
(t) => t.id === existingTaskIdNum
|
||
|
);
|
||
|
if (existingTaskIndex === -1) {
|
||
|
throw new Error(`Task with ID ${existingTaskIdNum} not found`);
|
||
|
}
|
||
|
|
||
|
const existingTask = data.tasks[existingTaskIndex];
|
||
|
|
||
|
// Check if task is already a subtask
|
||
|
if (existingTask.parentTaskId) {
|
||
|
throw new Error(
|
||
|
`Task ${existingTaskIdNum} is already a subtask of task ${existingTask.parentTaskId}`
|
||
|
);
|
||
|
}
|
||
|
|
||
|
// Check for circular dependency
|
||
|
if (existingTaskIdNum === parentIdNum) {
|
||
|
throw new Error(`Cannot make a task a subtask of itself`);
|
||
|
}
|
||
|
|
||
|
// Check for circular dependency using mockIsTaskDependentOn
|
||
|
if (mockIsTaskDependentOn()) {
|
||
|
throw new Error(
|
||
|
`Cannot create circular dependency: task ${parentIdNum} is already a subtask or dependent of task ${existingTaskIdNum}`
|
||
|
);
|
||
|
}
|
||
|
|
||
|
// Find the highest subtask ID to determine the next ID
|
||
|
const highestSubtaskId =
|
||
|
parentTask.subtasks.length > 0
|
||
|
? Math.max(...parentTask.subtasks.map((st) => st.id))
|
||
|
: 0;
|
||
|
const newSubtaskId = highestSubtaskId + 1;
|
||
|
|
||
|
// Clone the existing task to be converted to a subtask
|
||
|
newSubtask = {
|
||
|
...existingTask,
|
||
|
id: newSubtaskId,
|
||
|
parentTaskId: parentIdNum
|
||
|
};
|
||
|
|
||
|
// Add to parent's subtasks
|
||
|
parentTask.subtasks.push(newSubtask);
|
||
|
|
||
|
// Remove the task from the main tasks array
|
||
|
data.tasks.splice(existingTaskIndex, 1);
|
||
|
}
|
||
|
// Case 2: Create a new subtask
|
||
|
else if (newSubtaskData) {
|
||
|
// Find the highest subtask ID to determine the next ID
|
||
|
const highestSubtaskId =
|
||
|
parentTask.subtasks.length > 0
|
||
|
? Math.max(...parentTask.subtasks.map((st) => st.id))
|
||
|
: 0;
|
||
|
const newSubtaskId = highestSubtaskId + 1;
|
||
|
|
||
|
// Create the new subtask object
|
||
|
newSubtask = {
|
||
|
id: newSubtaskId,
|
||
|
title: newSubtaskData.title,
|
||
|
description: newSubtaskData.description || '',
|
||
|
details: newSubtaskData.details || '',
|
||
|
status: newSubtaskData.status || 'pending',
|
||
|
dependencies: newSubtaskData.dependencies || [],
|
||
|
parentTaskId: parentIdNum
|
||
|
};
|
||
|
|
||
|
// Add to parent's subtasks
|
||
|
parentTask.subtasks.push(newSubtask);
|
||
|
} else {
|
||
|
throw new Error('Either existingTaskId or newSubtaskData must be provided');
|
||
|
}
|
||
|
|
||
|
// Write the updated tasks back to the file
|
||
|
mockWriteJSON(tasksPath, data);
|
||
|
|
||
|
// Generate task files if requested
|
||
|
if (generateFiles) {
|
||
|
mockGenerateTaskFiles(tasksPath, path.dirname(tasksPath));
|
||
|
}
|
||
|
|
||
|
return newSubtask;
|
||
|
};
|
||
|
|
||
|
describe('addSubtask function', () => {
|
||
|
// Reset mocks before each test
|
||
|
beforeEach(() => {
|
||
|
jest.clearAllMocks();
|
||
|
|
||
|
// Default mock implementations
|
||
|
mockReadJSON.mockImplementation(() => ({
|
||
|
tasks: [
|
||
|
{
|
||
|
id: 1,
|
||
|
title: 'Parent Task',
|
||
|
description: 'This is a parent task',
|
||
|
status: 'pending',
|
||
|
dependencies: []
|
||
|
},
|
||
|
{
|
||
|
id: 2,
|
||
|
title: 'Existing Task',
|
||
|
description: 'This is an existing task',
|
||
|
status: 'pending',
|
||
|
dependencies: []
|
||
|
},
|
||
|
{
|
||
|
id: 3,
|
||
|
title: 'Another Task',
|
||
|
description: 'This is another task',
|
||
|
status: 'pending',
|
||
|
dependencies: [1]
|
||
|
}
|
||
|
]
|
||
|
}));
|
||
|
|
||
|
// Setup success write response
|
||
|
mockWriteJSON.mockImplementation((path, data) => {
|
||
|
return data;
|
||
|
});
|
||
|
|
||
|
// Set up default behavior for dependency check
|
||
|
mockIsTaskDependentOn.mockReturnValue(false);
|
||
|
});
|
||
|
|
||
|
test('should add a new subtask to a parent task', async () => {
|
||
|
// Create new subtask data
|
||
|
const newSubtaskData = {
|
||
|
title: 'New Subtask',
|
||
|
description: 'This is a new subtask',
|
||
|
details: 'Implementation details for the subtask',
|
||
|
status: 'pending',
|
||
|
dependencies: []
|
||
|
};
|
||
|
|
||
|
// Execute the test version of addSubtask
|
||
|
const newSubtask = testAddSubtask(
|
||
|
'tasks/tasks.json',
|
||
|
1,
|
||
|
null,
|
||
|
newSubtaskData,
|
||
|
true
|
||
|
);
|
||
|
|
||
|
// Verify readJSON was called with the correct path
|
||
|
expect(mockReadJSON).toHaveBeenCalledWith('tasks/tasks.json');
|
||
|
|
||
|
// Verify writeJSON was called with the correct path
|
||
|
expect(mockWriteJSON).toHaveBeenCalledWith(
|
||
|
'tasks/tasks.json',
|
||
|
expect.any(Object)
|
||
|
);
|
||
|
|
||
|
// Verify the subtask was created with correct data
|
||
|
expect(newSubtask).toBeDefined();
|
||
|
expect(newSubtask.id).toBe(1);
|
||
|
expect(newSubtask.title).toBe('New Subtask');
|
||
|
expect(newSubtask.parentTaskId).toBe(1);
|
||
|
|
||
|
// Verify generateTaskFiles was called
|
||
|
expect(mockGenerateTaskFiles).toHaveBeenCalled();
|
||
|
});
|
||
|
|
||
|
test('should convert an existing task to a subtask', async () => {
|
||
|
// Execute the test version of addSubtask to convert task 2 to a subtask of task 1
|
||
|
const convertedSubtask = testAddSubtask(
|
||
|
'tasks/tasks.json',
|
||
|
1,
|
||
|
2,
|
||
|
null,
|
||
|
true
|
||
|
);
|
||
|
|
||
|
// Verify readJSON was called with the correct path
|
||
|
expect(mockReadJSON).toHaveBeenCalledWith('tasks/tasks.json');
|
||
|
|
||
|
// Verify writeJSON was called
|
||
|
expect(mockWriteJSON).toHaveBeenCalled();
|
||
|
|
||
|
// Verify the subtask was created with correct data
|
||
|
expect(convertedSubtask).toBeDefined();
|
||
|
expect(convertedSubtask.id).toBe(1);
|
||
|
expect(convertedSubtask.title).toBe('Existing Task');
|
||
|
expect(convertedSubtask.parentTaskId).toBe(1);
|
||
|
|
||
|
// Verify generateTaskFiles was called
|
||
|
expect(mockGenerateTaskFiles).toHaveBeenCalled();
|
||
|
});
|
||
|
|
||
|
test('should throw an error if parent task does not exist', async () => {
|
||
|
// Create new subtask data
|
||
|
const newSubtaskData = {
|
||
|
title: 'New Subtask',
|
||
|
description: 'This is a new subtask'
|
||
|
};
|
||
|
|
||
|
// Override mockReadJSON for this specific test case
|
||
|
mockReadJSON.mockImplementationOnce(() => ({
|
||
|
tasks: [
|
||
|
{
|
||
|
id: 1,
|
||
|
title: 'Task 1',
|
||
|
status: 'pending'
|
||
|
}
|
||
|
]
|
||
|
}));
|
||
|
|
||
|
// Expect an error when trying to add a subtask to a non-existent parent
|
||
|
expect(() =>
|
||
|
testAddSubtask('tasks/tasks.json', 999, null, newSubtaskData)
|
||
|
).toThrow(/Parent task with ID 999 not found/);
|
||
|
|
||
|
// Verify writeJSON was not called
|
||
|
expect(mockWriteJSON).not.toHaveBeenCalled();
|
||
|
});
|
||
|
|
||
|
test('should throw an error if existing task does not exist', async () => {
|
||
|
// Expect an error when trying to convert a non-existent task
|
||
|
expect(() => testAddSubtask('tasks/tasks.json', 1, 999, null)).toThrow(
|
||
|
/Task with ID 999 not found/
|
||
|
);
|
||
|
|
||
|
// Verify writeJSON was not called
|
||
|
expect(mockWriteJSON).not.toHaveBeenCalled();
|
||
|
});
|
||
|
|
||
|
test('should throw an error if trying to create a circular dependency', async () => {
|
||
|
// Force the isTaskDependentOn mock to return true for this test only
|
||
|
mockIsTaskDependentOn.mockReturnValueOnce(true);
|
||
|
|
||
|
// Expect an error when trying to create a circular dependency
|
||
|
expect(() => testAddSubtask('tasks/tasks.json', 3, 1, null)).toThrow(
|
||
|
/circular dependency/
|
||
|
);
|
||
|
|
||
|
// Verify writeJSON was not called
|
||
|
expect(mockWriteJSON).not.toHaveBeenCalled();
|
||
|
});
|
||
|
|
||
|
test('should not regenerate task files if generateFiles is false', async () => {
|
||
|
// Create new subtask data
|
||
|
const newSubtaskData = {
|
||
|
title: 'New Subtask',
|
||
|
description: 'This is a new subtask'
|
||
|
};
|
||
|
|
||
|
// Execute the test version of addSubtask with generateFiles = false
|
||
|
testAddSubtask('tasks/tasks.json', 1, null, newSubtaskData, false);
|
||
|
|
||
|
// Verify writeJSON was called
|
||
|
expect(mockWriteJSON).toHaveBeenCalled();
|
||
|
|
||
|
// Verify task files were not regenerated
|
||
|
expect(mockGenerateTaskFiles).not.toHaveBeenCalled();
|
||
|
});
|
||
|
});
|