Improve cross-tag move UX and safety; add MCP suggestions and CLI tips (#1135)
* docs: Auto-update and format models.md * docs(ui,cli): remove --force from cross-tag move guidance; recommend --with-dependencies/--ignore-dependencies - scripts/modules/ui.js: drop force tip in conflict resolution - scripts/modules/commands.js: remove force examples from move help - docs/cross-tag-task-movement.md: purge force mentions; add explicit with/ignore examples * test(move): update cross-tag move tests to drop --force; assert with/ignore deps behavior and current-tag fallback - CLI integration: remove force expectations, keep with/ignore, current-tag fallback - Integration: remove force-path test - Unit: add scoped traversal test, adjust fixtures to avoid id collision * fix(move): scope dependency traversal to source tag; tag-aware ignore-dependencies filtering - resolveDependencies: traverse only sourceTag tasks to avoid cross-tag contamination - filter dependent IDs to those present in source tag, numeric only - ignore-dependencies: drop deps pointing to tasks from sourceTag; keep targetTag deps * test(mcp): ensure cross-tag move passes only with/ignore options and returns conflict suggestions - new test: tests/unit/mcp/tools/move-task-cross-tag-options.test.js * feat(move): add advisory tips when ignoring cross-tag dependencies; add integration test case * feat(cli/move): improve ID collision UX for cross-tag moves\n\n- Print Next Steps tips when core returns them (e.g., after ignore-dependencies)\n- Add dedicated help block when an ID already exists in target tag * feat(move/mcp): improve ID collision UX and suggestions\n\n- Core: include suggestions on TASK_ALREADY_EXISTS errors\n- MCP: map ID collision to TASK_ALREADY_EXISTS with suggestions\n- Tests: add MCP unit test for ID collision suggestions * test(move/cli): print tips on ignore-dependencies results; print ID collision suggestions\n\n- CLI integration test: assert Next Steps tips printed when result.tips present\n- Integration test: assert TASK_ALREADY_EXISTS error includes suggestions payload * chore(changeset): add changeset for cross-tag move UX improvements (CLI/MCP/core/tests) * Add cross-tag task movement help and validation improvements - Introduced a detailed help command for cross-tag task movement, enhancing user guidance on usage and options. - Updated validation logic in `validateCrossTagMove` to include checks for indirect dependencies, improving accuracy in conflict detection. - Refactored tests to ensure comprehensive coverage of new validation scenarios and error handling. - Cleaned up documentation to reflect the latest changes in task movement functionality. * refactor(commands): remove redundant tips printing after move operation - Eliminated duplicate printing of tips for next steps after the move operation, streamlining the output for users. - This change enhances clarity by ensuring tips are only displayed when relevant, improving overall user experience. * docs(move): clarify "force move" options and improve examples - Updated documentation to replace the deprecated "force move" concept with clear alternatives: `--with-dependencies` and `--ignore-dependencies`. - Enhanced Scenario 3 with explicit options and improved inline comments for better readability. - Removed confusing commented code in favor of a straightforward note in the Force Move section. * chore: run formatter * Update .changeset/clarify-force-move-docs.md Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Update docs/cross-tag-task-movement.md Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Update tests/unit/scripts/modules/task-manager/move-task-cross-tag.test.js Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * test(move): add test for dependency traversal scoping with --with-dependencies option - Introduced a new test to ensure that the dependency traversal is limited to tasks from the source tag when using the --with-dependencies option, addressing potential ID collisions across tags. * test(move): enhance tips validation in cross-tag task movement integration test --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
This commit is contained in:
135
tests/unit/mcp/tools/move-task-cross-tag-options.test.js
Normal file
135
tests/unit/mcp/tools/move-task-cross-tag-options.test.js
Normal file
@@ -0,0 +1,135 @@
|
||||
import { jest } from '@jest/globals';
|
||||
|
||||
// Mocks
|
||||
const mockFindTasksPath = jest
|
||||
.fn()
|
||||
.mockReturnValue('/test/path/.taskmaster/tasks/tasks.json');
|
||||
jest.unstable_mockModule(
|
||||
'../../../../mcp-server/src/core/utils/path-utils.js',
|
||||
() => ({
|
||||
findTasksPath: mockFindTasksPath
|
||||
})
|
||||
);
|
||||
|
||||
const mockEnableSilentMode = jest.fn();
|
||||
const mockDisableSilentMode = jest.fn();
|
||||
jest.unstable_mockModule('../../../../scripts/modules/utils.js', () => ({
|
||||
enableSilentMode: mockEnableSilentMode,
|
||||
disableSilentMode: mockDisableSilentMode
|
||||
}));
|
||||
|
||||
// Spyable mock for moveTasksBetweenTags
|
||||
const mockMoveTasksBetweenTags = jest.fn();
|
||||
jest.unstable_mockModule(
|
||||
'../../../../scripts/modules/task-manager/move-task.js',
|
||||
() => ({
|
||||
moveTasksBetweenTags: mockMoveTasksBetweenTags
|
||||
})
|
||||
);
|
||||
|
||||
// Import after mocks
|
||||
const { moveTaskCrossTagDirect } = await import(
|
||||
'../../../../mcp-server/src/core/direct-functions/move-task-cross-tag.js'
|
||||
);
|
||||
|
||||
describe('MCP Cross-Tag Move Direct Function - options & suggestions', () => {
|
||||
const mockLog = { info: jest.fn(), warn: jest.fn(), error: jest.fn() };
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('passes only withDependencies/ignoreDependencies (no force) to core', async () => {
|
||||
// Arrange: make core throw tag validation after call to capture params
|
||||
mockMoveTasksBetweenTags.mockImplementation(() => {
|
||||
const err = new Error('Source tag "invalid" not found or invalid');
|
||||
err.code = 'INVALID_SOURCE_TAG';
|
||||
throw err;
|
||||
});
|
||||
|
||||
// Act
|
||||
await moveTaskCrossTagDirect(
|
||||
{
|
||||
sourceIds: '1,2',
|
||||
sourceTag: 'backlog',
|
||||
targetTag: 'in-progress',
|
||||
withDependencies: true,
|
||||
projectRoot: '/test'
|
||||
},
|
||||
mockLog
|
||||
);
|
||||
|
||||
// Assert options argument (5th param)
|
||||
expect(mockMoveTasksBetweenTags).toHaveBeenCalled();
|
||||
const args = mockMoveTasksBetweenTags.mock.calls[0];
|
||||
const moveOptions = args[4];
|
||||
expect(moveOptions).toEqual({
|
||||
withDependencies: true,
|
||||
ignoreDependencies: false
|
||||
});
|
||||
expect('force' in moveOptions).toBe(false);
|
||||
});
|
||||
|
||||
it('returns conflict suggestions on cross-tag dependency conflicts', async () => {
|
||||
// Arrange: core throws cross-tag dependency conflicts
|
||||
mockMoveTasksBetweenTags.mockImplementation(() => {
|
||||
const err = new Error(
|
||||
'Cannot move tasks: 2 cross-tag dependency conflicts found'
|
||||
);
|
||||
err.code = 'CROSS_TAG_DEPENDENCY_CONFLICTS';
|
||||
throw err;
|
||||
});
|
||||
|
||||
// Act
|
||||
const result = await moveTaskCrossTagDirect(
|
||||
{
|
||||
sourceIds: '1',
|
||||
sourceTag: 'backlog',
|
||||
targetTag: 'in-progress',
|
||||
projectRoot: '/test'
|
||||
},
|
||||
mockLog
|
||||
);
|
||||
|
||||
// Assert
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error.code).toBe('CROSS_TAG_DEPENDENCY_CONFLICT');
|
||||
expect(Array.isArray(result.error.suggestions)).toBe(true);
|
||||
// Key suggestions
|
||||
const s = result.error.suggestions.join(' ');
|
||||
expect(s).toContain('--with-dependencies');
|
||||
expect(s).toContain('--ignore-dependencies');
|
||||
expect(s).toContain('validate-dependencies');
|
||||
expect(s).toContain('Move dependencies first');
|
||||
});
|
||||
|
||||
it('returns ID collision suggestions when target tag already has the ID', async () => {
|
||||
// Arrange: core throws TASK_ALREADY_EXISTS structured error
|
||||
mockMoveTasksBetweenTags.mockImplementation(() => {
|
||||
const err = new Error(
|
||||
'Task 1 already exists in target tag "in-progress"'
|
||||
);
|
||||
err.code = 'TASK_ALREADY_EXISTS';
|
||||
throw err;
|
||||
});
|
||||
|
||||
// Act
|
||||
const result = await moveTaskCrossTagDirect(
|
||||
{
|
||||
sourceIds: '1',
|
||||
sourceTag: 'backlog',
|
||||
targetTag: 'in-progress',
|
||||
projectRoot: '/test'
|
||||
},
|
||||
mockLog
|
||||
);
|
||||
|
||||
// Assert
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error.code).toBe('TASK_ALREADY_EXISTS');
|
||||
const joined = (result.error.suggestions || []).join(' ');
|
||||
expect(joined).toContain('different target tag');
|
||||
expect(joined).toContain('different set of IDs');
|
||||
expect(joined).toContain('within-tag');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user