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:
@@ -158,20 +158,25 @@ describe('Cross-Tag Move CLI Integration', () => {
|
||||
);
|
||||
|
||||
try {
|
||||
await moveTaskModule.moveTasksBetweenTags(
|
||||
const result = await moveTaskModule.moveTasksBetweenTags(
|
||||
tasksPath,
|
||||
taskIds,
|
||||
sourceTag,
|
||||
toTag,
|
||||
{
|
||||
withDependencies,
|
||||
ignoreDependencies,
|
||||
force
|
||||
ignoreDependencies
|
||||
}
|
||||
);
|
||||
|
||||
console.log(chalk.green('Successfully moved task(s) between tags'));
|
||||
|
||||
// Print advisory tips when present
|
||||
if (result && Array.isArray(result.tips) && result.tips.length > 0) {
|
||||
console.log('Next Steps:');
|
||||
result.tips.forEach((t) => console.log(` • ${t}`));
|
||||
}
|
||||
|
||||
// Generate task files for both tags
|
||||
await generateTaskFilesModule.default(
|
||||
tasksPath,
|
||||
@@ -185,6 +190,21 @@ describe('Cross-Tag Move CLI Integration', () => {
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(chalk.red(`Error: ${error.message}`));
|
||||
// Print ID collision guidance similar to CLI help block
|
||||
if (
|
||||
typeof error?.message === 'string' &&
|
||||
error.message.includes('already exists in target tag')
|
||||
) {
|
||||
console.log('');
|
||||
console.log('Conflict: ID already exists in target tag');
|
||||
console.log(
|
||||
' • Choose a different target tag without conflicting IDs'
|
||||
);
|
||||
console.log(' • Move a different set of IDs (avoid existing ones)');
|
||||
console.log(
|
||||
' • If needed, move within-tag to a new ID first, then cross-tag move'
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
} else {
|
||||
@@ -268,8 +288,7 @@ describe('Cross-Tag Move CLI Integration', () => {
|
||||
'in-progress',
|
||||
{
|
||||
withDependencies: undefined,
|
||||
ignoreDependencies: undefined,
|
||||
force: undefined
|
||||
ignoreDependencies: undefined
|
||||
}
|
||||
);
|
||||
});
|
||||
@@ -323,8 +342,7 @@ describe('Cross-Tag Move CLI Integration', () => {
|
||||
'in-progress',
|
||||
{
|
||||
withDependencies: true,
|
||||
ignoreDependencies: undefined,
|
||||
force: undefined
|
||||
ignoreDependencies: undefined
|
||||
}
|
||||
);
|
||||
});
|
||||
@@ -350,8 +368,7 @@ describe('Cross-Tag Move CLI Integration', () => {
|
||||
'in-progress',
|
||||
{
|
||||
withDependencies: undefined,
|
||||
ignoreDependencies: true,
|
||||
force: undefined
|
||||
ignoreDependencies: true
|
||||
}
|
||||
);
|
||||
});
|
||||
@@ -376,8 +393,7 @@ describe('Cross-Tag Move CLI Integration', () => {
|
||||
'new-tag',
|
||||
{
|
||||
withDependencies: undefined,
|
||||
ignoreDependencies: undefined,
|
||||
force: undefined
|
||||
ignoreDependencies: undefined
|
||||
}
|
||||
);
|
||||
});
|
||||
@@ -440,6 +456,57 @@ describe('Cross-Tag Move CLI Integration', () => {
|
||||
restore();
|
||||
});
|
||||
|
||||
it('should print advisory tips when result.tips are returned (ignore-dependencies)', async () => {
|
||||
const { errorMessages, logMessages, restore } = captureConsoleAndExit();
|
||||
try {
|
||||
// Arrange: mock move to return tips
|
||||
mockMoveTasksBetweenTags.mockResolvedValue({
|
||||
message: 'ok',
|
||||
tips: [
|
||||
'Run "task-master validate-dependencies" to check for dependency issues.',
|
||||
'Run "task-master fix-dependencies" to automatically repair dangling dependencies.'
|
||||
]
|
||||
});
|
||||
|
||||
await moveAction({
|
||||
from: '2',
|
||||
fromTag: 'backlog',
|
||||
toTag: 'in-progress',
|
||||
ignoreDependencies: true
|
||||
});
|
||||
|
||||
const joined = logMessages.join('\n');
|
||||
expect(joined).toContain('Next Steps');
|
||||
expect(joined).toContain('validate-dependencies');
|
||||
expect(joined).toContain('fix-dependencies');
|
||||
} finally {
|
||||
restore();
|
||||
}
|
||||
});
|
||||
|
||||
it('should print ID collision suggestions when target already has the ID', async () => {
|
||||
const { errorMessages, logMessages, restore } = captureConsoleAndExit();
|
||||
try {
|
||||
// Arrange: mock move to throw collision
|
||||
const err = new Error(
|
||||
'Task 1 already exists in target tag "in-progress"'
|
||||
);
|
||||
mockMoveTasksBetweenTags.mockRejectedValue(err);
|
||||
|
||||
await expect(
|
||||
moveAction({ from: '1', fromTag: 'backlog', toTag: 'in-progress' })
|
||||
).rejects.toThrow('already exists in target tag');
|
||||
|
||||
const joined = logMessages.join('\n');
|
||||
expect(joined).toContain('Conflict: ID already exists in target tag');
|
||||
expect(joined).toContain('different target tag');
|
||||
expect(joined).toContain('different set of IDs');
|
||||
expect(joined).toContain('within-tag');
|
||||
} finally {
|
||||
restore();
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle same tag error correctly', async () => {
|
||||
const options = {
|
||||
from: '1',
|
||||
@@ -485,8 +552,7 @@ describe('Cross-Tag Move CLI Integration', () => {
|
||||
from: '1',
|
||||
toTag: 'in-progress',
|
||||
withDependencies: false,
|
||||
ignoreDependencies: false,
|
||||
force: false
|
||||
ignoreDependencies: false
|
||||
// fromTag is intentionally not provided to test fallback
|
||||
});
|
||||
|
||||
@@ -498,8 +564,7 @@ describe('Cross-Tag Move CLI Integration', () => {
|
||||
'in-progress',
|
||||
{
|
||||
withDependencies: false,
|
||||
ignoreDependencies: false,
|
||||
force: false
|
||||
ignoreDependencies: false
|
||||
}
|
||||
);
|
||||
|
||||
@@ -536,8 +601,7 @@ describe('Cross-Tag Move CLI Integration', () => {
|
||||
'in-progress',
|
||||
{
|
||||
withDependencies: undefined,
|
||||
ignoreDependencies: undefined,
|
||||
force: undefined
|
||||
ignoreDependencies: undefined
|
||||
}
|
||||
);
|
||||
|
||||
@@ -555,32 +619,7 @@ describe('Cross-Tag Move CLI Integration', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle --force flag correctly', async () => {
|
||||
// Mock successful cross-tag move with force flag
|
||||
mockMoveTasksBetweenTags.mockResolvedValue(undefined);
|
||||
mockGenerateTaskFiles.mockResolvedValue(undefined);
|
||||
|
||||
const options = {
|
||||
from: '1',
|
||||
fromTag: 'backlog',
|
||||
toTag: 'in-progress',
|
||||
force: true
|
||||
};
|
||||
|
||||
await moveAction(options);
|
||||
|
||||
expect(mockMoveTasksBetweenTags).toHaveBeenCalledWith(
|
||||
expect.stringContaining('tasks.json'),
|
||||
[1],
|
||||
'backlog',
|
||||
'in-progress',
|
||||
{
|
||||
withDependencies: undefined,
|
||||
ignoreDependencies: undefined,
|
||||
force: true // Force flag should be passed through
|
||||
}
|
||||
);
|
||||
});
|
||||
// Note: --force flag is no longer supported for cross-tag moves
|
||||
|
||||
it('should fail when invalid task ID is provided', async () => {
|
||||
const options = {
|
||||
@@ -662,90 +701,11 @@ describe('Cross-Tag Move CLI Integration', () => {
|
||||
restore();
|
||||
});
|
||||
|
||||
it('should combine --with-dependencies and --force flags correctly', async () => {
|
||||
// Mock successful cross-tag move with both flags
|
||||
mockMoveTasksBetweenTags.mockResolvedValue(undefined);
|
||||
mockGenerateTaskFiles.mockResolvedValue(undefined);
|
||||
// Note: --force combinations removed
|
||||
|
||||
const options = {
|
||||
from: '1,2',
|
||||
fromTag: 'backlog',
|
||||
toTag: 'in-progress',
|
||||
withDependencies: true,
|
||||
force: true
|
||||
};
|
||||
// Note: --force combinations removed
|
||||
|
||||
await moveAction(options);
|
||||
|
||||
expect(mockMoveTasksBetweenTags).toHaveBeenCalledWith(
|
||||
expect.stringContaining('tasks.json'),
|
||||
[1, 2],
|
||||
'backlog',
|
||||
'in-progress',
|
||||
{
|
||||
withDependencies: true,
|
||||
ignoreDependencies: undefined,
|
||||
force: true // Both flags should be passed
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('should combine --ignore-dependencies and --force flags correctly', async () => {
|
||||
// Mock successful cross-tag move with both flags
|
||||
mockMoveTasksBetweenTags.mockResolvedValue(undefined);
|
||||
mockGenerateTaskFiles.mockResolvedValue(undefined);
|
||||
|
||||
const options = {
|
||||
from: '1',
|
||||
fromTag: 'backlog',
|
||||
toTag: 'in-progress',
|
||||
ignoreDependencies: true,
|
||||
force: true
|
||||
};
|
||||
|
||||
await moveAction(options);
|
||||
|
||||
expect(mockMoveTasksBetweenTags).toHaveBeenCalledWith(
|
||||
expect.stringContaining('tasks.json'),
|
||||
[1],
|
||||
'backlog',
|
||||
'in-progress',
|
||||
{
|
||||
withDependencies: undefined,
|
||||
ignoreDependencies: true,
|
||||
force: true // Both flags should be passed
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle all three flags combined correctly', async () => {
|
||||
// Mock successful cross-tag move with all flags
|
||||
mockMoveTasksBetweenTags.mockResolvedValue(undefined);
|
||||
mockGenerateTaskFiles.mockResolvedValue(undefined);
|
||||
|
||||
const options = {
|
||||
from: '1,2,3',
|
||||
fromTag: 'backlog',
|
||||
toTag: 'in-progress',
|
||||
withDependencies: true,
|
||||
ignoreDependencies: true,
|
||||
force: true
|
||||
};
|
||||
|
||||
await moveAction(options);
|
||||
|
||||
expect(mockMoveTasksBetweenTags).toHaveBeenCalledWith(
|
||||
expect.stringContaining('tasks.json'),
|
||||
[1, 2, 3],
|
||||
'backlog',
|
||||
'in-progress',
|
||||
{
|
||||
withDependencies: true,
|
||||
ignoreDependencies: true,
|
||||
force: true // All three flags should be passed
|
||||
}
|
||||
);
|
||||
});
|
||||
// Note: --force combinations removed
|
||||
|
||||
it('should handle whitespace in comma-separated task IDs', async () => {
|
||||
// Mock successful cross-tag move with whitespace
|
||||
|
||||
Reference in New Issue
Block a user