claude-task-master/tests/unit/mcp/tools/remove-task.test.js
Parthy 2852149a47
fix: Critical writeJSON Context Fixes - Prevent Tag Corruption (#910)
* feat(tasks): Fix critical tag corruption bug in task management

- Fixed missing context parameters in writeJSON calls across add-task, remove-task, and add-subtask functions
- Added projectRoot and tag parameters to prevent data corruption in multi-tag environments
- Re-enabled generateTaskFiles calls to ensure markdown files are updated after operations
- Enhanced add_subtask MCP tool with tag parameter support
- Refactored addSubtaskDirect function to properly pass context to core logic
- Streamlined codebase by removing deprecated functionality

This resolves the critical bug where task operations in one tag context would corrupt or delete tasks from other tags in tasks.json.

* feat(task-manager): Enhance addSubtask with current tag support

- Added `getCurrentTag` utility to retrieve the current tag context for task operations.
- Updated `addSubtask` to use the current tag when reading and writing tasks, ensuring proper context handling.
- Refactored tests to accommodate changes in the `addSubtask` function, ensuring accurate mock implementations and expectations.
- Cleaned up test cases for better readability and maintainability.

This improves task management by preventing tag-related data corruption and enhances the overall functionality of the task manager.

* feat(remove-task): Add tag support for task removal and enhance error handling

- Introduced `tag` parameter in `removeTaskDirect` to specify context for task operations, improving multi-tag support.
- Updated logging to include tag context in messages for better traceability.
- Refactored task removal logic to streamline the process and improve error reporting.
- Added comprehensive unit tests to validate tag handling and ensure robust error management.

This enhancement prevents task data corruption across different tags and improves the overall reliability of the task management system.

* feat(add-task): Add projectRoot and tag parameters to addTask tests

- Updated `addTask` unit tests to include `projectRoot` and `tag` parameters for better context handling.
- Enhanced test cases to ensure accurate expectations and improve overall test coverage.

This change aligns with recent enhancements in task management, ensuring consistency across task operations.

* feat(set-task-status): Add tag parameter support and enhance task status handling

- Introduced `tag` parameter in `setTaskStatusDirect` and related functions to improve context management in multi-tag environments.
- Updated `writeJSON` calls to ensure task data integrity across different tags.
- Enhanced unit tests to validate tag preservation during task status updates, ensuring robust functionality.

This change aligns with recent improvements in task management, preventing data corruption and enhancing overall reliability.

* feat(tag-management): Enhance writeJSON calls to preserve tag context

- Updated `writeJSON` calls in `createTag`, `deleteTag`, `renameTag`, `copyTag`, and `enhanceTagsWithMetadata` to include `projectRoot` for better context management and to prevent tag corruption.
- Added comprehensive unit tests for tag management functions to ensure data integrity and proper tag handling during operations.

This change improves the reliability of tag management by ensuring that operations do not corrupt existing tags and maintains the overall structure of the task data.

* feat(expand-task): Update writeJSON to include projectRoot and tag context

- Modified `writeJSON` call in `expandTaskDirect` to pass `projectRoot` and `tag` parameters, ensuring proper context management when saving tasks.json.
- This change aligns with recent enhancements in task management, preventing potential data corruption and improving overall reliability.

* feat(fix-dependencies): Add projectRoot and tag parameters for enhanced context management

- Updated `fixDependenciesDirect` and `registerFixDependenciesTool` to include `projectRoot` and `tag` parameters, improving context handling during dependency fixes.
- Introduced a new unit test for `fixDependenciesCommand` to ensure proper preservation of projectRoot and tag data in JSON outputs.

This change enhances the reliability of dependency management by ensuring that context is maintained across operations, preventing potential data issues.

* fix(context): propagate projectRoot and tag through dependency, expansion, status-update and tag-management commands to prevent cross-tag data corruption

* test(fix-dependencies): Enhance unit tests for fixDependenciesCommand

- Refactored tests to use unstable mocks for utils, ui, and task-manager modules, improving isolation and reliability.
- Added checks for process.exit to ensure proper handling of invalid data scenarios.
- Updated test cases to verify writeJSON calls with projectRoot and tag parameters, ensuring accurate context preservation during dependency fixes.

This change strengthens the test suite for dependency management, ensuring robust functionality and preventing potential data issues.

* chore(plan): remove outdated fix plan for `writeJSON` context parameters
2025-07-02 21:45:10 +02:00

529 lines
13 KiB
JavaScript

/**
* Tests for the remove-task MCP tool
*
* Note: This test does NOT test the actual implementation. It tests that:
* 1. The tool is registered correctly with the correct parameters
* 2. Arguments are passed correctly to removeTaskDirect
* 3. Error handling works as expected
* 4. Tag parameter is properly handled and passed through
*
* We do NOT import the real implementation - everything is mocked
*/
import { jest } from '@jest/globals';
// Mock EVERYTHING
const mockRemoveTaskDirect = jest.fn();
jest.mock('../../../../mcp-server/src/core/task-master-core.js', () => ({
removeTaskDirect: mockRemoveTaskDirect
}));
const mockHandleApiResult = jest.fn((result) => result);
const mockWithNormalizedProjectRoot = jest.fn((fn) => fn);
const mockCreateErrorResponse = jest.fn((msg) => ({
success: false,
error: { code: 'ERROR', message: msg }
}));
const mockFindTasksPath = jest.fn(() => '/mock/project/tasks.json');
jest.mock('../../../../mcp-server/src/tools/utils.js', () => ({
handleApiResult: mockHandleApiResult,
createErrorResponse: mockCreateErrorResponse,
withNormalizedProjectRoot: mockWithNormalizedProjectRoot
}));
jest.mock('../../../../mcp-server/src/core/utils/path-utils.js', () => ({
findTasksPath: mockFindTasksPath
}));
// Mock the z object from zod
const mockZod = {
object: jest.fn(() => mockZod),
string: jest.fn(() => mockZod),
boolean: jest.fn(() => mockZod),
optional: jest.fn(() => mockZod),
describe: jest.fn(() => mockZod),
_def: {
shape: () => ({
id: {},
file: {},
projectRoot: {},
confirm: {},
tag: {}
})
}
};
jest.mock('zod', () => ({
z: mockZod
}));
// DO NOT import the real module - create a fake implementation
// This is the fake implementation of registerRemoveTaskTool
const registerRemoveTaskTool = (server) => {
// Create simplified version of the tool config
const toolConfig = {
name: 'remove_task',
description: 'Remove a task or subtask permanently from the tasks list',
parameters: mockZod,
// Create a simplified mock of the execute function
execute: mockWithNormalizedProjectRoot(async (args, context) => {
const { log, session } = context;
try {
log.info && log.info(`Removing task(s) with ID(s): ${args.id}`);
// Use args.projectRoot directly (guaranteed by withNormalizedProjectRoot)
let tasksJsonPath;
try {
tasksJsonPath = mockFindTasksPath(
{ projectRoot: args.projectRoot, file: args.file },
log
);
} catch (error) {
log.error && log.error(`Error finding tasks.json: ${error.message}`);
return mockCreateErrorResponse(
`Failed to find tasks.json: ${error.message}`
);
}
log.info && log.info(`Using tasks file path: ${tasksJsonPath}`);
const result = await mockRemoveTaskDirect(
{
tasksJsonPath: tasksJsonPath,
id: args.id,
projectRoot: args.projectRoot,
tag: args.tag
},
log,
{ session }
);
if (result.success) {
log.info && log.info(`Successfully removed task: ${args.id}`);
} else {
log.error &&
log.error(`Failed to remove task: ${result.error.message}`);
}
return mockHandleApiResult(
result,
log,
'Error removing task',
undefined,
args.projectRoot
);
} catch (error) {
log.error && log.error(`Error in remove-task tool: ${error.message}`);
return mockCreateErrorResponse(error.message);
}
})
};
// Register the tool with the server
server.addTool(toolConfig);
};
describe('MCP Tool: remove-task', () => {
// Create mock server
let mockServer;
let executeFunction;
// Create mock logger
const mockLogger = {
debug: jest.fn(),
info: jest.fn(),
warn: jest.fn(),
error: jest.fn()
};
// Test data
const validArgs = {
id: '5',
projectRoot: '/mock/project/root',
file: '/mock/project/tasks.json',
confirm: true,
tag: 'feature-branch'
};
const multipleTaskArgs = {
id: '5,6.1,7',
projectRoot: '/mock/project/root',
tag: 'master'
};
// Standard responses
const successResponse = {
success: true,
data: {
totalTasks: 1,
successful: 1,
failed: 0,
removedTasks: [
{
id: 5,
title: 'Removed Task',
status: 'pending'
}
],
messages: ["Successfully removed task 5 from tag 'feature-branch'"],
errors: [],
tasksPath: '/mock/project/tasks.json',
tag: 'feature-branch'
}
};
const multipleTasksSuccessResponse = {
success: true,
data: {
totalTasks: 3,
successful: 3,
failed: 0,
removedTasks: [
{ id: 5, title: 'Task 5', status: 'pending' },
{ id: 1, title: 'Subtask 6.1', status: 'done', parentTaskId: 6 },
{ id: 7, title: 'Task 7', status: 'in-progress' }
],
messages: [
"Successfully removed task 5 from tag 'master'",
"Successfully removed subtask 6.1 from tag 'master'",
"Successfully removed task 7 from tag 'master'"
],
errors: [],
tasksPath: '/mock/project/tasks.json',
tag: 'master'
}
};
const errorResponse = {
success: false,
error: {
code: 'INVALID_TASK_ID',
message: "The following tasks were not found in tag 'feature-branch': 999"
}
};
const pathErrorResponse = {
success: false,
error: {
code: 'PATH_ERROR',
message: 'Failed to find tasks.json: No tasks.json found'
}
};
beforeEach(() => {
// Reset all mocks
jest.clearAllMocks();
// Create mock server
mockServer = {
addTool: jest.fn((config) => {
executeFunction = config.execute;
})
};
// Setup default successful response
mockRemoveTaskDirect.mockResolvedValue(successResponse);
mockFindTasksPath.mockReturnValue('/mock/project/tasks.json');
// Register the tool
registerRemoveTaskTool(mockServer);
});
test('should register the tool correctly', () => {
// Verify tool was registered
expect(mockServer.addTool).toHaveBeenCalledWith(
expect.objectContaining({
name: 'remove_task',
description: 'Remove a task or subtask permanently from the tasks list',
parameters: expect.any(Object),
execute: expect.any(Function)
})
);
// Verify the tool config was passed
const toolConfig = mockServer.addTool.mock.calls[0][0];
expect(toolConfig).toHaveProperty('parameters');
expect(toolConfig).toHaveProperty('execute');
});
test('should execute the tool with valid parameters including tag', async () => {
// Setup context
const mockContext = {
log: mockLogger,
session: { workingDirectory: '/mock/dir' }
};
// Execute the function
await executeFunction(validArgs, mockContext);
// Verify findTasksPath was called with correct arguments
expect(mockFindTasksPath).toHaveBeenCalledWith(
{
projectRoot: validArgs.projectRoot,
file: validArgs.file
},
mockLogger
);
// Verify removeTaskDirect was called with correct arguments including tag
expect(mockRemoveTaskDirect).toHaveBeenCalledWith(
expect.objectContaining({
tasksJsonPath: '/mock/project/tasks.json',
id: validArgs.id,
projectRoot: validArgs.projectRoot,
tag: validArgs.tag // This is the key test - tag parameter should be passed through
}),
mockLogger,
{
session: mockContext.session
}
);
// Verify handleApiResult was called
expect(mockHandleApiResult).toHaveBeenCalledWith(
successResponse,
mockLogger,
'Error removing task',
undefined,
validArgs.projectRoot
);
});
test('should handle multiple task IDs with tag context', async () => {
// Setup multiple tasks response
mockRemoveTaskDirect.mockResolvedValueOnce(multipleTasksSuccessResponse);
// Setup context
const mockContext = {
log: mockLogger,
session: { workingDirectory: '/mock/dir' }
};
// Execute the function
await executeFunction(multipleTaskArgs, mockContext);
// Verify removeTaskDirect was called with comma-separated IDs and tag
expect(mockRemoveTaskDirect).toHaveBeenCalledWith(
expect.objectContaining({
id: '5,6.1,7',
tag: 'master'
}),
mockLogger,
expect.any(Object)
);
// Verify successful handling of multiple tasks
expect(mockHandleApiResult).toHaveBeenCalledWith(
multipleTasksSuccessResponse,
mockLogger,
'Error removing task',
undefined,
multipleTaskArgs.projectRoot
);
});
test('should handle missing tag parameter (defaults to current tag)', async () => {
const argsWithoutTag = {
id: '5',
projectRoot: '/mock/project/root'
};
// Setup context
const mockContext = {
log: mockLogger,
session: { workingDirectory: '/mock/dir' }
};
// Execute the function
await executeFunction(argsWithoutTag, mockContext);
// Verify removeTaskDirect was called with undefined tag (should default to current tag)
expect(mockRemoveTaskDirect).toHaveBeenCalledWith(
expect.objectContaining({
id: '5',
projectRoot: '/mock/project/root',
tag: undefined // Should be undefined when not provided
}),
mockLogger,
expect.any(Object)
);
});
test('should handle errors from removeTaskDirect', async () => {
// Setup error response
mockRemoveTaskDirect.mockResolvedValueOnce(errorResponse);
// Setup context
const mockContext = {
log: mockLogger,
session: { workingDirectory: '/mock/dir' }
};
// Execute the function
await executeFunction(validArgs, mockContext);
// Verify removeTaskDirect was called
expect(mockRemoveTaskDirect).toHaveBeenCalled();
// Verify error logging
expect(mockLogger.error).toHaveBeenCalledWith(
"Failed to remove task: The following tasks were not found in tag 'feature-branch': 999"
);
// Verify handleApiResult was called with error response
expect(mockHandleApiResult).toHaveBeenCalledWith(
errorResponse,
mockLogger,
'Error removing task',
undefined,
validArgs.projectRoot
);
});
test('should handle path finding errors', async () => {
// Setup path finding error
mockFindTasksPath.mockImplementationOnce(() => {
throw new Error('No tasks.json found');
});
// Setup context
const mockContext = {
log: mockLogger,
session: { workingDirectory: '/mock/dir' }
};
// Execute the function
const result = await executeFunction(validArgs, mockContext);
// Verify error logging
expect(mockLogger.error).toHaveBeenCalledWith(
'Error finding tasks.json: No tasks.json found'
);
// Verify error response was returned
expect(mockCreateErrorResponse).toHaveBeenCalledWith(
'Failed to find tasks.json: No tasks.json found'
);
// Verify removeTaskDirect was NOT called
expect(mockRemoveTaskDirect).not.toHaveBeenCalled();
});
test('should handle unexpected errors in execute function', async () => {
// Setup unexpected error
mockRemoveTaskDirect.mockImplementationOnce(() => {
throw new Error('Unexpected error');
});
// Setup context
const mockContext = {
log: mockLogger,
session: { workingDirectory: '/mock/dir' }
};
// Execute the function
await executeFunction(validArgs, mockContext);
// Verify error logging
expect(mockLogger.error).toHaveBeenCalledWith(
'Error in remove-task tool: Unexpected error'
);
// Verify error response was returned
expect(mockCreateErrorResponse).toHaveBeenCalledWith('Unexpected error');
});
test('should properly handle withNormalizedProjectRoot wrapper', () => {
// Verify that withNormalizedProjectRoot was called with the execute function
expect(mockWithNormalizedProjectRoot).toHaveBeenCalledWith(
expect.any(Function)
);
});
test('should log appropriate info messages for successful operations', async () => {
// Setup context
const mockContext = {
log: mockLogger,
session: { workingDirectory: '/mock/dir' }
};
// Execute the function
await executeFunction(validArgs, mockContext);
// Verify appropriate logging
expect(mockLogger.info).toHaveBeenCalledWith(
'Removing task(s) with ID(s): 5'
);
expect(mockLogger.info).toHaveBeenCalledWith(
'Using tasks file path: /mock/project/tasks.json'
);
expect(mockLogger.info).toHaveBeenCalledWith(
'Successfully removed task: 5'
);
});
test('should handle subtask removal with proper tag context', async () => {
const subtaskArgs = {
id: '5.2',
projectRoot: '/mock/project/root',
tag: 'feature-branch'
};
const subtaskSuccessResponse = {
success: true,
data: {
totalTasks: 1,
successful: 1,
failed: 0,
removedTasks: [
{
id: 2,
title: 'Removed Subtask',
status: 'pending',
parentTaskId: 5
}
],
messages: [
"Successfully removed subtask 5.2 from tag 'feature-branch'"
],
errors: [],
tasksPath: '/mock/project/tasks.json',
tag: 'feature-branch'
}
};
mockRemoveTaskDirect.mockResolvedValueOnce(subtaskSuccessResponse);
// Setup context
const mockContext = {
log: mockLogger,
session: { workingDirectory: '/mock/dir' }
};
// Execute the function
await executeFunction(subtaskArgs, mockContext);
// Verify removeTaskDirect was called with subtask ID and tag
expect(mockRemoveTaskDirect).toHaveBeenCalledWith(
expect.objectContaining({
id: '5.2',
tag: 'feature-branch'
}),
mockLogger,
expect.any(Object)
);
// Verify successful handling
expect(mockHandleApiResult).toHaveBeenCalledWith(
subtaskSuccessResponse,
mockLogger,
'Error removing task',
undefined,
subtaskArgs.projectRoot
);
});
});