Files
claude-task-master/tests/unit/scripts/modules/task-manager/clear-subtasks.test.js
Eyal Toledano d5360f625f fix: resolve all remaining test failures and improve test reliability
- Fix clear-subtasks test by implementing deep copy of mock data to prevent mutation issues between tests
- Fix add-task test by uncommenting and properly configuring generateTaskFiles call with correct parameters
- Fix analyze-task-complexity tests by properly mocking fs.writeFileSync with shared mock function
- Update test expectations to match actual function signatures and data structures
- Improve mock setup consistency across all test suites
- Ensure all tests now pass (329 total: 318 passed, 11 skipped, 0 failed)
2025-06-13 22:31:24 -04:00

302 lines
7.6 KiB
JavaScript

/**
* Tests for the clear-subtasks.js module
*/
import { jest } from '@jest/globals';
// Mock the dependencies before importing the module under test
jest.unstable_mockModule('../../../../../scripts/modules/utils.js', () => ({
readJSON: jest.fn(),
writeJSON: jest.fn(),
log: jest.fn(),
CONFIG: {
model: 'mock-claude-model',
maxTokens: 4000,
temperature: 0.7,
debug: false
},
findTaskById: jest.fn(),
isSilentMode: jest.fn(() => false),
truncate: jest.fn((text) => text),
ensureTagMetadata: jest.fn()
}));
jest.unstable_mockModule('../../../../../scripts/modules/ui.js', () => ({
displayBanner: jest.fn()
}));
jest.unstable_mockModule(
'../../../../../scripts/modules/task-manager/generate-task-files.js',
() => ({
default: jest.fn().mockResolvedValue()
})
);
// Mock external UI libraries
jest.unstable_mockModule('chalk', () => ({
default: {
white: {
bold: jest.fn((text) => text)
},
cyan: Object.assign(
jest.fn((text) => text),
{
bold: jest.fn((text) => text)
}
),
green: jest.fn((text) => text),
yellow: jest.fn((text) => text),
bold: jest.fn((text) => text)
}
}));
jest.unstable_mockModule('boxen', () => ({
default: jest.fn((text) => text)
}));
jest.unstable_mockModule('cli-table3', () => ({
default: jest.fn().mockImplementation(() => ({
push: jest.fn(),
toString: jest.fn(() => 'mocked table')
}))
}));
// Mock process.exit to prevent Jest worker crashes
const mockExit = jest.spyOn(process, 'exit').mockImplementation((code) => {
throw new Error(`process.exit called with "${code}"`);
});
// Import the mocked modules
const { readJSON, writeJSON, log, findTaskById, ensureTagMetadata } =
await import('../../../../../scripts/modules/utils.js');
const generateTaskFiles = (
await import(
'../../../../../scripts/modules/task-manager/generate-task-files.js'
)
).default;
// Import the module under test
const { default: clearSubtasks } = await import(
'../../../../../scripts/modules/task-manager/clear-subtasks.js'
);
describe('clearSubtasks', () => {
const sampleTasks = {
master: {
tasks: [
{ id: 1, title: 'Task 1', subtasks: [] },
{ id: 2, title: 'Task 2', subtasks: [] },
{
id: 3,
title: 'Task 3',
subtasks: [{ id: 1, title: 'Subtask 3.1' }]
},
{
id: 4,
title: 'Task 4',
subtasks: [{ id: 1, title: 'Subtask 4.1' }]
}
]
}
};
beforeEach(() => {
jest.clearAllMocks();
mockExit.mockClear();
readJSON.mockImplementation((tasksPath, projectRoot, tag) => {
// Create a deep copy to avoid mutation issues between tests
const sampleTasksCopy = JSON.parse(JSON.stringify(sampleTasks));
// Return the data for the 'master' tag, which is what the tests use
return {
...sampleTasksCopy.master,
tag: tag || 'master',
_rawTaggedData: sampleTasksCopy
};
});
writeJSON.mockResolvedValue();
generateTaskFiles.mockResolvedValue();
log.mockImplementation(() => {});
});
test('should clear subtasks from a specific task', () => {
// Arrange
const taskId = '3';
const tasksPath = 'tasks/tasks.json';
// Act
clearSubtasks(tasksPath, taskId);
// Assert
expect(readJSON).toHaveBeenCalledWith(tasksPath, undefined, undefined);
expect(writeJSON).toHaveBeenCalledWith(
tasksPath,
expect.objectContaining({
_rawTaggedData: expect.objectContaining({
master: expect.objectContaining({
tasks: expect.arrayContaining([
expect.objectContaining({
id: 3,
subtasks: [] // Should be empty
})
])
})
})
}),
undefined,
undefined
);
expect(generateTaskFiles).toHaveBeenCalledWith(tasksPath, 'tasks', {
projectRoot: undefined,
tag: undefined
});
});
test('should clear subtasks from multiple tasks when given comma-separated IDs', () => {
// Arrange
const taskIds = '3,4';
const tasksPath = 'tasks/tasks.json';
// Act
clearSubtasks(tasksPath, taskIds);
// Assert
expect(readJSON).toHaveBeenCalledWith(tasksPath, undefined, undefined);
expect(writeJSON).toHaveBeenCalledWith(
tasksPath,
expect.objectContaining({
_rawTaggedData: expect.objectContaining({
master: expect.objectContaining({
tasks: expect.arrayContaining([
expect.objectContaining({ id: 3, subtasks: [] }),
expect.objectContaining({ id: 4, subtasks: [] })
])
})
})
}),
undefined,
undefined
);
expect(generateTaskFiles).toHaveBeenCalledWith(tasksPath, 'tasks', {
projectRoot: undefined,
tag: undefined
});
});
test('should handle tasks with no subtasks', () => {
// Arrange
const taskId = '1'; // Task 1 already has no subtasks
const tasksPath = 'tasks/tasks.json';
// Act
clearSubtasks(tasksPath, taskId);
// Assert
expect(readJSON).toHaveBeenCalledWith(tasksPath, undefined, undefined);
// Should not write the file if no changes were made
expect(writeJSON).not.toHaveBeenCalled();
expect(generateTaskFiles).not.toHaveBeenCalled();
});
test('should handle non-existent task IDs gracefully', () => {
// Arrange
const taskId = '99'; // Non-existent task
const tasksPath = 'tasks/tasks.json';
// Act
clearSubtasks(tasksPath, taskId);
// Assert
expect(readJSON).toHaveBeenCalledWith(tasksPath, undefined, undefined);
expect(log).toHaveBeenCalledWith('error', 'Task 99 not found');
// Should not write the file if no changes were made
expect(writeJSON).not.toHaveBeenCalled();
expect(generateTaskFiles).not.toHaveBeenCalled();
});
test('should handle multiple task IDs including both valid and non-existent IDs', () => {
// Arrange
const taskIds = '3,99'; // Mix of valid and invalid IDs
const tasksPath = 'tasks/tasks.json';
// Act
clearSubtasks(tasksPath, taskIds);
// Assert
expect(readJSON).toHaveBeenCalledWith(tasksPath, undefined, undefined);
expect(log).toHaveBeenCalledWith('error', 'Task 99 not found');
// Since task 3 has subtasks that should be cleared, writeJSON should be called
expect(writeJSON).toHaveBeenCalledWith(
tasksPath,
expect.objectContaining({
tasks: expect.arrayContaining([
expect.objectContaining({ id: 3, subtasks: [] })
]),
tag: 'master',
_rawTaggedData: expect.objectContaining({
master: expect.objectContaining({
tasks: expect.arrayContaining([
expect.objectContaining({ id: 3, subtasks: [] })
])
})
})
}),
undefined,
undefined
);
expect(generateTaskFiles).toHaveBeenCalledWith(tasksPath, 'tasks', {
projectRoot: undefined,
tag: undefined
});
});
test('should handle file read errors', () => {
// Arrange
readJSON.mockImplementation(() => {
throw new Error('File read failed');
});
// Act & Assert
expect(() => {
clearSubtasks('tasks/tasks.json', '3');
}).toThrow('File read failed');
});
test('should handle invalid tasks data', () => {
// Arrange
readJSON.mockReturnValue(null);
// Act & Assert
expect(() => {
clearSubtasks('tasks/tasks.json', '3');
}).toThrow('process.exit called');
expect(log).toHaveBeenCalledWith('error', 'No valid tasks found.');
});
test('should handle file write errors', () => {
// Arrange
// Ensure task 3 has subtasks to clear so writeJSON gets called
readJSON.mockReturnValue({
...sampleTasks.master,
tag: 'master',
_rawTaggedData: sampleTasks,
tasks: [
...sampleTasks.master.tasks.slice(0, 2),
{
...sampleTasks.master.tasks[2],
subtasks: [{ id: 1, title: 'Subtask to clear' }]
},
...sampleTasks.master.tasks.slice(3)
]
});
writeJSON.mockImplementation(() => {
throw new Error('File write failed');
});
// Act & Assert
expect(() => {
clearSubtasks('tasks/tasks.json', '3');
}).toThrow('File write failed');
});
});