Files
claude-task-master/tests/integration/move-task-cross-tag.integration.test.js
Parthy 04e11b5e82 feat: implement cross-tag task movement functionality (#1088)
* feat: enhance move command with cross-tag functionality

- Updated the `move` command to allow moving tasks between different tags, including options for handling dependencies.
- Added new options: `--from-tag`, `--to-tag`, `--with-dependencies`, `--ignore-dependencies`, and `--force`.
- Implemented validation for cross-tag moves and dependency checks.
- Introduced helper functions in the dependency manager for validating and resolving cross-tag dependencies.
- Added integration and unit tests to cover new functionality and edge cases.

* fix: refactor cross-tag move logic and enhance validation

- Moved the import of `moveTasksBetweenTags` to the correct location in `commands.js` for better clarity.
- Added new helper functions in `dependency-manager.js` to improve validation and error handling for cross-tag moves.
- Enhanced existing functions to ensure proper handling of task dependencies and conflicts.
- Updated tests to cover new validation scenarios and ensure robust error messaging for invalid task IDs and tags.

* fix: improve task ID handling and error messaging in cross-tag moves

- Refactored `moveTasksBetweenTags` to normalize task IDs for comparison, ensuring consistent handling of string and numeric IDs.
- Enhanced error messages for cases where source and target tags are the same but no destination is specified.
- Updated tests to validate new behavior, including handling string dependencies correctly during cross-tag moves.
- Cleaned up existing code for better readability and maintainability.

* test: add comprehensive tests for cross-tag move and dependency validation

- Introduced new test files for `move-cross-tag` and `cross-tag-dependencies` to cover various scenarios in cross-tag task movement.
- Implemented tests for handling task movement with and without dependencies, including edge cases for error handling.
- Enhanced existing tests in `fix-dependencies-command` and `move-task` to ensure robust validation of task IDs and dependencies.
- Mocked necessary modules and functions to isolate tests and improve reliability.
- Ensured coverage for both successful and failed cross-tag move operations, validating expected outcomes and error messages.

* test: refactor cross-tag move tests for better clarity and reusability

- Introduced a helper function `simulateCrossTagMove` to streamline cross-tag move test cases, reducing redundancy and improving readability.
- Updated existing tests to utilize the new helper function, ensuring consistent handling of expected messages and options.
- Enhanced test coverage for various scenarios, including handling of dependencies and flags.

* feat: add cross-tag task movement functionality

- Introduced new commands for moving tasks between different tags, enhancing project organization capabilities.
- Updated README with usage examples for cross-tag movement, including options for handling dependencies.
- Created comprehensive documentation for cross-tag task movement, detailing usage, error handling, and best practices.
- Implemented core logic for cross-tag moves, including validation for dependencies and error handling.
- Added integration and unit tests to ensure robust functionality and coverage for various scenarios, including edge cases.

* fix: enhance error handling and logging in cross-tag task movement

- Improved logging in `moveTaskCrossTagDirect` to include detailed arguments for better traceability.
- Refactored error handling to utilize structured error objects, providing clearer suggestions for resolving cross-tag dependency conflicts and subtask movement restrictions.
- Updated documentation to reflect changes in error handling and provide clearer guidance on task movement options.
- Added integration tests for cross-tag movement scenarios, ensuring robust validation of error handling and task movement logic.
- Cleaned up existing tests for clarity and reusability, enhancing overall test coverage.

* feat: enhance dependency resolution and error handling in task movement

- Added recursive dependency resolution for tasks in `moveTasksBetweenTags`, improving handling of complex task relationships.
- Introduced helper functions to find all dependencies and reverse dependencies, ensuring comprehensive coverage during task moves.
- Enhanced error messages in `validateSubtaskMove` and `displaySubtaskMoveError` for better clarity on movement restrictions.
- Updated tests to cover new functionality, including integration tests for complex cross-tag movement scenarios and edge cases.
- Refactored existing code for improved readability and maintainability, ensuring consistent handling of task IDs and dependencies.

* feat: unify dependency traversal and enhance task management utilities

- Introduced `traverseDependencies` utility for unified forward and reverse dependency traversal, improving code reusability and clarity.
- Refactored `findAllDependenciesRecursively` to leverage the new utility, streamlining dependency resolution in task management.
- Added `formatTaskIdForDisplay` helper for better task ID formatting in UI, enhancing user experience during error displays.
- Updated tests to cover new utility functions and ensure robust validation of dependency handling across various scenarios.
- Improved overall code organization and readability, ensuring consistent handling of task dependencies and IDs.

* fix: improve validation for dependency parameters in `findAllDependenciesRecursively`

- Added checks to ensure `sourceTasks` and `allTasks` are arrays, throwing errors if not, to prevent runtime issues.
- Updated documentation comment for clarity on the function's purpose and parameters.

* fix: remove `force` option from task movement parameters

- Eliminated the `force` parameter from the `moveTaskCrossTagDirect` function and related tools, simplifying the task movement logic.
- Updated documentation and tests to reflect the removal of the `force` option, ensuring clarity and consistency across the codebase.
- Adjusted related functions and tests to focus on `ignoreDependencies` as the primary control for handling dependency conflicts during task moves.

* Add cross-tag task movement functionality

- Introduced functionality for organizing tasks across different contexts by enabling cross-tag movement.
- Added `formatTaskIdForDisplay` helper to improve task ID formatting in UI error messages.
- Updated relevant tests to incorporate new functionality and ensure accurate error displays during task movements.

* Update scripts/modules/dependency-manager.js

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* refactor(dependency-manager): Fix subtask resolution and extract helper functions

1. Fix subtask finding logic (lines 1315-1330):
   - Correctly locate parent task by numeric ID
   - Search within parent's subtasks array instead of top-level tasks
   - Properly handle relative subtask references

2. Extract helper functions from getDependentTaskIds (lines 1440-1636):
   - Move findTasksThatDependOn as module-level function
   - Move taskDependsOnSource as module-level function
   - Move subtasksDependOnSource as module-level function
   - Improves readability, maintainability, and testability

Both fixes address architectural issues and improve code organization.

* refactor(dependency-manager): Enhance subtask resolution and dependency validation

- Improved subtask resolution logic to correctly find parent tasks and their subtasks, ensuring accurate identification of dependencies.
- Filtered out null/undefined dependencies before processing, enhancing robustness in dependency checks.
- Updated comments for clarity on the logic flow and purpose of changes, improving code maintainability.

* refactor(move-task): clarify destination ID description and improve skipped task handling

- Updated the description for the destination ID to clarify its usage in cross-tag moves.
- Simplified the handling of skipped tasks during multiple task movements, improving readability and logging.
- Enhanced the API result response to include detailed information about moved and skipped tasks, ensuring better feedback for users.

* refactor(commands): remove redundant tag validation logic

- Eliminated the check for identical source and target tags in the task movement logic, simplifying the code.
- This change streamlines the flow for within-tag moves, enhancing readability and maintainability.

* refactor(commands): enhance move command logic and error handling

- Introduced helper functions for better organization of cross-tag and within-tag move logic, improving code readability and maintainability.
- Enhanced error handling with structured error objects, providing clearer feedback for dependency conflicts and invalid tag combinations.
- Updated move command help output to include best practices and error resolution tips, ensuring users have comprehensive guidance during task movements.
- Streamlined task movement logic to handle multiple tasks more effectively, including detailed logging of successful and failed moves.

* test(dependency-manager): add subtasks to task structure and mock dependency traversal

- Updated `circular-dependencies.test.js` to include subtasks in task definitions, enhancing test coverage for task structures with nested dependencies.
- Mocked `traverseDependencies` in `fix-dependencies-command.test.js` to ensure consistent behavior during tests, improving reliability of dependency-related tests.

* refactor(dependency-manager): extract subtask finding logic into helper function

- Added `findSubtaskInParent` function to encapsulate subtask resolution within a parent task's subtasks array, improving code organization and readability.
- Updated `findDependencyTask` to utilize the new helper function, streamlining the logic for finding subtasks and enhancing maintainability.
- Enhanced comments for clarity on the purpose and functionality of the new subtask finding logic.

* refactor(ui): enhance subtask ID validation and improve error handling

- Added validation for subtask ID format in `formatDependenciesWithStatus` and `taskExists`, ensuring proper handling of invalid formats.
- Updated error logging in `displaySubtaskMoveError` to provide warnings for unexpected task ID formats, improving user feedback.
- Converted hints to a Set in `displayDependencyValidationHints` to ensure unique hints are displayed, enhancing clarity in the UI.

* test(cli): remove redundant timing check in complex cross-tag scenarios

- Eliminated the timing check for task completion within 5 seconds in `complex-cross-tag-scenarios.test.js`, streamlining the test logic.
- This change focuses on verifying task success without unnecessary timing constraints, enhancing test clarity and maintainability.

* test(integration): enhance task movement tests with mock file system

- Added integration tests for moving tasks within the same tag and between different tags using the actual `moveTask` and `moveTasksBetweenTags` functions.
- Implemented `mock-fs` to simulate file system interactions, improving test isolation and reliability.
- Verified task movement success and ensured proper handling of subtasks and dependencies, enhancing overall test coverage for task management functionality.
- Included error handling tests for missing tags and task IDs to ensure robustness in task movement operations.

* test(unit): add comprehensive tests for moveTaskCrossTagDirect functionality

- Introduced new test cases to verify mock functionality, ensuring that mocks for `findTasksPath` and `readJSON` are working as expected.
- Added tests for parameter validation, error handling, and function call flow, including scenarios for missing project roots and identical source/target tags.
- Enhanced coverage for ID parsing and move options, ensuring robust handling of various input conditions and improving overall test reliability.

* test(integration): skip tests for dependency conflict handling and withDependencies option

- Marked tests for handling dependency conflicts and the withDependencies option as skipped due to issues with the mock setup.
- Added TODOs to address the mock-fs setup for complex dependency scenarios, ensuring future improvements in test reliability.

* test(unit): expand cross-tag move command tests with comprehensive mocks

- Added extensive mocks for various modules to enhance the testing of the cross-tag move functionality in `move-cross-tag.test.js`.
- Implemented detailed test cases for handling cross-tag moves, including validation for missing parameters and identical source/target tags.
- Improved error handling tests to ensure robust feedback for invalid operations, enhancing overall test reliability and coverage.

* test(integration): add complex dependency scenarios to task movement tests

- Introduced new integration tests for handling complex dependency scenarios in task movement, utilizing the actual `moveTasksBetweenTags` function.
- Added tests for circular dependencies, nested dependency chains, and cross-tag dependency resolution, enhancing coverage and reliability.
- Documented limitations of the mock-fs setup for complex scenarios and provided warnings in the test output to guide future improvements.
- Skipped tests for dependency conflicts and the withDependencies option due to mock setup issues, with TODOs for resolution.

* test(unit): refactor move-cross-tag tests with focused mock system

- Simplified mocking in `move-cross-tag.test.js` by implementing a configuration-driven mock system, reducing the number of mocked modules from 20+ to 5 core functionalities.
- Introduced a reusable mock factory to streamline the creation of mocks based on configuration, enhancing maintainability and clarity.
- Added documentation for the new mock system, detailing usage examples and benefits, including reduced complexity and improved test focus.
- Implemented tests to validate the mock configuration, ensuring flexibility in enabling/disabling specific mocks.

* test(unit): clean up mocks and improve isEmpty function in fix-dependencies-command tests

- Removed the mock for `traverseDependencies` as it was unnecessary, simplifying the test setup.
- Updated the `isEmpty` function to clarify its behavior regarding null and undefined values, enhancing code readability and maintainability.

* test(unit): update traverseDependencies mock for consistency across tests

- Standardized the mock implementation of `traverseDependencies` in both `fix-dependencies-command.test.js` and `complexity-report-tag-isolation.test.js` to accept `sourceTasks`, `allTasks`, and `options` parameters, ensuring uniformity in test setups.
- This change enhances clarity and maintainability of the tests by aligning the mock behavior across different test files.

* fix(core): improve task movement error handling and ID normalization

- Wrapped task movement logic in a try-finally block to ensure console output is restored even on errors, enhancing reliability.
- Normalized source IDs to handle mixed string/number comparisons, preventing potential issues in dependency checks.
- Added tests for ID type consistency to verify that the normalization fix works correctly across various scenarios, improving test coverage and robustness.

* refactor(task-manager): restructure task movement logic for improved validation and execution

- Renamed and refactored `moveTasksBetweenTags` to streamline the task movement process into distinct phases: validation, data preparation, dependency resolution, execution, and finalization.
- Introduced `validateMove`, `prepareTaskData`, `resolveDependencies`, `executeMoveOperation`, and `finalizeMove` functions to enhance modularity and clarity.
- Updated documentation comments to reflect changes in function responsibilities and parameters.
- Added comprehensive unit tests for the new structure, ensuring robust validation and error handling across various scenarios.
- Improved handling of dependencies and task existence checks during the move operation, enhancing overall reliability.

* fix(move-task): streamline task movement logic and improve error handling

- Refactored the task movement process to enhance clarity and maintainability by replacing `forEach` with a `for...of` loop for better async handling.
- Consolidated error handling and result logging to ensure consistent feedback during task moves.
- Updated the logic for generating files only on the last move, improving performance and reducing unnecessary operations.
- Enhanced validation for skipped tasks, ensuring accurate reporting of moved and skipped tasks in the final result.

* fix(docs): update error message formatting and enhance clarity in task movement documentation

- Changed code block syntax from generic to `text` for better readability in error messages related to task movement and dependency conflicts.
- Ensured consistent formatting across all error message examples to improve user understanding of task movement restrictions and resolutions.
- Added a newline at the end of the file for proper formatting.

* Update .changeset/crazy-meals-hope.md

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* chore: improve changeset

* chore: improve changeset

* fix referenced bug in docs and remove docs

* chore: fix format

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: Ralph Khreish <35776126+Crunchyman-ralph@users.noreply.github.com>
2025-08-11 18:58:51 +02:00

773 lines
19 KiB
JavaScript

import { jest } from '@jest/globals';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Mock dependencies before importing
const mockUtils = {
readJSON: jest.fn(),
writeJSON: jest.fn(),
findProjectRoot: jest.fn(() => '/test/project/root'),
log: jest.fn(),
setTasksForTag: jest.fn(),
traverseDependencies: jest.fn((sourceTasks, allTasks, options = {}) => {
// Mock realistic dependency behavior for testing
const { direction = 'forward' } = options;
if (direction === 'forward') {
// Return dependencies that tasks have
const result = [];
sourceTasks.forEach((task) => {
if (task.dependencies && Array.isArray(task.dependencies)) {
result.push(...task.dependencies);
}
});
return result;
} else if (direction === 'reverse') {
// Return tasks that depend on the source tasks
const sourceIds = sourceTasks.map((t) => t.id);
const normalizedSourceIds = sourceIds.map((id) => String(id));
const result = [];
allTasks.forEach((task) => {
if (task.dependencies && Array.isArray(task.dependencies)) {
const hasDependency = task.dependencies.some((depId) =>
normalizedSourceIds.includes(String(depId))
);
if (hasDependency) {
result.push(task.id);
}
}
});
return result;
}
return [];
})
};
// Mock the utils module
jest.unstable_mockModule('../../scripts/modules/utils.js', () => mockUtils);
// Mock other dependencies
jest.unstable_mockModule(
'../../scripts/modules/task-manager/is-task-dependent.js',
() => ({
default: jest.fn(() => false)
})
);
jest.unstable_mockModule('../../scripts/modules/dependency-manager.js', () => ({
findCrossTagDependencies: jest.fn(() => {
// Since dependencies can only exist within the same tag,
// this function should never find any cross-tag conflicts
return [];
}),
getDependentTaskIds: jest.fn(
(sourceTasks, crossTagDependencies, allTasks) => {
// Since we now use findAllDependenciesRecursively in the actual implementation,
// this mock simulates finding all dependencies recursively within the same tag
const dependentIds = new Set();
const processedIds = new Set();
function findAllDependencies(taskId) {
if (processedIds.has(taskId)) return;
processedIds.add(taskId);
const task = allTasks.find((t) => t.id === taskId);
if (!task || !Array.isArray(task.dependencies)) return;
task.dependencies.forEach((depId) => {
const normalizedDepId =
typeof depId === 'string' ? parseInt(depId, 10) : depId;
if (!isNaN(normalizedDepId) && normalizedDepId !== taskId) {
dependentIds.add(normalizedDepId);
findAllDependencies(normalizedDepId);
}
});
}
sourceTasks.forEach((sourceTask) => {
if (sourceTask && sourceTask.id) {
findAllDependencies(sourceTask.id);
}
});
return Array.from(dependentIds);
}
),
validateSubtaskMove: jest.fn((taskId, sourceTag, targetTag) => {
// Throw error for subtask IDs
const taskIdStr = String(taskId);
if (taskIdStr.includes('.')) {
throw new Error('Cannot move subtasks directly between tags');
}
})
}));
jest.unstable_mockModule(
'../../scripts/modules/task-manager/generate-task-files.js',
() => ({
default: jest.fn().mockResolvedValue()
})
);
// Import the modules we'll be testing after mocking
const { moveTasksBetweenTags } = await import(
'../../scripts/modules/task-manager/move-task.js'
);
describe('Cross-Tag Task Movement Integration Tests', () => {
let testDataPath;
let mockTasksData;
beforeEach(() => {
// Setup test data path
testDataPath = path.join(__dirname, 'temp-test-tasks.json');
// Initialize mock data with multiple tags
mockTasksData = {
backlog: {
tasks: [
{
id: 1,
title: 'Backlog Task 1',
description: 'A task in backlog',
status: 'pending',
dependencies: [],
priority: 'medium',
tag: 'backlog'
},
{
id: 2,
title: 'Backlog Task 2',
description: 'Another task in backlog',
status: 'pending',
dependencies: [1],
priority: 'high',
tag: 'backlog'
},
{
id: 3,
title: 'Backlog Task 3',
description: 'Independent task',
status: 'pending',
dependencies: [],
priority: 'low',
tag: 'backlog'
}
]
},
'in-progress': {
tasks: [
{
id: 4,
title: 'In Progress Task 1',
description: 'A task being worked on',
status: 'in-progress',
dependencies: [],
priority: 'high',
tag: 'in-progress'
}
]
},
done: {
tasks: [
{
id: 5,
title: 'Completed Task 1',
description: 'A completed task',
status: 'done',
dependencies: [],
priority: 'medium',
tag: 'done'
}
]
}
};
// Setup mock utils
mockUtils.readJSON.mockReturnValue(mockTasksData);
mockUtils.writeJSON.mockImplementation((path, data, projectRoot, tag) => {
// Simulate writing to file
return Promise.resolve();
});
});
afterEach(() => {
jest.clearAllMocks();
// Clean up temp file if it exists
if (fs.existsSync(testDataPath)) {
fs.unlinkSync(testDataPath);
}
});
describe('Basic Cross-Tag Movement', () => {
it('should move a single task between tags successfully', async () => {
const taskIds = [1];
const sourceTag = 'backlog';
const targetTag = 'in-progress';
const result = await moveTasksBetweenTags(
testDataPath,
taskIds,
sourceTag,
targetTag,
{},
{ projectRoot: '/test/project' }
);
// Verify readJSON was called with correct parameters
expect(mockUtils.readJSON).toHaveBeenCalledWith(
testDataPath,
'/test/project',
sourceTag
);
// Verify writeJSON was called with updated data
expect(mockUtils.writeJSON).toHaveBeenCalledWith(
testDataPath,
expect.objectContaining({
backlog: expect.objectContaining({
tasks: expect.arrayContaining([
expect.objectContaining({ id: 2 }),
expect.objectContaining({ id: 3 })
])
}),
'in-progress': expect.objectContaining({
tasks: expect.arrayContaining([
expect.objectContaining({ id: 4 }),
expect.objectContaining({
id: 1,
tag: 'in-progress'
})
])
})
}),
'/test/project',
null
);
// Verify result structure
expect(result).toEqual({
message: 'Successfully moved 1 tasks from "backlog" to "in-progress"',
movedTasks: [
{
id: 1,
fromTag: 'backlog',
toTag: 'in-progress'
}
]
});
});
it('should move multiple tasks between tags', async () => {
const taskIds = [1, 3];
const sourceTag = 'backlog';
const targetTag = 'done';
const result = await moveTasksBetweenTags(
testDataPath,
taskIds,
sourceTag,
targetTag,
{},
{ projectRoot: '/test/project' }
);
// Verify the moved tasks are in the target tag
expect(mockUtils.writeJSON).toHaveBeenCalledWith(
testDataPath,
expect.objectContaining({
backlog: expect.objectContaining({
tasks: expect.arrayContaining([expect.objectContaining({ id: 2 })])
}),
done: expect.objectContaining({
tasks: expect.arrayContaining([
expect.objectContaining({ id: 5 }),
expect.objectContaining({
id: 1,
tag: 'done'
}),
expect.objectContaining({
id: 3,
tag: 'done'
})
])
})
}),
'/test/project',
null
);
// Verify result structure
expect(result.movedTasks).toHaveLength(2);
expect(result.movedTasks).toEqual(
expect.arrayContaining([
{ id: 1, fromTag: 'backlog', toTag: 'done' },
{ id: 3, fromTag: 'backlog', toTag: 'done' }
])
);
});
it('should create target tag if it does not exist', async () => {
const taskIds = [1];
const sourceTag = 'backlog';
const targetTag = 'new-tag';
const result = await moveTasksBetweenTags(
testDataPath,
taskIds,
sourceTag,
targetTag,
{},
{ projectRoot: '/test/project' }
);
// Verify new tag was created
expect(mockUtils.writeJSON).toHaveBeenCalledWith(
testDataPath,
expect.objectContaining({
'new-tag': expect.objectContaining({
tasks: expect.arrayContaining([
expect.objectContaining({
id: 1,
tag: 'new-tag'
})
])
})
}),
'/test/project',
null
);
});
});
describe('Dependency Handling', () => {
it('should move task with dependencies when withDependencies is true', async () => {
const taskIds = [2]; // Task 2 depends on Task 1
const sourceTag = 'backlog';
const targetTag = 'in-progress';
const result = await moveTasksBetweenTags(
testDataPath,
taskIds,
sourceTag,
targetTag,
{ withDependencies: true },
{ projectRoot: '/test/project' }
);
// Verify both task 2 and its dependency (task 1) were moved
expect(mockUtils.writeJSON).toHaveBeenCalledWith(
testDataPath,
expect.objectContaining({
backlog: expect.objectContaining({
tasks: expect.arrayContaining([expect.objectContaining({ id: 3 })])
}),
'in-progress': expect.objectContaining({
tasks: expect.arrayContaining([
expect.objectContaining({ id: 4 }),
expect.objectContaining({
id: 1,
tag: 'in-progress'
}),
expect.objectContaining({
id: 2,
tag: 'in-progress'
})
])
})
}),
'/test/project',
null
);
});
it('should move task normally when ignoreDependencies is true (no cross-tag conflicts to ignore)', async () => {
const taskIds = [2]; // Task 2 depends on Task 1
const sourceTag = 'backlog';
const targetTag = 'in-progress';
const result = await moveTasksBetweenTags(
testDataPath,
taskIds,
sourceTag,
targetTag,
{ ignoreDependencies: true },
{ projectRoot: '/test/project' }
);
// Since dependencies only exist within tags, there are no cross-tag conflicts to ignore
// Task 2 moves with its dependencies intact
expect(mockUtils.writeJSON).toHaveBeenCalledWith(
testDataPath,
expect.objectContaining({
backlog: expect.objectContaining({
tasks: expect.arrayContaining([
expect.objectContaining({ id: 1 }),
expect.objectContaining({ id: 3 })
])
}),
'in-progress': expect.objectContaining({
tasks: expect.arrayContaining([
expect.objectContaining({ id: 4 }),
expect.objectContaining({
id: 2,
tag: 'in-progress',
dependencies: [1] // Dependencies preserved since no cross-tag conflicts
})
])
})
}),
'/test/project',
null
);
});
it('should move task without cross-tag dependency conflicts (since dependencies only exist within tags)', async () => {
const taskIds = [2]; // Task 2 depends on Task 1 (both in same tag)
const sourceTag = 'backlog';
const targetTag = 'in-progress';
// Since dependencies can only exist within the same tag,
// there should be no cross-tag conflicts
const result = await moveTasksBetweenTags(
testDataPath,
taskIds,
sourceTag,
targetTag,
{},
{ projectRoot: '/test/project' }
);
// Verify task was moved successfully (without dependencies)
expect(mockUtils.writeJSON).toHaveBeenCalledWith(
testDataPath,
expect.objectContaining({
backlog: expect.objectContaining({
tasks: expect.arrayContaining([
expect.objectContaining({ id: 1 }), // Task 1 stays in backlog
expect.objectContaining({ id: 3 })
])
}),
'in-progress': expect.objectContaining({
tasks: expect.arrayContaining([
expect.objectContaining({ id: 4 }),
expect.objectContaining({
id: 2,
tag: 'in-progress'
})
])
})
}),
'/test/project',
null
);
});
});
describe('Error Handling', () => {
it('should throw error for invalid source tag', async () => {
const taskIds = [1];
const sourceTag = 'nonexistent-tag';
const targetTag = 'in-progress';
// Mock readJSON to return data without the source tag
mockUtils.readJSON.mockReturnValue({
'in-progress': { tasks: [] }
});
await expect(
moveTasksBetweenTags(
testDataPath,
taskIds,
sourceTag,
targetTag,
{},
{ projectRoot: '/test/project' }
)
).rejects.toThrow('Source tag "nonexistent-tag" not found or invalid');
});
it('should throw error for invalid task IDs', async () => {
const taskIds = [999]; // Non-existent task ID
const sourceTag = 'backlog';
const targetTag = 'in-progress';
await expect(
moveTasksBetweenTags(
testDataPath,
taskIds,
sourceTag,
targetTag,
{},
{ projectRoot: '/test/project' }
)
).rejects.toThrow('Task 999 not found in source tag "backlog"');
});
it('should throw error for subtask movement', async () => {
const taskIds = ['1.1']; // Subtask ID
const sourceTag = 'backlog';
const targetTag = 'in-progress';
await expect(
moveTasksBetweenTags(
testDataPath,
taskIds,
sourceTag,
targetTag,
{},
{ projectRoot: '/test/project' }
)
).rejects.toThrow('Cannot move subtasks directly between tags');
});
it('should handle ID conflicts in target tag', async () => {
// Setup data with conflicting IDs
const conflictingData = {
backlog: {
tasks: [
{
id: 1,
title: 'Backlog Task',
tag: 'backlog'
}
]
},
'in-progress': {
tasks: [
{
id: 1, // Same ID as in backlog
title: 'In Progress Task',
tag: 'in-progress'
}
]
}
};
mockUtils.readJSON.mockReturnValue(conflictingData);
const taskIds = [1];
const sourceTag = 'backlog';
const targetTag = 'in-progress';
await expect(
moveTasksBetweenTags(
testDataPath,
taskIds,
sourceTag,
targetTag,
{},
{ projectRoot: '/test/project' }
)
).rejects.toThrow('Task 1 already exists in target tag "in-progress"');
});
});
describe('Edge Cases', () => {
it('should handle empty task list in source tag', async () => {
const emptyData = {
backlog: { tasks: [] },
'in-progress': { tasks: [] }
};
mockUtils.readJSON.mockReturnValue(emptyData);
const taskIds = [1];
const sourceTag = 'backlog';
const targetTag = 'in-progress';
await expect(
moveTasksBetweenTags(
testDataPath,
taskIds,
sourceTag,
targetTag,
{},
{ projectRoot: '/test/project' }
)
).rejects.toThrow('Task 1 not found in source tag "backlog"');
});
it('should preserve task metadata during move', async () => {
const taskIds = [1];
const sourceTag = 'backlog';
const targetTag = 'in-progress';
const result = await moveTasksBetweenTags(
testDataPath,
taskIds,
sourceTag,
targetTag,
{},
{ projectRoot: '/test/project' }
);
// Verify task metadata is preserved
expect(mockUtils.writeJSON).toHaveBeenCalledWith(
testDataPath,
expect.objectContaining({
'in-progress': expect.objectContaining({
tasks: expect.arrayContaining([
expect.objectContaining({
id: 1,
title: 'Backlog Task 1',
description: 'A task in backlog',
status: 'pending',
priority: 'medium',
tag: 'in-progress', // Tag should be updated
metadata: expect.objectContaining({
moveHistory: expect.arrayContaining([
expect.objectContaining({
fromTag: 'backlog',
toTag: 'in-progress',
timestamp: expect.any(String)
})
])
})
})
])
})
}),
'/test/project',
null
);
});
it('should handle force flag for dependency conflicts', async () => {
const taskIds = [2]; // Task 2 depends on Task 1
const sourceTag = 'backlog';
const targetTag = 'in-progress';
const result = await moveTasksBetweenTags(
testDataPath,
taskIds,
sourceTag,
targetTag,
{ force: true },
{ projectRoot: '/test/project' }
);
// Verify task was moved despite dependency conflicts
expect(mockUtils.writeJSON).toHaveBeenCalledWith(
testDataPath,
expect.objectContaining({
'in-progress': expect.objectContaining({
tasks: expect.arrayContaining([
expect.objectContaining({
id: 2,
tag: 'in-progress'
})
])
})
}),
'/test/project',
null
);
});
});
describe('Complex Scenarios', () => {
it('should handle complex moves without cross-tag conflicts (dependencies only within tags)', async () => {
// Setup data with valid within-tag dependencies
const validData = {
backlog: {
tasks: [
{
id: 1,
title: 'Task 1',
dependencies: [], // No dependencies
tag: 'backlog'
},
{
id: 3,
title: 'Task 3',
dependencies: [1], // Depends on Task 1 (same tag)
tag: 'backlog'
}
]
},
'in-progress': {
tasks: [
{
id: 2,
title: 'Task 2',
dependencies: [], // No dependencies
tag: 'in-progress'
}
]
}
};
mockUtils.readJSON.mockReturnValue(validData);
const taskIds = [3];
const sourceTag = 'backlog';
const targetTag = 'in-progress';
// Should succeed since there are no cross-tag conflicts
const result = await moveTasksBetweenTags(
testDataPath,
taskIds,
sourceTag,
targetTag,
{},
{ projectRoot: '/test/project' }
);
expect(result).toEqual({
message: 'Successfully moved 1 tasks from "backlog" to "in-progress"',
movedTasks: [{ id: 3, fromTag: 'backlog', toTag: 'in-progress' }]
});
});
it('should handle bulk move with mixed dependency scenarios', async () => {
const taskIds = [1, 2, 3]; // Multiple tasks with dependencies
const sourceTag = 'backlog';
const targetTag = 'in-progress';
const result = await moveTasksBetweenTags(
testDataPath,
taskIds,
sourceTag,
targetTag,
{ withDependencies: true },
{ projectRoot: '/test/project' }
);
// Verify all tasks were moved
expect(mockUtils.writeJSON).toHaveBeenCalledWith(
testDataPath,
expect.objectContaining({
backlog: expect.objectContaining({
tasks: [] // All tasks should be moved
}),
'in-progress': expect.objectContaining({
tasks: expect.arrayContaining([
expect.objectContaining({ id: 4 }),
expect.objectContaining({ id: 1, tag: 'in-progress' }),
expect.objectContaining({ id: 2, tag: 'in-progress' }),
expect.objectContaining({ id: 3, tag: 'in-progress' })
])
})
}),
'/test/project',
null
);
// Verify result structure
expect(result.movedTasks).toHaveLength(3);
expect(result.movedTasks).toEqual(
expect.arrayContaining([
{ id: 1, fromTag: 'backlog', toTag: 'in-progress' },
{ id: 2, fromTag: 'backlog', toTag: 'in-progress' },
{ id: 3, fromTag: 'backlog', toTag: 'in-progress' }
])
);
});
});
});