more wip
This commit is contained in:
@@ -3,10 +3,7 @@
|
||||
const { spawn } = require('child_process');
|
||||
const path = require('path');
|
||||
|
||||
const args = [
|
||||
'--config', 'jest.e2e.config.js',
|
||||
...process.argv.slice(2)
|
||||
];
|
||||
const args = ['--config', 'jest.e2e.config.js', ...process.argv.slice(2)];
|
||||
|
||||
const jest = spawn('jest', args, {
|
||||
cwd: path.join(__dirname, '../..'),
|
||||
@@ -16,4 +13,4 @@ const jest = spawn('jest', args, {
|
||||
|
||||
jest.on('exit', (code) => {
|
||||
process.exit(code);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,34 +9,37 @@ const { join } = require('path');
|
||||
|
||||
module.exports = async () => {
|
||||
console.log('\n🚀 Setting up E2E test environment...\n');
|
||||
|
||||
|
||||
try {
|
||||
// Ensure task-master is linked globally
|
||||
const projectRoot = join(__dirname, '../../..');
|
||||
console.log('📦 Linking task-master globally...');
|
||||
execSync('npm link', {
|
||||
execSync('npm link', {
|
||||
cwd: projectRoot,
|
||||
stdio: 'inherit'
|
||||
});
|
||||
|
||||
|
||||
// Verify .env file exists
|
||||
const envPath = join(projectRoot, '.env');
|
||||
if (!existsSync(envPath)) {
|
||||
console.warn('⚠️ Warning: .env file not found. Some tests may fail without API keys.');
|
||||
console.warn(
|
||||
'⚠️ Warning: .env file not found. Some tests may fail without API keys.'
|
||||
);
|
||||
} else {
|
||||
console.log('✅ .env file found');
|
||||
}
|
||||
|
||||
|
||||
// Verify task-master command is available
|
||||
try {
|
||||
execSync('task-master --version', { stdio: 'pipe' });
|
||||
console.log('✅ task-master command is available\n');
|
||||
} catch (error) {
|
||||
throw new Error('task-master command not found. Please ensure npm link succeeded.');
|
||||
throw new Error(
|
||||
'task-master command not found. Please ensure npm link succeeded.'
|
||||
);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Global setup failed:', error.message);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
module.exports = async () => {
|
||||
console.log('\n🧹 Cleaning up E2E test environment...\n');
|
||||
|
||||
|
||||
// Any global cleanup needed
|
||||
// Note: Individual test directories are cleaned up in afterEach hooks
|
||||
};
|
||||
};
|
||||
|
||||
@@ -14,7 +14,7 @@ expect.extend({
|
||||
toContainTaskId(received) {
|
||||
const taskIdRegex = /#?\d+/;
|
||||
const pass = taskIdRegex.test(received);
|
||||
|
||||
|
||||
if (pass) {
|
||||
return {
|
||||
message: () => `expected ${received} not to contain a task ID`,
|
||||
@@ -27,10 +27,10 @@ expect.extend({
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
toHaveExitCode(received, expected) {
|
||||
const pass = received.exitCode === expected;
|
||||
|
||||
|
||||
if (pass) {
|
||||
return {
|
||||
message: () => `expected exit code not to be ${expected}`,
|
||||
@@ -38,16 +38,17 @@ expect.extend({
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
message: () => `expected exit code ${expected} but got ${received.exitCode}\nstderr: ${received.stderr}`,
|
||||
message: () =>
|
||||
`expected exit code ${expected} but got ${received.exitCode}\nstderr: ${received.stderr}`,
|
||||
pass: false
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
toContainInOutput(received, expected) {
|
||||
const output = (received.stdout || '') + (received.stderr || '');
|
||||
const pass = output.includes(expected);
|
||||
|
||||
|
||||
if (pass) {
|
||||
return {
|
||||
message: () => `expected output not to contain "${expected}"`,
|
||||
@@ -55,7 +56,8 @@ expect.extend({
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
message: () => `expected output to contain "${expected}"\nstdout: ${received.stdout}\nstderr: ${received.stderr}`,
|
||||
message: () =>
|
||||
`expected output to contain "${expected}"\nstdout: ${received.stdout}\nstderr: ${received.stderr}`,
|
||||
pass: false
|
||||
};
|
||||
}
|
||||
@@ -76,5 +78,5 @@ global.createTestContext = (testName) => {
|
||||
// Clean up any hanging processes
|
||||
afterAll(async () => {
|
||||
// Give time for any async operations to complete
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
});
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
});
|
||||
|
||||
526
tests/e2e/tests/commands/add-dependency.test.js
Normal file
526
tests/e2e/tests/commands/add-dependency.test.js
Normal file
@@ -0,0 +1,526 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from '@jest/globals';
|
||||
import { mkdtempSync } from 'fs';
|
||||
import { tmpdir } from 'os';
|
||||
import { join } from 'path';
|
||||
import { rmSync, existsSync, readFileSync } from 'fs';
|
||||
|
||||
describe('task-master add-dependency', () => {
|
||||
let testDir;
|
||||
let helpers;
|
||||
|
||||
beforeEach(() => {
|
||||
// Create test directory
|
||||
testDir = mkdtempSync(join(tmpdir(), 'tm-test-add-dep-'));
|
||||
process.chdir(testDir);
|
||||
|
||||
// Get helpers from global context
|
||||
helpers = global.testHelpers;
|
||||
|
||||
// Copy .env if exists
|
||||
const envPath = join(process.cwd(), '../../.env');
|
||||
if (existsSync(envPath)) {
|
||||
const envContent = readFileSync(envPath, 'utf-8');
|
||||
helpers.writeFile('.env', envContent);
|
||||
}
|
||||
|
||||
// Initialize task-master project
|
||||
const initResult = helpers.taskMaster('init', ['-y']);
|
||||
expect(initResult).toHaveExitCode(0);
|
||||
|
||||
// Ensure tasks.json exists
|
||||
const tasksPath = join(testDir, '.taskmaster/tasks/tasks.json');
|
||||
if (!helpers.fileExists(tasksPath)) {
|
||||
helpers.writeFile(tasksPath, JSON.stringify({ tasks: [] }, null, 2));
|
||||
}
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Clean up test directory
|
||||
process.chdir('..');
|
||||
rmSync(testDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
describe('Basic dependency creation', () => {
|
||||
it('should add a single dependency to a task', () => {
|
||||
// Create tasks
|
||||
const dep = helpers.taskMaster('add-task', ['Dependency task', '-m']);
|
||||
const depId = helpers.extractTaskId(dep.stdout);
|
||||
|
||||
const task = helpers.taskMaster('add-task', ['Main task', '-m']);
|
||||
const taskId = helpers.extractTaskId(task.stdout);
|
||||
|
||||
// Add dependency
|
||||
const result = helpers.taskMaster('add-dependency', [taskId, depId]);
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('Dependency added successfully');
|
||||
expect(result.stdout).toContain(`${taskId} now depends on ${depId}`);
|
||||
|
||||
// Verify dependency was added
|
||||
const showResult = helpers.taskMaster('show', [taskId]);
|
||||
expect(showResult.stdout).toContain('Dependencies:');
|
||||
expect(showResult.stdout).toContain(`${depId} - Dependency task`);
|
||||
});
|
||||
|
||||
it('should add multiple dependencies at once', () => {
|
||||
// Create dependency tasks
|
||||
const dep1 = helpers.taskMaster('add-task', ['First dependency', '-m']);
|
||||
const depId1 = helpers.extractTaskId(dep1.stdout);
|
||||
|
||||
const dep2 = helpers.taskMaster('add-task', ['Second dependency', '-m']);
|
||||
const depId2 = helpers.extractTaskId(dep2.stdout);
|
||||
|
||||
const dep3 = helpers.taskMaster('add-task', ['Third dependency', '-m']);
|
||||
const depId3 = helpers.extractTaskId(dep3.stdout);
|
||||
|
||||
const task = helpers.taskMaster('add-task', ['Main task', '-m']);
|
||||
const taskId = helpers.extractTaskId(task.stdout);
|
||||
|
||||
// Add multiple dependencies
|
||||
const result = helpers.taskMaster('add-dependency', [
|
||||
taskId,
|
||||
`${depId1},${depId2},${depId3}`
|
||||
]);
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('3 dependencies added');
|
||||
|
||||
// Verify all dependencies were added
|
||||
const showResult = helpers.taskMaster('show', [taskId]);
|
||||
expect(showResult.stdout).toContain(depId1);
|
||||
expect(showResult.stdout).toContain(depId2);
|
||||
expect(showResult.stdout).toContain(depId3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Dependency validation', () => {
|
||||
it('should prevent circular dependencies', () => {
|
||||
// Create circular dependency chain
|
||||
const task1 = helpers.taskMaster('add-task', ['Task 1', '-m']);
|
||||
const id1 = helpers.extractTaskId(task1.stdout);
|
||||
|
||||
const task2 = helpers.taskMaster('add-task', ['Task 2', '-m']);
|
||||
const id2 = helpers.extractTaskId(task2.stdout);
|
||||
|
||||
// Add first dependency
|
||||
helpers.taskMaster('add-dependency', [id2, id1]);
|
||||
|
||||
// Try to create circular dependency
|
||||
const result = helpers.taskMaster('add-dependency', [id1, id2], {
|
||||
allowFailure: true
|
||||
});
|
||||
expect(result.exitCode).not.toBe(0);
|
||||
expect(result.stderr).toContain('circular dependency');
|
||||
});
|
||||
|
||||
it('should prevent self-dependencies', () => {
|
||||
const task = helpers.taskMaster('add-task', ['Task', '-m']);
|
||||
const taskId = helpers.extractTaskId(task.stdout);
|
||||
|
||||
const result = helpers.taskMaster('add-dependency', [taskId, taskId], {
|
||||
allowFailure: true
|
||||
});
|
||||
expect(result.exitCode).not.toBe(0);
|
||||
expect(result.stderr).toContain('cannot depend on itself');
|
||||
});
|
||||
|
||||
it('should detect transitive circular dependencies', () => {
|
||||
// Create chain: A -> B -> C, then try C -> A
|
||||
const taskA = helpers.taskMaster('add-task', ['Task A', '-m']);
|
||||
const idA = helpers.extractTaskId(taskA.stdout);
|
||||
|
||||
const taskB = helpers.taskMaster('add-task', ['Task B', '-m']);
|
||||
const idB = helpers.extractTaskId(taskB.stdout);
|
||||
|
||||
const taskC = helpers.taskMaster('add-task', ['Task C', '-m']);
|
||||
const idC = helpers.extractTaskId(taskC.stdout);
|
||||
|
||||
// Create chain
|
||||
helpers.taskMaster('add-dependency', [idB, idA]);
|
||||
helpers.taskMaster('add-dependency', [idC, idB]);
|
||||
|
||||
// Try to create circular dependency
|
||||
const result = helpers.taskMaster('add-dependency', [idA, idC], {
|
||||
allowFailure: true
|
||||
});
|
||||
expect(result.exitCode).not.toBe(0);
|
||||
expect(result.stderr).toContain('circular dependency');
|
||||
});
|
||||
|
||||
it('should prevent duplicate dependencies', () => {
|
||||
const dep = helpers.taskMaster('add-task', ['Dependency', '-m']);
|
||||
const depId = helpers.extractTaskId(dep.stdout);
|
||||
|
||||
const task = helpers.taskMaster('add-task', ['Task', '-m']);
|
||||
const taskId = helpers.extractTaskId(task.stdout);
|
||||
|
||||
// Add dependency first time
|
||||
helpers.taskMaster('add-dependency', [taskId, depId]);
|
||||
|
||||
// Try to add same dependency again
|
||||
const result = helpers.taskMaster('add-dependency', [taskId, depId]);
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('already depends on');
|
||||
expect(result.stdout).toContain('No changes made');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Status updates', () => {
|
||||
it('should update task status to blocked when adding dependencies', () => {
|
||||
const dep = helpers.taskMaster('add-task', [
|
||||
'Incomplete dependency',
|
||||
'-m'
|
||||
]);
|
||||
const depId = helpers.extractTaskId(dep.stdout);
|
||||
|
||||
const task = helpers.taskMaster('add-task', ['Task', '-m']);
|
||||
const taskId = helpers.extractTaskId(task.stdout);
|
||||
|
||||
// Start the task
|
||||
helpers.taskMaster('set-status', [taskId, 'in-progress']);
|
||||
|
||||
// Add dependency (should change status to blocked)
|
||||
const result = helpers.taskMaster('add-dependency', [taskId, depId]);
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('Status changed to: blocked');
|
||||
|
||||
// Verify status
|
||||
const showResult = helpers.taskMaster('show', [taskId]);
|
||||
expect(showResult.stdout).toContain('Status: blocked');
|
||||
});
|
||||
|
||||
it('should not change status if all dependencies are complete', () => {
|
||||
const dep = helpers.taskMaster('add-task', ['Complete dependency', '-m']);
|
||||
const depId = helpers.extractTaskId(dep.stdout);
|
||||
helpers.taskMaster('set-status', [depId, 'done']);
|
||||
|
||||
const task = helpers.taskMaster('add-task', ['Task', '-m']);
|
||||
const taskId = helpers.extractTaskId(task.stdout);
|
||||
helpers.taskMaster('set-status', [taskId, 'in-progress']);
|
||||
|
||||
// Add completed dependency
|
||||
const result = helpers.taskMaster('add-dependency', [taskId, depId]);
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).not.toContain('Status changed');
|
||||
|
||||
// Status should remain in-progress
|
||||
const showResult = helpers.taskMaster('show', [taskId]);
|
||||
expect(showResult.stdout).toContain('Status: in-progress');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Subtask dependencies', () => {
|
||||
it('should add dependency to a subtask', () => {
|
||||
// Create parent and dependency
|
||||
const parent = helpers.taskMaster('add-task', ['Parent task', '-m']);
|
||||
const parentId = helpers.extractTaskId(parent.stdout);
|
||||
|
||||
const dep = helpers.taskMaster('add-task', ['Dependency', '-m']);
|
||||
const depId = helpers.extractTaskId(dep.stdout);
|
||||
|
||||
// Expand parent
|
||||
helpers.taskMaster('expand', ['-i', parentId, '-n', '2'], {
|
||||
timeout: 60000
|
||||
});
|
||||
|
||||
// Add dependency to subtask
|
||||
const subtaskId = `${parentId}.1`;
|
||||
const result = helpers.taskMaster('add-dependency', [subtaskId, depId]);
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain(`${subtaskId} now depends on ${depId}`);
|
||||
});
|
||||
|
||||
it('should allow subtask to depend on another subtask', () => {
|
||||
// Create parent task
|
||||
const parent = helpers.taskMaster('add-task', ['Parent', '-m']);
|
||||
const parentId = helpers.extractTaskId(parent.stdout);
|
||||
|
||||
// Expand to create subtasks
|
||||
helpers.taskMaster('expand', ['-i', parentId, '-n', '3'], {
|
||||
timeout: 60000
|
||||
});
|
||||
|
||||
// Make subtask 2 depend on subtask 1
|
||||
const result = helpers.taskMaster('add-dependency', [
|
||||
`${parentId}.2`,
|
||||
`${parentId}.1`
|
||||
]);
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('Dependency added successfully');
|
||||
});
|
||||
|
||||
it('should prevent parent depending on its own subtask', () => {
|
||||
const parent = helpers.taskMaster('add-task', ['Parent', '-m']);
|
||||
const parentId = helpers.extractTaskId(parent.stdout);
|
||||
|
||||
helpers.taskMaster('expand', ['-i', parentId, '-n', '2'], {
|
||||
timeout: 60000
|
||||
});
|
||||
|
||||
const result = helpers.taskMaster(
|
||||
'add-dependency',
|
||||
[parentId, `${parentId}.1`],
|
||||
{ allowFailure: true }
|
||||
);
|
||||
expect(result.exitCode).not.toBe(0);
|
||||
expect(result.stderr).toContain('cannot depend on its own subtask');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Bulk operations', () => {
|
||||
it('should add dependencies to multiple tasks', () => {
|
||||
// Create dependency
|
||||
const dep = helpers.taskMaster('add-task', ['Shared dependency', '-m']);
|
||||
const depId = helpers.extractTaskId(dep.stdout);
|
||||
|
||||
// Create multiple tasks
|
||||
const task1 = helpers.taskMaster('add-task', ['Task 1', '-m']);
|
||||
const id1 = helpers.extractTaskId(task1.stdout);
|
||||
|
||||
const task2 = helpers.taskMaster('add-task', ['Task 2', '-m']);
|
||||
const id2 = helpers.extractTaskId(task2.stdout);
|
||||
|
||||
const task3 = helpers.taskMaster('add-task', ['Task 3', '-m']);
|
||||
const id3 = helpers.extractTaskId(task3.stdout);
|
||||
|
||||
// Add dependency to all tasks
|
||||
const result = helpers.taskMaster('add-dependency', [
|
||||
`${id1},${id2},${id3}`,
|
||||
depId
|
||||
]);
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('3 tasks updated');
|
||||
|
||||
// Verify all have the dependency
|
||||
for (const id of [id1, id2, id3]) {
|
||||
const showResult = helpers.taskMaster('show', [id]);
|
||||
expect(showResult.stdout).toContain(depId);
|
||||
}
|
||||
});
|
||||
|
||||
it('should add dependencies by range', () => {
|
||||
// Create dependency
|
||||
const dep = helpers.taskMaster('add-task', ['Dependency', '-m']);
|
||||
const depId = helpers.extractTaskId(dep.stdout);
|
||||
|
||||
// Create sequential tasks
|
||||
const ids = [];
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const result = helpers.taskMaster('add-task', [`Task ${i + 1}`, '-m']);
|
||||
ids.push(helpers.extractTaskId(result.stdout));
|
||||
}
|
||||
|
||||
// Add dependency to range
|
||||
const result = helpers.taskMaster('add-dependency', [
|
||||
'--from',
|
||||
ids[1],
|
||||
'--to',
|
||||
ids[3],
|
||||
depId
|
||||
]);
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('3 tasks updated');
|
||||
|
||||
// Verify middle tasks have dependency
|
||||
for (let i = 1; i <= 3; i++) {
|
||||
const showResult = helpers.taskMaster('show', [ids[i]]);
|
||||
expect(showResult.stdout).toContain(depId);
|
||||
}
|
||||
|
||||
// Verify edge tasks don't have dependency
|
||||
const show0 = helpers.taskMaster('show', [ids[0]]);
|
||||
expect(show0.stdout).not.toContain(`Dependencies:.*${depId}`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Complex dependency graphs', () => {
|
||||
it('should handle diamond dependency pattern', () => {
|
||||
// Create diamond: A depends on B and C, both B and C depend on D
|
||||
const taskD = helpers.taskMaster('add-task', ['Task D (base)', '-m']);
|
||||
const idD = helpers.extractTaskId(taskD.stdout);
|
||||
|
||||
const taskB = helpers.taskMaster('add-task', ['Task B', '-m']);
|
||||
const idB = helpers.extractTaskId(taskB.stdout);
|
||||
helpers.taskMaster('add-dependency', [idB, idD]);
|
||||
|
||||
const taskC = helpers.taskMaster('add-task', ['Task C', '-m']);
|
||||
const idC = helpers.extractTaskId(taskC.stdout);
|
||||
helpers.taskMaster('add-dependency', [idC, idD]);
|
||||
|
||||
const taskA = helpers.taskMaster('add-task', ['Task A (top)', '-m']);
|
||||
const idA = helpers.extractTaskId(taskA.stdout);
|
||||
|
||||
// Add both dependencies to create diamond
|
||||
const result = helpers.taskMaster('add-dependency', [
|
||||
idA,
|
||||
`${idB},${idC}`
|
||||
]);
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('2 dependencies added');
|
||||
|
||||
// Verify the structure
|
||||
const showResult = helpers.taskMaster('show', [idA]);
|
||||
expect(showResult.stdout).toContain(idB);
|
||||
expect(showResult.stdout).toContain(idC);
|
||||
});
|
||||
|
||||
it('should show transitive dependencies', () => {
|
||||
// Create chain A -> B -> C -> D
|
||||
const taskD = helpers.taskMaster('add-task', ['Task D', '-m']);
|
||||
const idD = helpers.extractTaskId(taskD.stdout);
|
||||
|
||||
const taskC = helpers.taskMaster('add-task', ['Task C', '-m']);
|
||||
const idC = helpers.extractTaskId(taskC.stdout);
|
||||
helpers.taskMaster('add-dependency', [idC, idD]);
|
||||
|
||||
const taskB = helpers.taskMaster('add-task', ['Task B', '-m']);
|
||||
const idB = helpers.extractTaskId(taskB.stdout);
|
||||
helpers.taskMaster('add-dependency', [idB, idC]);
|
||||
|
||||
const taskA = helpers.taskMaster('add-task', ['Task A', '-m']);
|
||||
const idA = helpers.extractTaskId(taskA.stdout);
|
||||
helpers.taskMaster('add-dependency', [idA, idB]);
|
||||
|
||||
// Show should indicate full dependency chain
|
||||
const result = helpers.taskMaster('show', [idA]);
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('Dependencies:');
|
||||
expect(result.stdout).toContain(idB);
|
||||
// May also show transitive dependencies in some views
|
||||
});
|
||||
});
|
||||
|
||||
describe('Tag context', () => {
|
||||
it('should add dependencies within a tag', () => {
|
||||
// Create tag
|
||||
helpers.taskMaster('add-tag', ['feature']);
|
||||
helpers.taskMaster('use-tag', ['feature']);
|
||||
|
||||
// Create tasks in feature tag
|
||||
const dep = helpers.taskMaster('add-task', ['Feature dependency', '-m']);
|
||||
const depId = helpers.extractTaskId(dep.stdout);
|
||||
|
||||
const task = helpers.taskMaster('add-task', ['Feature task', '-m']);
|
||||
const taskId = helpers.extractTaskId(task.stdout);
|
||||
|
||||
// Add dependency with tag context
|
||||
const result = helpers.taskMaster('add-dependency', [
|
||||
taskId,
|
||||
depId,
|
||||
'--tag',
|
||||
'feature'
|
||||
]);
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('[feature]');
|
||||
});
|
||||
|
||||
it('should prevent cross-tag dependencies by default', () => {
|
||||
// Create tasks in different tags
|
||||
const masterTask = helpers.taskMaster('add-task', ['Master task', '-m']);
|
||||
const masterId = helpers.extractTaskId(masterTask.stdout);
|
||||
|
||||
helpers.taskMaster('add-tag', ['feature']);
|
||||
helpers.taskMaster('use-tag', ['feature']);
|
||||
const featureTask = helpers.taskMaster('add-task', [
|
||||
'Feature task',
|
||||
'-m'
|
||||
]);
|
||||
const featureId = helpers.extractTaskId(featureTask.stdout);
|
||||
|
||||
// Try to add cross-tag dependency
|
||||
const result = helpers.taskMaster(
|
||||
'add-dependency',
|
||||
[featureId, masterId, '--tag', 'feature'],
|
||||
{ allowFailure: true }
|
||||
);
|
||||
// Depending on implementation, this might warn or fail
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error handling', () => {
|
||||
it('should handle non-existent task IDs', () => {
|
||||
const task = helpers.taskMaster('add-task', ['Task', '-m']);
|
||||
const taskId = helpers.extractTaskId(task.stdout);
|
||||
|
||||
const result = helpers.taskMaster('add-dependency', [taskId, '999'], {
|
||||
allowFailure: true
|
||||
});
|
||||
expect(result.exitCode).not.toBe(0);
|
||||
expect(result.stderr).toMatch(/Task.*999.*not found/i);
|
||||
});
|
||||
|
||||
it('should handle invalid task ID format', () => {
|
||||
const result = helpers.taskMaster('add-dependency', ['invalid-id', '1'], {
|
||||
allowFailure: true
|
||||
});
|
||||
expect(result.exitCode).not.toBe(0);
|
||||
expect(result.stderr).toContain('Invalid task ID');
|
||||
});
|
||||
|
||||
it('should require both task and dependency IDs', () => {
|
||||
const result = helpers.taskMaster('add-dependency', ['1'], {
|
||||
allowFailure: true
|
||||
});
|
||||
expect(result.exitCode).not.toBe(0);
|
||||
expect(result.stderr).toContain('required');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Output options', () => {
|
||||
it('should support quiet mode', () => {
|
||||
const dep = helpers.taskMaster('add-task', ['Dep', '-m']);
|
||||
const depId = helpers.extractTaskId(dep.stdout);
|
||||
|
||||
const task = helpers.taskMaster('add-task', ['Task', '-m']);
|
||||
const taskId = helpers.extractTaskId(task.stdout);
|
||||
|
||||
const result = helpers.taskMaster('add-dependency', [
|
||||
taskId,
|
||||
depId,
|
||||
'-q'
|
||||
]);
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout.split('\n').length).toBeLessThan(3);
|
||||
});
|
||||
|
||||
it('should support JSON output', () => {
|
||||
const dep = helpers.taskMaster('add-task', ['Dep', '-m']);
|
||||
const depId = helpers.extractTaskId(dep.stdout);
|
||||
|
||||
const task = helpers.taskMaster('add-task', ['Task', '-m']);
|
||||
const taskId = helpers.extractTaskId(task.stdout);
|
||||
|
||||
const result = helpers.taskMaster('add-dependency', [
|
||||
taskId,
|
||||
depId,
|
||||
'--json'
|
||||
]);
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
const json = JSON.parse(result.stdout);
|
||||
expect(json.task.id).toBe(parseInt(taskId));
|
||||
expect(json.task.dependencies).toContain(parseInt(depId));
|
||||
expect(json.added).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Visualization', () => {
|
||||
it('should show dependency graph after adding', () => {
|
||||
// Create simple dependency chain
|
||||
const task1 = helpers.taskMaster('add-task', ['Base task', '-m']);
|
||||
const id1 = helpers.extractTaskId(task1.stdout);
|
||||
|
||||
const task2 = helpers.taskMaster('add-task', ['Middle task', '-m']);
|
||||
const id2 = helpers.extractTaskId(task2.stdout);
|
||||
|
||||
const task3 = helpers.taskMaster('add-task', ['Top task', '-m']);
|
||||
const id3 = helpers.extractTaskId(task3.stdout);
|
||||
|
||||
// Build chain
|
||||
helpers.taskMaster('add-dependency', [id2, id1]);
|
||||
const result = helpers.taskMaster('add-dependency', [id3, id2]);
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('Dependency chain:');
|
||||
expect(result.stdout).toMatch(/→|depends on/);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -3,7 +3,14 @@
|
||||
* Tests all aspects of task creation including AI and manual modes
|
||||
*/
|
||||
|
||||
const { mkdtempSync, existsSync, readFileSync, rmSync, writeFileSync, mkdirSync } = require('fs');
|
||||
const {
|
||||
mkdtempSync,
|
||||
existsSync,
|
||||
readFileSync,
|
||||
rmSync,
|
||||
writeFileSync,
|
||||
mkdirSync
|
||||
} = require('fs');
|
||||
const { join } = require('path');
|
||||
const { tmpdir } = require('os');
|
||||
const path = require('path');
|
||||
@@ -15,11 +22,11 @@ describe('add-task command', () => {
|
||||
beforeEach(async () => {
|
||||
// Create test directory
|
||||
testDir = mkdtempSync(join(tmpdir(), 'task-master-add-task-'));
|
||||
|
||||
|
||||
// Initialize test helpers
|
||||
const context = global.createTestContext('add-task');
|
||||
helpers = context.helpers;
|
||||
|
||||
|
||||
// Copy .env file if it exists
|
||||
const mainEnvPath = join(__dirname, '../../../../.env');
|
||||
const testEnvPath = join(testDir, '.env');
|
||||
@@ -27,11 +34,13 @@ describe('add-task command', () => {
|
||||
const envContent = readFileSync(mainEnvPath, 'utf8');
|
||||
writeFileSync(testEnvPath, envContent);
|
||||
}
|
||||
|
||||
|
||||
// Initialize task-master project
|
||||
const initResult = await helpers.taskMaster('init', ['-y'], { cwd: testDir });
|
||||
const initResult = await helpers.taskMaster('init', ['-y'], {
|
||||
cwd: testDir
|
||||
});
|
||||
expect(initResult).toHaveExitCode(0);
|
||||
|
||||
|
||||
// Ensure tasks.json exists (bug workaround)
|
||||
const tasksJsonPath = join(testDir, '.taskmaster/tasks/tasks.json');
|
||||
if (!existsSync(tasksJsonPath)) {
|
||||
@@ -54,13 +63,15 @@ describe('add-task command', () => {
|
||||
['--prompt', 'Create a user authentication system with JWT tokens'],
|
||||
{ cwd: testDir, timeout: 30000 }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContainTaskId();
|
||||
|
||||
|
||||
const taskId = helpers.extractTaskId(result.stdout);
|
||||
const showResult = await helpers.taskMaster('show', [taskId], { cwd: testDir });
|
||||
|
||||
const showResult = await helpers.taskMaster('show', [taskId], {
|
||||
cwd: testDir
|
||||
});
|
||||
|
||||
// AI generated task should contain a title and description
|
||||
expect(showResult.stdout).toContain('Title:');
|
||||
expect(showResult.stdout).toContain('Description:');
|
||||
@@ -68,25 +79,28 @@ describe('add-task command', () => {
|
||||
}, 45000); // 45 second timeout for this test
|
||||
|
||||
it('should handle very long prompts', async () => {
|
||||
const longPrompt = 'Create a comprehensive system that ' + 'handles many features '.repeat(50);
|
||||
const longPrompt =
|
||||
'Create a comprehensive system that ' +
|
||||
'handles many features '.repeat(50);
|
||||
const result = await helpers.taskMaster(
|
||||
'add-task',
|
||||
['--prompt', longPrompt],
|
||||
{ cwd: testDir, timeout: 30000 }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContainTaskId();
|
||||
}, 45000);
|
||||
|
||||
it('should handle special characters in prompt', async () => {
|
||||
const specialPrompt = 'Implement feature: User data and settings with special chars';
|
||||
const specialPrompt =
|
||||
'Implement feature: User data and settings with special chars';
|
||||
const result = await helpers.taskMaster(
|
||||
'add-task',
|
||||
['--prompt', specialPrompt],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContainTaskId();
|
||||
});
|
||||
@@ -94,14 +108,19 @@ describe('add-task command', () => {
|
||||
it('should verify AI generates reasonable output', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'add-task',
|
||||
['--prompt', 'Build a responsive navigation menu with dropdown support'],
|
||||
[
|
||||
'--prompt',
|
||||
'Build a responsive navigation menu with dropdown support'
|
||||
],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
|
||||
const taskId = helpers.extractTaskId(result.stdout);
|
||||
const showResult = await helpers.taskMaster('show', [taskId], { cwd: testDir });
|
||||
const showResult = await helpers.taskMaster('show', [taskId], {
|
||||
cwd: testDir
|
||||
});
|
||||
// Verify AI generated task has proper structure
|
||||
expect(showResult.stdout).toContain('Title:');
|
||||
expect(showResult.stdout).toContain('Status:');
|
||||
@@ -115,52 +134,65 @@ describe('add-task command', () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'add-task',
|
||||
[
|
||||
'--title', 'Setup database connection',
|
||||
'--description', 'Configure PostgreSQL connection with connection pooling'
|
||||
'--title',
|
||||
'Setup database connection',
|
||||
'--description',
|
||||
'Configure PostgreSQL connection with connection pooling'
|
||||
],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContainTaskId();
|
||||
|
||||
|
||||
const taskId = helpers.extractTaskId(result.stdout);
|
||||
const showResult = await helpers.taskMaster('show', [taskId], { cwd: testDir });
|
||||
|
||||
const showResult = await helpers.taskMaster('show', [taskId], {
|
||||
cwd: testDir
|
||||
});
|
||||
|
||||
// Check that at least part of our title and description are shown
|
||||
expect(showResult.stdout).toContain('Setup');
|
||||
expect(showResult.stdout).toContain('Configure');
|
||||
});
|
||||
|
||||
|
||||
it('should create task with manual details', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'add-task',
|
||||
[
|
||||
'--title', 'Implement caching layer',
|
||||
'--description', 'Add Redis caching to improve performance',
|
||||
'--details', 'Use Redis for session storage and API response caching'
|
||||
'--title',
|
||||
'Implement caching layer',
|
||||
'--description',
|
||||
'Add Redis caching to improve performance',
|
||||
'--details',
|
||||
'Use Redis for session storage and API response caching'
|
||||
],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContainTaskId();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Task creation with options', () => {
|
||||
|
||||
it('should create task with priority', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'add-task',
|
||||
['--prompt', 'Fix critical security vulnerability', '--priority', 'high'],
|
||||
[
|
||||
'--prompt',
|
||||
'Fix critical security vulnerability',
|
||||
'--priority',
|
||||
'high'
|
||||
],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
const taskId = helpers.extractTaskId(result.stdout);
|
||||
|
||||
const showResult = await helpers.taskMaster('show', [taskId], { cwd: testDir });
|
||||
|
||||
const showResult = await helpers.taskMaster('show', [taskId], {
|
||||
cwd: testDir
|
||||
});
|
||||
expect(showResult.stdout.toLowerCase()).toContain('high');
|
||||
});
|
||||
|
||||
@@ -168,22 +200,29 @@ describe('add-task command', () => {
|
||||
// Create dependency task first
|
||||
const depResult = await helpers.taskMaster(
|
||||
'add-task',
|
||||
['--title', 'Setup environment', '--description', 'Initial environment setup'],
|
||||
[
|
||||
'--title',
|
||||
'Setup environment',
|
||||
'--description',
|
||||
'Initial environment setup'
|
||||
],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
const depTaskId = helpers.extractTaskId(depResult.stdout);
|
||||
|
||||
|
||||
// Create task with dependency
|
||||
const result = await helpers.taskMaster(
|
||||
'add-task',
|
||||
['--prompt', 'Deploy application', '--dependencies', depTaskId],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
const taskId = helpers.extractTaskId(result.stdout);
|
||||
|
||||
const showResult = await helpers.taskMaster('show', [taskId], { cwd: testDir });
|
||||
|
||||
const showResult = await helpers.taskMaster('show', [taskId], {
|
||||
cwd: testDir
|
||||
});
|
||||
expect(showResult.stdout).toContain(depTaskId);
|
||||
});
|
||||
|
||||
@@ -195,25 +234,32 @@ describe('add-task command', () => {
|
||||
{ cwd: testDir }
|
||||
);
|
||||
const depId1 = helpers.extractTaskId(dep1.stdout);
|
||||
|
||||
|
||||
const dep2 = await helpers.taskMaster(
|
||||
'add-task',
|
||||
['--prompt', 'Configure database'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
const depId2 = helpers.extractTaskId(dep2.stdout);
|
||||
|
||||
|
||||
// Create task with multiple dependencies
|
||||
const result = await helpers.taskMaster(
|
||||
'add-task',
|
||||
['--prompt', 'Deploy application', '--dependencies', `${depId1},${depId2}`],
|
||||
[
|
||||
'--prompt',
|
||||
'Deploy application',
|
||||
'--dependencies',
|
||||
`${depId1},${depId2}`
|
||||
],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
const taskId = helpers.extractTaskId(result.stdout);
|
||||
|
||||
const showResult = await helpers.taskMaster('show', [taskId], { cwd: testDir });
|
||||
|
||||
const showResult = await helpers.taskMaster('show', [taskId], {
|
||||
cwd: testDir
|
||||
});
|
||||
expect(showResult.stdout).toContain(depId1);
|
||||
expect(showResult.stdout).toContain(depId2);
|
||||
});
|
||||
@@ -222,33 +268,43 @@ describe('add-task command', () => {
|
||||
// Setup
|
||||
const depResult = await helpers.taskMaster(
|
||||
'add-task',
|
||||
['--title', 'Prerequisite task', '--description', 'Task that must be completed first'],
|
||||
[
|
||||
'--title',
|
||||
'Prerequisite task',
|
||||
'--description',
|
||||
'Task that must be completed first'
|
||||
],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
const depTaskId = helpers.extractTaskId(depResult.stdout);
|
||||
|
||||
|
||||
await helpers.taskMaster(
|
||||
'add-tag',
|
||||
['feature-complete', '--description', 'Complete feature test'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
|
||||
// Create task with all options
|
||||
const result = await helpers.taskMaster(
|
||||
'add-task',
|
||||
[
|
||||
'--prompt', 'Comprehensive task with all features',
|
||||
'--priority', 'medium',
|
||||
'--dependencies', depTaskId
|
||||
'--prompt',
|
||||
'Comprehensive task with all features',
|
||||
'--priority',
|
||||
'medium',
|
||||
'--dependencies',
|
||||
depTaskId
|
||||
],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
const taskId = helpers.extractTaskId(result.stdout);
|
||||
|
||||
|
||||
// Verify all options
|
||||
const showResult = await helpers.taskMaster('show', [taskId], { cwd: testDir });
|
||||
const showResult = await helpers.taskMaster('show', [taskId], {
|
||||
cwd: testDir
|
||||
});
|
||||
expect(showResult.stdout.toLowerCase()).toContain('medium');
|
||||
expect(showResult.stdout).toContain(depTaskId);
|
||||
});
|
||||
@@ -256,23 +312,24 @@ describe('add-task command', () => {
|
||||
|
||||
describe('Error handling', () => {
|
||||
it('should fail without prompt or title+description', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'add-task',
|
||||
[],
|
||||
{ cwd: testDir, allowFailure: true }
|
||||
);
|
||||
|
||||
const result = await helpers.taskMaster('add-task', [], {
|
||||
cwd: testDir,
|
||||
allowFailure: true
|
||||
});
|
||||
|
||||
expect(result.exitCode).not.toBe(0);
|
||||
expect(result.stderr).toContain('Either --prompt or both --title and --description must be provided');
|
||||
expect(result.stderr).toContain(
|
||||
'Either --prompt or both --title and --description must be provided'
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
it('should fail with only title (missing description)', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'add-task',
|
||||
['--title', 'Incomplete task'],
|
||||
{ cwd: testDir, allowFailure: true }
|
||||
);
|
||||
|
||||
|
||||
expect(result.exitCode).not.toBe(0);
|
||||
});
|
||||
|
||||
@@ -282,15 +339,17 @@ describe('add-task command', () => {
|
||||
['--prompt', 'Test task', '--priority', 'invalid'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
|
||||
// Should succeed but use default priority and show warning
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('Invalid priority "invalid"');
|
||||
expect(result.stdout).toContain('Using default priority "medium"');
|
||||
|
||||
|
||||
const taskId = helpers.extractTaskId(result.stdout);
|
||||
|
||||
const showResult = await helpers.taskMaster('show', [taskId], { cwd: testDir });
|
||||
|
||||
const showResult = await helpers.taskMaster('show', [taskId], {
|
||||
cwd: testDir
|
||||
});
|
||||
expect(showResult.stdout).toContain('Priority: │ medium');
|
||||
});
|
||||
|
||||
@@ -301,7 +360,7 @@ describe('add-task command', () => {
|
||||
['--prompt', 'Test task', '--dependencies', '99999'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
|
||||
// Should succeed but with warning
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('do not exist');
|
||||
@@ -320,9 +379,9 @@ describe('add-task command', () => {
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
const results = await Promise.all(promises);
|
||||
|
||||
|
||||
results.forEach((result) => {
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContainTaskId();
|
||||
@@ -335,100 +394,116 @@ describe('add-task command', () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'add-task',
|
||||
[
|
||||
'--prompt', 'Research best practices for implementing OAuth2 authentication',
|
||||
'--prompt',
|
||||
'Research best practices for implementing OAuth2 authentication',
|
||||
'--research'
|
||||
],
|
||||
{ cwd: testDir, timeout: 45000 }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContainTaskId();
|
||||
|
||||
|
||||
// Verify task was created
|
||||
const taskId = helpers.extractTaskId(result.stdout);
|
||||
const showResult = await helpers.taskMaster('show', [taskId], { cwd: testDir });
|
||||
const showResult = await helpers.taskMaster('show', [taskId], {
|
||||
cwd: testDir
|
||||
});
|
||||
// Verify task was created with research mode (should have more detailed output)
|
||||
expect(showResult.stdout).toContain('Title:');
|
||||
expect(showResult.stdout).toContain('Implementation Details:');
|
||||
}, 60000);
|
||||
});
|
||||
|
||||
|
||||
describe('File path handling', () => {
|
||||
it('should use custom tasks file path', async () => {
|
||||
// Create custom tasks file
|
||||
const customPath = join(testDir, 'custom-tasks.json');
|
||||
writeFileSync(customPath, JSON.stringify({ master: { tasks: [] } }));
|
||||
|
||||
|
||||
const result = await helpers.taskMaster(
|
||||
'add-task',
|
||||
[
|
||||
'--file', customPath,
|
||||
'--prompt', 'Task in custom file'
|
||||
],
|
||||
['--file', customPath, '--prompt', 'Task in custom file'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
|
||||
// Verify task was added to custom file
|
||||
const customContent = JSON.parse(readFileSync(customPath, 'utf8'));
|
||||
expect(customContent.master.tasks.length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('Priority validation', () => {
|
||||
it('should accept all valid priority values', async () => {
|
||||
const priorities = ['high', 'medium', 'low'];
|
||||
|
||||
|
||||
for (const priority of priorities) {
|
||||
const result = await helpers.taskMaster(
|
||||
'add-task',
|
||||
['--prompt', `Task with ${priority} priority`, '--priority', priority],
|
||||
[
|
||||
'--prompt',
|
||||
`Task with ${priority} priority`,
|
||||
'--priority',
|
||||
priority
|
||||
],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
const taskId = helpers.extractTaskId(result.stdout);
|
||||
|
||||
const showResult = await helpers.taskMaster('show', [taskId], { cwd: testDir });
|
||||
|
||||
const showResult = await helpers.taskMaster('show', [taskId], {
|
||||
cwd: testDir
|
||||
});
|
||||
expect(showResult.stdout.toLowerCase()).toContain(priority);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
it('should accept priority values case-insensitively', async () => {
|
||||
const priorities = ['HIGH', 'Medium', 'LoW'];
|
||||
const expected = ['high', 'medium', 'low'];
|
||||
|
||||
|
||||
for (let i = 0; i < priorities.length; i++) {
|
||||
const result = await helpers.taskMaster(
|
||||
'add-task',
|
||||
['--prompt', `Task with ${priorities[i]} priority`, '--priority', priorities[i]],
|
||||
[
|
||||
'--prompt',
|
||||
`Task with ${priorities[i]} priority`,
|
||||
'--priority',
|
||||
priorities[i]
|
||||
],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
const taskId = helpers.extractTaskId(result.stdout);
|
||||
|
||||
const showResult = await helpers.taskMaster('show', [taskId], { cwd: testDir });
|
||||
|
||||
const showResult = await helpers.taskMaster('show', [taskId], {
|
||||
cwd: testDir
|
||||
});
|
||||
expect(showResult.stdout).toContain(`Priority: │ ${expected[i]}`);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
it('should default to medium priority when not specified', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'add-task',
|
||||
['--prompt', 'Task without explicit priority'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
const taskId = helpers.extractTaskId(result.stdout);
|
||||
|
||||
const showResult = await helpers.taskMaster('show', [taskId], { cwd: testDir });
|
||||
|
||||
const showResult = await helpers.taskMaster('show', [taskId], {
|
||||
cwd: testDir
|
||||
});
|
||||
expect(showResult.stdout.toLowerCase()).toContain('medium');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('AI dependency suggestions', () => {
|
||||
it('should let AI suggest dependencies based on context', async () => {
|
||||
// Create some existing tasks that AI might reference
|
||||
@@ -438,14 +513,14 @@ describe('add-task command', () => {
|
||||
['--prompt', 'Setup authentication system'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
|
||||
// Create a task that should logically depend on auth
|
||||
const result = await helpers.taskMaster(
|
||||
'add-task',
|
||||
['--prompt', 'Implement user profile page with authentication checks'],
|
||||
{ cwd: testDir, timeout: 45000 }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
// Check if AI suggested dependencies
|
||||
if (result.stdout.includes('AI suggested')) {
|
||||
@@ -453,25 +528,26 @@ describe('add-task command', () => {
|
||||
}
|
||||
}, 60000);
|
||||
});
|
||||
|
||||
|
||||
describe('Tag support', () => {
|
||||
it('should add task to specific tag', async () => {
|
||||
// Create a new tag
|
||||
await helpers.taskMaster('add-tag', ['feature-branch', '--description', 'Feature branch tag'], { cwd: testDir });
|
||||
|
||||
await helpers.taskMaster(
|
||||
'add-tag',
|
||||
['feature-branch', '--description', 'Feature branch tag'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
// Add task to specific tag
|
||||
const result = await helpers.taskMaster(
|
||||
'add-task',
|
||||
[
|
||||
'--prompt', 'Task for feature branch',
|
||||
'--tag', 'feature-branch'
|
||||
],
|
||||
['--prompt', 'Task for feature branch', '--tag', 'feature-branch'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContainTaskId();
|
||||
|
||||
|
||||
// Verify task is in the correct tag
|
||||
const taskId = helpers.extractTaskId(result.stdout);
|
||||
const showResult = await helpers.taskMaster(
|
||||
@@ -481,50 +557,48 @@ describe('add-task command', () => {
|
||||
);
|
||||
expect(showResult).toHaveExitCode(0);
|
||||
});
|
||||
|
||||
|
||||
it('should add to master tag by default', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'add-task',
|
||||
['--prompt', 'Task for master tag'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
|
||||
// Verify task is in master tag
|
||||
const tasksContent = JSON.parse(readFileSync(join(testDir, '.taskmaster/tasks/tasks.json'), 'utf8'));
|
||||
const tasksContent = JSON.parse(
|
||||
readFileSync(join(testDir, '.taskmaster/tasks/tasks.json'), 'utf8')
|
||||
);
|
||||
expect(tasksContent.master.tasks.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('AI fallback behavior', () => {
|
||||
it('should handle invalid model gracefully', async () => {
|
||||
// Set an invalid model
|
||||
await helpers.taskMaster(
|
||||
'models',
|
||||
['--set-main', 'invalid-model-xyz'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
await helpers.taskMaster('models', ['--set-main', 'invalid-model-xyz'], {
|
||||
cwd: testDir
|
||||
});
|
||||
|
||||
const result = await helpers.taskMaster(
|
||||
'add-task',
|
||||
['--prompt', 'Test fallback behavior'],
|
||||
{ cwd: testDir, allowFailure: true }
|
||||
);
|
||||
|
||||
|
||||
// Should either use fallback or fail gracefully
|
||||
if (result.exitCode === 0) {
|
||||
expect(result.stdout).toContainTaskId();
|
||||
} else {
|
||||
expect(result.stderr).toBeTruthy();
|
||||
}
|
||||
|
||||
|
||||
// Reset to valid model for other tests
|
||||
await helpers.taskMaster(
|
||||
'models',
|
||||
['--set-main', 'gpt-3.5-turbo'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
await helpers.taskMaster('models', ['--set-main', 'gpt-3.5-turbo'], {
|
||||
cwd: testDir
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,7 +3,14 @@
|
||||
* Tests all aspects of complexity analysis including research mode and output formats
|
||||
*/
|
||||
|
||||
const { mkdtempSync, existsSync, readFileSync, rmSync, writeFileSync, mkdirSync } = require('fs');
|
||||
const {
|
||||
mkdtempSync,
|
||||
existsSync,
|
||||
readFileSync,
|
||||
rmSync,
|
||||
writeFileSync,
|
||||
mkdirSync
|
||||
} = require('fs');
|
||||
const { join } = require('path');
|
||||
const { tmpdir } = require('os');
|
||||
const { execSync } = require('child_process');
|
||||
@@ -17,12 +24,12 @@ describe('analyze-complexity command', () => {
|
||||
beforeEach(async () => {
|
||||
// Create test directory
|
||||
testDir = mkdtempSync(join(tmpdir(), 'task-master-analyze-complexity-'));
|
||||
|
||||
|
||||
// Initialize test helpers
|
||||
const context = global.createTestContext('analyze-complexity');
|
||||
helpers = context.helpers;
|
||||
logger = context.logger;
|
||||
|
||||
|
||||
// Copy .env file if it exists
|
||||
const mainEnvPath = join(__dirname, '../../../../.env');
|
||||
const testEnvPath = join(testDir, '.env');
|
||||
@@ -30,14 +37,16 @@ describe('analyze-complexity command', () => {
|
||||
const envContent = readFileSync(mainEnvPath, 'utf8');
|
||||
writeFileSync(testEnvPath, envContent);
|
||||
}
|
||||
|
||||
|
||||
// Initialize task-master project
|
||||
const initResult = await helpers.taskMaster('init', ['-y'], { cwd: testDir });
|
||||
const initResult = await helpers.taskMaster('init', ['-y'], {
|
||||
cwd: testDir
|
||||
});
|
||||
expect(initResult).toHaveExitCode(0);
|
||||
|
||||
// Setup test tasks for analysis
|
||||
taskIds = [];
|
||||
|
||||
|
||||
// Create simple task
|
||||
const simple = await helpers.taskMaster(
|
||||
'add-task',
|
||||
@@ -45,19 +54,22 @@ describe('analyze-complexity command', () => {
|
||||
{ cwd: testDir }
|
||||
);
|
||||
taskIds.push(helpers.extractTaskId(simple.stdout));
|
||||
|
||||
|
||||
// Create complex task with subtasks
|
||||
const complex = await helpers.taskMaster(
|
||||
'add-task',
|
||||
['--prompt', 'Build a complete e-commerce platform with payment processing'],
|
||||
[
|
||||
'--prompt',
|
||||
'Build a complete e-commerce platform with payment processing'
|
||||
],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
const complexId = helpers.extractTaskId(complex.stdout);
|
||||
taskIds.push(complexId);
|
||||
|
||||
|
||||
// Expand complex task to add subtasks
|
||||
await helpers.taskMaster('expand', [complexId], { cwd: testDir });
|
||||
|
||||
|
||||
// Create task with dependencies
|
||||
const withDeps = await helpers.taskMaster(
|
||||
'add-task',
|
||||
@@ -76,12 +88,10 @@ describe('analyze-complexity command', () => {
|
||||
|
||||
describe('Basic complexity analysis', () => {
|
||||
it('should analyze complexity without flags', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'analyze-complexity',
|
||||
[],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
const result = await helpers.taskMaster('analyze-complexity', [], {
|
||||
cwd: testDir
|
||||
});
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout.toLowerCase()).toContain('complexity');
|
||||
});
|
||||
@@ -92,7 +102,7 @@ describe('analyze-complexity command', () => {
|
||||
['--research'],
|
||||
{ cwd: testDir, timeout: 120000 }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout.toLowerCase()).toContain('complexity');
|
||||
}, 120000);
|
||||
@@ -106,12 +116,12 @@ describe('analyze-complexity command', () => {
|
||||
['--output', outputPath],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
|
||||
const fullPath = join(testDir, outputPath);
|
||||
expect(existsSync(fullPath)).toBe(true);
|
||||
|
||||
|
||||
// Verify it's valid JSON
|
||||
const report = JSON.parse(readFileSync(fullPath, 'utf8'));
|
||||
expect(report).toBeDefined();
|
||||
@@ -124,15 +134,15 @@ describe('analyze-complexity command', () => {
|
||||
['--format', 'json'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
|
||||
// Output should be valid JSON
|
||||
let parsed;
|
||||
expect(() => {
|
||||
parsed = JSON.parse(result.stdout);
|
||||
}).not.toThrow();
|
||||
|
||||
|
||||
expect(parsed).toBeDefined();
|
||||
expect(typeof parsed).toBe('object');
|
||||
});
|
||||
@@ -143,13 +153,20 @@ describe('analyze-complexity command', () => {
|
||||
['--detailed'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
|
||||
const output = result.stdout.toLowerCase();
|
||||
const expectedDetails = ['subtasks', 'dependencies', 'description', 'metadata'];
|
||||
const foundDetails = expectedDetails.filter(detail => output.includes(detail));
|
||||
|
||||
const expectedDetails = [
|
||||
'subtasks',
|
||||
'dependencies',
|
||||
'description',
|
||||
'metadata'
|
||||
];
|
||||
const foundDetails = expectedDetails.filter((detail) =>
|
||||
output.includes(detail)
|
||||
);
|
||||
|
||||
expect(foundDetails.length).toBeGreaterThanOrEqual(2);
|
||||
});
|
||||
});
|
||||
@@ -161,11 +178,11 @@ describe('analyze-complexity command', () => {
|
||||
['--tasks', taskIds.join(',')],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
|
||||
// Should analyze only specified tasks
|
||||
taskIds.forEach(taskId => {
|
||||
taskIds.forEach((taskId) => {
|
||||
expect(result.stdout).toContain(taskId);
|
||||
});
|
||||
});
|
||||
@@ -179,27 +196,29 @@ describe('analyze-complexity command', () => {
|
||||
{ cwd: testDir }
|
||||
);
|
||||
const taggedId = helpers.extractTaskId(taggedResult.stdout);
|
||||
|
||||
|
||||
const result = await helpers.taskMaster(
|
||||
'analyze-complexity',
|
||||
['--tag', 'complex-tag'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain(taggedId);
|
||||
});
|
||||
|
||||
it('should filter by status', async () => {
|
||||
// Set one task to completed
|
||||
await helpers.taskMaster('set-status', [taskIds[0], 'completed'], { cwd: testDir });
|
||||
|
||||
await helpers.taskMaster('set-status', [taskIds[0], 'completed'], {
|
||||
cwd: testDir
|
||||
});
|
||||
|
||||
const result = await helpers.taskMaster(
|
||||
'analyze-complexity',
|
||||
['--status', 'pending'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
// Should not include completed task
|
||||
expect(result.stdout).not.toContain(taskIds[0]);
|
||||
@@ -210,12 +229,19 @@ describe('analyze-complexity command', () => {
|
||||
it('should use custom thresholds', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'analyze-complexity',
|
||||
['--low-threshold', '3', '--medium-threshold', '7', '--high-threshold', '10'],
|
||||
[
|
||||
'--low-threshold',
|
||||
'3',
|
||||
'--medium-threshold',
|
||||
'7',
|
||||
'--high-threshold',
|
||||
'10'
|
||||
],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
|
||||
const output = result.stdout.toLowerCase();
|
||||
expect(output).toContain('low');
|
||||
expect(output).toContain('medium');
|
||||
@@ -228,7 +254,7 @@ describe('analyze-complexity command', () => {
|
||||
['--low-threshold', '-1'],
|
||||
{ cwd: testDir, allowFailure: true }
|
||||
);
|
||||
|
||||
|
||||
expect(result.exitCode).not.toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -237,16 +263,14 @@ describe('analyze-complexity command', () => {
|
||||
it('should handle empty project', async () => {
|
||||
// Create a new temp directory
|
||||
const emptyDir = mkdtempSync(join(tmpdir(), 'task-master-empty-'));
|
||||
|
||||
|
||||
try {
|
||||
await helpers.taskMaster('init', ['-y'], { cwd: emptyDir });
|
||||
|
||||
const result = await helpers.taskMaster(
|
||||
'analyze-complexity',
|
||||
[],
|
||||
{ cwd: emptyDir }
|
||||
);
|
||||
|
||||
|
||||
const result = await helpers.taskMaster('analyze-complexity', [], {
|
||||
cwd: emptyDir
|
||||
});
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout.toLowerCase()).toMatch(/no tasks|0/);
|
||||
} finally {
|
||||
@@ -260,7 +284,7 @@ describe('analyze-complexity command', () => {
|
||||
['--output', '/invalid/path/report.json'],
|
||||
{ cwd: testDir, allowFailure: true }
|
||||
);
|
||||
|
||||
|
||||
expect(result.exitCode).not.toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -279,15 +303,13 @@ describe('analyze-complexity command', () => {
|
||||
);
|
||||
}
|
||||
await Promise.all(promises);
|
||||
|
||||
|
||||
const startTime = Date.now();
|
||||
const result = await helpers.taskMaster(
|
||||
'analyze-complexity',
|
||||
[],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
const result = await helpers.taskMaster('analyze-complexity', [], {
|
||||
cwd: testDir
|
||||
});
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(duration).toBeLessThan(10000); // Should complete in less than 10 seconds
|
||||
});
|
||||
@@ -300,13 +322,13 @@ describe('analyze-complexity command', () => {
|
||||
['--format', 'json'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
|
||||
const analysis = JSON.parse(result.stdout);
|
||||
const simpleTask = analysis.tasks?.find(t => t.id === taskIds[0]);
|
||||
const complexTask = analysis.tasks?.find(t => t.id === taskIds[1]);
|
||||
|
||||
const simpleTask = analysis.tasks?.find((t) => t.id === taskIds[0]);
|
||||
const complexTask = analysis.tasks?.find((t) => t.id === taskIds[1]);
|
||||
|
||||
expect(simpleTask).toBeDefined();
|
||||
expect(complexTask).toBeDefined();
|
||||
expect(complexTask.complexity).toBeGreaterThan(simpleTask.complexity);
|
||||
@@ -321,15 +343,15 @@ describe('analyze-complexity command', () => {
|
||||
['--output', '.taskmaster/reports/task-complexity-report.json'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
const result = await helpers.taskMaster(
|
||||
'complexity-report',
|
||||
[],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
|
||||
const result = await helpers.taskMaster('complexity-report', [], {
|
||||
cwd: testDir
|
||||
});
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout.toLowerCase()).toMatch(/complexity report|complexity/);
|
||||
expect(result.stdout.toLowerCase()).toMatch(
|
||||
/complexity report|complexity/
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,7 +3,14 @@
|
||||
* Tests all aspects of task expansion including single, multiple, and recursive expansion
|
||||
*/
|
||||
|
||||
const { mkdtempSync, existsSync, readFileSync, rmSync, writeFileSync, mkdirSync } = require('fs');
|
||||
const {
|
||||
mkdtempSync,
|
||||
existsSync,
|
||||
readFileSync,
|
||||
rmSync,
|
||||
writeFileSync,
|
||||
mkdirSync
|
||||
} = require('fs');
|
||||
const { join } = require('path');
|
||||
const { tmpdir } = require('os');
|
||||
|
||||
@@ -17,11 +24,11 @@ describe('expand-task command', () => {
|
||||
beforeEach(async () => {
|
||||
// Create test directory
|
||||
testDir = mkdtempSync(join(tmpdir(), 'task-master-expand-task-'));
|
||||
|
||||
|
||||
// Initialize test helpers
|
||||
const context = global.createTestContext('expand-task');
|
||||
helpers = context.helpers;
|
||||
|
||||
|
||||
// Copy .env file if it exists
|
||||
const mainEnvPath = join(__dirname, '../../../../.env');
|
||||
const testEnvPath = join(testDir, '.env');
|
||||
@@ -29,18 +36,20 @@ describe('expand-task command', () => {
|
||||
const envContent = readFileSync(mainEnvPath, 'utf8');
|
||||
writeFileSync(testEnvPath, envContent);
|
||||
}
|
||||
|
||||
|
||||
// Initialize task-master project
|
||||
const initResult = await helpers.taskMaster('init', ['-y'], { cwd: testDir });
|
||||
const initResult = await helpers.taskMaster('init', ['-y'], {
|
||||
cwd: testDir
|
||||
});
|
||||
expect(initResult).toHaveExitCode(0);
|
||||
|
||||
|
||||
// Ensure tasks.json exists (bug workaround)
|
||||
const tasksJsonPath = join(testDir, '.taskmaster/tasks/tasks.json');
|
||||
if (!existsSync(tasksJsonPath)) {
|
||||
mkdirSync(join(testDir, '.taskmaster/tasks'), { recursive: true });
|
||||
writeFileSync(tasksJsonPath, JSON.stringify({ master: { tasks: [] } }));
|
||||
}
|
||||
|
||||
|
||||
// Create simple task for expansion
|
||||
const simpleResult = await helpers.taskMaster(
|
||||
'add-task',
|
||||
@@ -48,19 +57,27 @@ describe('expand-task command', () => {
|
||||
{ cwd: testDir }
|
||||
);
|
||||
simpleTaskId = helpers.extractTaskId(simpleResult.stdout);
|
||||
|
||||
|
||||
// Create complex task for expansion
|
||||
const complexResult = await helpers.taskMaster(
|
||||
'add-task',
|
||||
['--prompt', 'Build a full-stack web application with React frontend and Node.js backend'],
|
||||
[
|
||||
'--prompt',
|
||||
'Build a full-stack web application with React frontend and Node.js backend'
|
||||
],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
complexTaskId = helpers.extractTaskId(complexResult.stdout);
|
||||
|
||||
|
||||
// Create manual task (no AI prompt)
|
||||
const manualResult = await helpers.taskMaster(
|
||||
'add-task',
|
||||
['--title', 'Manual task for expansion', '--description', 'This is a manually created task'],
|
||||
[
|
||||
'--title',
|
||||
'Manual task for expansion',
|
||||
'--description',
|
||||
'This is a manually created task'
|
||||
],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
manualTaskId = helpers.extractTaskId(manualResult.stdout);
|
||||
@@ -80,12 +97,14 @@ describe('expand-task command', () => {
|
||||
['--id', simpleTaskId],
|
||||
{ cwd: testDir, timeout: 45000 }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('Expanded');
|
||||
|
||||
|
||||
// Verify subtasks were created
|
||||
const showResult = await helpers.taskMaster('show', [simpleTaskId], { cwd: testDir });
|
||||
const showResult = await helpers.taskMaster('show', [simpleTaskId], {
|
||||
cwd: testDir
|
||||
});
|
||||
expect(showResult.stdout).toContain('Subtasks:');
|
||||
}, 60000);
|
||||
|
||||
@@ -95,11 +114,13 @@ describe('expand-task command', () => {
|
||||
['--id', complexTaskId, '--num', '3'],
|
||||
{ cwd: testDir, timeout: 45000 }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
|
||||
// Check that we got approximately 3 subtasks
|
||||
const showResult = await helpers.taskMaster('show', [complexTaskId], { cwd: testDir });
|
||||
const showResult = await helpers.taskMaster('show', [complexTaskId], {
|
||||
cwd: testDir
|
||||
});
|
||||
const subtaskMatches = showResult.stdout.match(/\d+\.\d+/g);
|
||||
expect(subtaskMatches).toBeTruthy();
|
||||
expect(subtaskMatches.length).toBeGreaterThanOrEqual(2);
|
||||
@@ -112,7 +133,7 @@ describe('expand-task command', () => {
|
||||
['--id', simpleTaskId, '--research'],
|
||||
{ cwd: testDir, timeout: 60000 }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('research');
|
||||
}, 90000);
|
||||
@@ -120,14 +141,21 @@ describe('expand-task command', () => {
|
||||
it('should expand with additional context', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'expand',
|
||||
['--id', manualTaskId, '--prompt', 'Focus on security best practices and testing'],
|
||||
[
|
||||
'--id',
|
||||
manualTaskId,
|
||||
'--prompt',
|
||||
'Focus on security best practices and testing'
|
||||
],
|
||||
{ cwd: testDir, timeout: 45000 }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
|
||||
// Verify context was used
|
||||
const showResult = await helpers.taskMaster('show', [manualTaskId], { cwd: testDir });
|
||||
const showResult = await helpers.taskMaster('show', [manualTaskId], {
|
||||
cwd: testDir
|
||||
});
|
||||
const outputLower = showResult.stdout.toLowerCase();
|
||||
expect(outputLower).toMatch(/security|test/);
|
||||
}, 60000);
|
||||
@@ -135,39 +163,37 @@ describe('expand-task command', () => {
|
||||
|
||||
describe('Bulk expansion', () => {
|
||||
it('should expand all tasks', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'expand',
|
||||
['--all'],
|
||||
{ cwd: testDir, timeout: 120000 }
|
||||
);
|
||||
|
||||
const result = await helpers.taskMaster('expand', ['--all'], {
|
||||
cwd: testDir,
|
||||
timeout: 120000
|
||||
});
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('Expanding all');
|
||||
|
||||
|
||||
// Verify all tasks have subtasks
|
||||
const tasksPath = join(testDir, '.taskmaster/tasks/tasks.json');
|
||||
const tasksData = JSON.parse(readFileSync(tasksPath, 'utf8'));
|
||||
const tasks = tasksData.master.tasks;
|
||||
|
||||
const tasksWithSubtasks = tasks.filter(t => t.subtasks && t.subtasks.length > 0);
|
||||
|
||||
const tasksWithSubtasks = tasks.filter(
|
||||
(t) => t.subtasks && t.subtasks.length > 0
|
||||
);
|
||||
expect(tasksWithSubtasks.length).toBeGreaterThanOrEqual(2);
|
||||
}, 150000);
|
||||
|
||||
it('should expand all with force flag', async () => {
|
||||
// First expand one task
|
||||
await helpers.taskMaster(
|
||||
'expand',
|
||||
['--id', simpleTaskId],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
await helpers.taskMaster('expand', ['--id', simpleTaskId], {
|
||||
cwd: testDir
|
||||
});
|
||||
|
||||
// Then expand all with force
|
||||
const result = await helpers.taskMaster(
|
||||
'expand',
|
||||
['--all', '--force'],
|
||||
{ cwd: testDir, timeout: 120000 }
|
||||
);
|
||||
|
||||
const result = await helpers.taskMaster('expand', ['--all', '--force'], {
|
||||
cwd: testDir,
|
||||
timeout: 120000
|
||||
});
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('force');
|
||||
}, 150000);
|
||||
@@ -176,30 +202,32 @@ describe('expand-task command', () => {
|
||||
describe('Specific task ranges', () => {
|
||||
it('should expand tasks by ID range', async () => {
|
||||
// Create more tasks
|
||||
await helpers.taskMaster(
|
||||
'add-task',
|
||||
['--prompt', 'Additional task 1'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
await helpers.taskMaster(
|
||||
'add-task',
|
||||
['--prompt', 'Additional task 2'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
await helpers.taskMaster('add-task', ['--prompt', 'Additional task 1'], {
|
||||
cwd: testDir
|
||||
});
|
||||
await helpers.taskMaster('add-task', ['--prompt', 'Additional task 2'], {
|
||||
cwd: testDir
|
||||
});
|
||||
|
||||
const result = await helpers.taskMaster(
|
||||
'expand',
|
||||
['--from', '2', '--to', '4'],
|
||||
{ cwd: testDir, timeout: 90000 }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
|
||||
// Verify tasks 2-4 were expanded
|
||||
const showResult2 = await helpers.taskMaster('show', ['2'], { cwd: testDir });
|
||||
const showResult3 = await helpers.taskMaster('show', ['3'], { cwd: testDir });
|
||||
const showResult4 = await helpers.taskMaster('show', ['4'], { cwd: testDir });
|
||||
|
||||
const showResult2 = await helpers.taskMaster('show', ['2'], {
|
||||
cwd: testDir
|
||||
});
|
||||
const showResult3 = await helpers.taskMaster('show', ['3'], {
|
||||
cwd: testDir
|
||||
});
|
||||
const showResult4 = await helpers.taskMaster('show', ['4'], {
|
||||
cwd: testDir
|
||||
});
|
||||
|
||||
expect(showResult2.stdout).toContain('Subtasks:');
|
||||
expect(showResult3.stdout).toContain('Subtasks:');
|
||||
expect(showResult4.stdout).toContain('Subtasks:');
|
||||
@@ -211,13 +239,17 @@ describe('expand-task command', () => {
|
||||
['--id', `${simpleTaskId},${complexTaskId}`],
|
||||
{ cwd: testDir, timeout: 90000 }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
|
||||
// Both tasks should have subtasks
|
||||
const showResult1 = await helpers.taskMaster('show', [simpleTaskId], { cwd: testDir });
|
||||
const showResult2 = await helpers.taskMaster('show', [complexTaskId], { cwd: testDir });
|
||||
|
||||
const showResult1 = await helpers.taskMaster('show', [simpleTaskId], {
|
||||
cwd: testDir
|
||||
});
|
||||
const showResult2 = await helpers.taskMaster('show', [complexTaskId], {
|
||||
cwd: testDir
|
||||
});
|
||||
|
||||
expect(showResult1.stdout).toContain('Subtasks:');
|
||||
expect(showResult2.stdout).toContain('Subtasks:');
|
||||
}, 120000);
|
||||
@@ -225,31 +257,28 @@ describe('expand-task command', () => {
|
||||
|
||||
describe('Error handling', () => {
|
||||
it('should fail for non-existent task ID', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'expand',
|
||||
['--id', '99999'],
|
||||
{ cwd: testDir, allowFailure: true }
|
||||
);
|
||||
|
||||
const result = await helpers.taskMaster('expand', ['--id', '99999'], {
|
||||
cwd: testDir,
|
||||
allowFailure: true
|
||||
});
|
||||
|
||||
expect(result.exitCode).not.toBe(0);
|
||||
expect(result.stderr).toContain('not found');
|
||||
});
|
||||
|
||||
it('should skip already expanded tasks without force', async () => {
|
||||
// First expansion
|
||||
await helpers.taskMaster(
|
||||
'expand',
|
||||
['--id', simpleTaskId],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
await helpers.taskMaster('expand', ['--id', simpleTaskId], {
|
||||
cwd: testDir
|
||||
});
|
||||
|
||||
// Second expansion without force
|
||||
const result = await helpers.taskMaster(
|
||||
'expand',
|
||||
['--id', simpleTaskId],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout.toLowerCase()).toMatch(/already|skip/);
|
||||
});
|
||||
@@ -260,7 +289,7 @@ describe('expand-task command', () => {
|
||||
['--id', simpleTaskId, '--num', '-1'],
|
||||
{ cwd: testDir, allowFailure: true }
|
||||
);
|
||||
|
||||
|
||||
expect(result.exitCode).not.toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -269,22 +298,22 @@ describe('expand-task command', () => {
|
||||
it('should expand tasks in specific tag', async () => {
|
||||
// Create tag and tagged task
|
||||
await helpers.taskMaster('add-tag', ['feature-tag'], { cwd: testDir });
|
||||
|
||||
|
||||
const taggedResult = await helpers.taskMaster(
|
||||
'add-task',
|
||||
['--prompt', 'Tagged task for expansion', '--tag', 'feature-tag'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
const taggedId = helpers.extractTaskId(taggedResult.stdout);
|
||||
|
||||
|
||||
const result = await helpers.taskMaster(
|
||||
'expand',
|
||||
['--id', taggedId, '--tag', 'feature-tag'],
|
||||
{ cwd: testDir, timeout: 45000 }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
|
||||
// Verify expansion in correct tag
|
||||
const showResult = await helpers.taskMaster(
|
||||
'show',
|
||||
@@ -302,27 +331,27 @@ describe('expand-task command', () => {
|
||||
['--id', simpleTaskId, '--model', 'gpt-3.5-turbo'],
|
||||
{ cwd: testDir, timeout: 45000 }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
}, 60000);
|
||||
});
|
||||
|
||||
describe('Output validation', () => {
|
||||
it('should create valid subtask structure', async () => {
|
||||
await helpers.taskMaster(
|
||||
'expand',
|
||||
['--id', complexTaskId],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
await helpers.taskMaster('expand', ['--id', complexTaskId], {
|
||||
cwd: testDir
|
||||
});
|
||||
|
||||
const tasksPath = join(testDir, '.taskmaster/tasks/tasks.json');
|
||||
const tasksData = JSON.parse(readFileSync(tasksPath, 'utf8'));
|
||||
const task = tasksData.master.tasks.find(t => t.id === parseInt(complexTaskId));
|
||||
|
||||
const task = tasksData.master.tasks.find(
|
||||
(t) => t.id === parseInt(complexTaskId)
|
||||
);
|
||||
|
||||
expect(task.subtasks).toBeDefined();
|
||||
expect(Array.isArray(task.subtasks)).toBe(true);
|
||||
expect(task.subtasks.length).toBeGreaterThan(0);
|
||||
|
||||
|
||||
// Validate subtask structure
|
||||
task.subtasks.forEach((subtask, index) => {
|
||||
expect(subtask.id).toBe(`${complexTaskId}.${index + 1}`);
|
||||
@@ -340,17 +369,15 @@ describe('expand-task command', () => {
|
||||
{ cwd: testDir }
|
||||
);
|
||||
const depTaskId = helpers.extractTaskId(depResult.stdout);
|
||||
|
||||
|
||||
// Expand the task
|
||||
await helpers.taskMaster(
|
||||
'expand',
|
||||
['--id', depTaskId],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
await helpers.taskMaster('expand', ['--id', depTaskId], { cwd: testDir });
|
||||
|
||||
// Check dependencies are preserved
|
||||
const showResult = await helpers.taskMaster('show', [depTaskId], { cwd: testDir });
|
||||
const showResult = await helpers.taskMaster('show', [depTaskId], {
|
||||
cwd: testDir
|
||||
});
|
||||
expect(showResult.stdout).toContain(`Dependencies: ${simpleTaskId}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
804
tests/e2e/tests/commands/list.test.js
Normal file
804
tests/e2e/tests/commands/list.test.js
Normal file
@@ -0,0 +1,804 @@
|
||||
/**
|
||||
* Comprehensive E2E tests for list command
|
||||
* Tests all aspects of task listing including filtering and display options
|
||||
*/
|
||||
|
||||
const {
|
||||
mkdtempSync,
|
||||
existsSync,
|
||||
readFileSync,
|
||||
rmSync,
|
||||
writeFileSync,
|
||||
mkdirSync
|
||||
} = require('fs');
|
||||
const { join } = require('path');
|
||||
const { tmpdir } = require('os');
|
||||
const path = require('path');
|
||||
|
||||
describe('list command', () => {
|
||||
let testDir;
|
||||
let helpers;
|
||||
|
||||
beforeEach(async () => {
|
||||
// Create test directory
|
||||
testDir = mkdtempSync(join(tmpdir(), 'task-master-list-'));
|
||||
|
||||
// Initialize test helpers
|
||||
const context = global.createTestContext('list');
|
||||
helpers = context.helpers;
|
||||
|
||||
// Copy .env file if it exists
|
||||
const mainEnvPath = join(__dirname, '../../../../.env');
|
||||
const testEnvPath = join(testDir, '.env');
|
||||
if (existsSync(mainEnvPath)) {
|
||||
const envContent = readFileSync(mainEnvPath, 'utf8');
|
||||
writeFileSync(testEnvPath, envContent);
|
||||
}
|
||||
|
||||
// Initialize task-master project
|
||||
const initResult = await helpers.taskMaster('init', ['-y'], {
|
||||
cwd: testDir
|
||||
});
|
||||
expect(initResult).toHaveExitCode(0);
|
||||
|
||||
// Ensure tasks.json exists (bug workaround)
|
||||
const tasksJsonPath = join(testDir, '.taskmaster/tasks/tasks.json');
|
||||
if (!existsSync(tasksJsonPath)) {
|
||||
mkdirSync(join(testDir, '.taskmaster/tasks'), { recursive: true });
|
||||
writeFileSync(tasksJsonPath, JSON.stringify({ master: { tasks: [] } }));
|
||||
}
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Clean up test directory
|
||||
if (testDir && existsSync(testDir)) {
|
||||
rmSync(testDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
describe('Basic listing', () => {
|
||||
it('should list all tasks', async () => {
|
||||
// Create some test tasks
|
||||
await helpers.taskMaster(
|
||||
'add-task',
|
||||
['--title', 'Task 1', '--description', 'First task'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
await helpers.taskMaster(
|
||||
'add-task',
|
||||
['--title', 'Task 2', '--description', 'Second task'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
const result = await helpers.taskMaster('list', [], { cwd: testDir });
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('Task 1');
|
||||
expect(result.stdout).toContain('Task 2');
|
||||
expect(result.stdout).toContain('Project Dashboard');
|
||||
expect(result.stdout).toContain('ID');
|
||||
expect(result.stdout).toContain('Title');
|
||||
expect(result.stdout).toContain('Status');
|
||||
expect(result.stdout).toContain('Priority');
|
||||
expect(result.stdout).toContain('Dependencies');
|
||||
});
|
||||
|
||||
it('should show empty list message when no tasks exist', async () => {
|
||||
const result = await helpers.taskMaster('list', [], { cwd: testDir });
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('No tasks found');
|
||||
});
|
||||
|
||||
it('should display task progress dashboard', async () => {
|
||||
// Create tasks with different statuses
|
||||
const task1 = await helpers.taskMaster(
|
||||
'add-task',
|
||||
['--title', 'Completed task', '--description', 'Done'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
const taskId1 = helpers.extractTaskId(task1.stdout);
|
||||
await helpers.taskMaster(
|
||||
'set-status',
|
||||
['--id', taskId1, '--status', 'done'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
await helpers.taskMaster(
|
||||
'add-task',
|
||||
['--title', 'In progress task', '--description', 'Working on it'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
const result = await helpers.taskMaster('list', [], { cwd: testDir });
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('Project Dashboard');
|
||||
expect(result.stdout).toContain('Tasks Progress:');
|
||||
expect(result.stdout).toContain('Done:');
|
||||
expect(result.stdout).toContain('In Progress:');
|
||||
expect(result.stdout).toContain('Pending:');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Status filtering', () => {
|
||||
beforeEach(async () => {
|
||||
// Create tasks with different statuses
|
||||
const task1 = await helpers.taskMaster(
|
||||
'add-task',
|
||||
['--title', 'Pending task', '--description', 'Not started'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
const task2 = await helpers.taskMaster(
|
||||
'add-task',
|
||||
['--title', 'In progress task', '--description', 'Working on it'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
const taskId2 = helpers.extractTaskId(task2.stdout);
|
||||
await helpers.taskMaster(
|
||||
'set-status',
|
||||
['--id', taskId2, '--status', 'in-progress'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
const task3 = await helpers.taskMaster(
|
||||
'add-task',
|
||||
['--title', 'Done task', '--description', 'Completed'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
const taskId3 = helpers.extractTaskId(task3.stdout);
|
||||
await helpers.taskMaster(
|
||||
'set-status',
|
||||
['--id', taskId3, '--status', 'done'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
const task4 = await helpers.taskMaster(
|
||||
'add-task',
|
||||
['--title', 'Blocked task', '--description', 'Blocked by dependency'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
const taskId4 = helpers.extractTaskId(task4.stdout);
|
||||
await helpers.taskMaster(
|
||||
'set-status',
|
||||
['--id', taskId4, '--status', 'blocked'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
const task5 = await helpers.taskMaster(
|
||||
'add-task',
|
||||
['--title', 'Deferred task', '--description', 'Postponed'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
const taskId5 = helpers.extractTaskId(task5.stdout);
|
||||
await helpers.taskMaster(
|
||||
'set-status',
|
||||
['--id', taskId5, '--status', 'deferred'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
const task6 = await helpers.taskMaster(
|
||||
'add-task',
|
||||
['--title', 'Cancelled task', '--description', 'No longer needed'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
const taskId6 = helpers.extractTaskId(task6.stdout);
|
||||
await helpers.taskMaster(
|
||||
'set-status',
|
||||
['--id', taskId6, '--status', 'cancelled'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
});
|
||||
|
||||
it('should filter by pending status', async () => {
|
||||
const result = await helpers.taskMaster('list', ['--status', 'pending'], {
|
||||
cwd: testDir
|
||||
});
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('Pending task');
|
||||
expect(result.stdout).not.toContain('In progress task');
|
||||
expect(result.stdout).not.toContain('Done task');
|
||||
expect(result.stdout).toContain('Filtered by status: pending');
|
||||
});
|
||||
|
||||
it('should filter by in-progress status', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'list',
|
||||
['--status', 'in-progress'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('In progress task');
|
||||
expect(result.stdout).not.toContain('Pending task');
|
||||
expect(result.stdout).not.toContain('Done task');
|
||||
});
|
||||
|
||||
it('should filter by done status', async () => {
|
||||
const result = await helpers.taskMaster('list', ['--status', 'done'], {
|
||||
cwd: testDir
|
||||
});
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('Done task');
|
||||
expect(result.stdout).not.toContain('Pending task');
|
||||
expect(result.stdout).not.toContain('In progress task');
|
||||
});
|
||||
|
||||
it('should filter by blocked status', async () => {
|
||||
const result = await helpers.taskMaster('list', ['--status', 'blocked'], {
|
||||
cwd: testDir
|
||||
});
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('Blocked task');
|
||||
expect(result.stdout).not.toContain('Pending task');
|
||||
});
|
||||
|
||||
it('should filter by deferred status', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'list',
|
||||
['--status', 'deferred'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('Deferred task');
|
||||
expect(result.stdout).not.toContain('Pending task');
|
||||
});
|
||||
|
||||
it('should filter by cancelled status', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'list',
|
||||
['--status', 'cancelled'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('Cancelled task');
|
||||
expect(result.stdout).not.toContain('Pending task');
|
||||
});
|
||||
|
||||
it('should handle multiple statuses with comma separation', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'list',
|
||||
['--status', 'pending,in-progress'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('Pending task');
|
||||
expect(result.stdout).toContain('In progress task');
|
||||
expect(result.stdout).not.toContain('Done task');
|
||||
expect(result.stdout).not.toContain('Blocked task');
|
||||
});
|
||||
|
||||
it('should show empty message for non-existent status filter', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'list',
|
||||
['--status', 'invalid-status'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain(
|
||||
"No tasks with status 'invalid-status' found"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Priority display', () => {
|
||||
it('should display task priorities correctly', async () => {
|
||||
// Create tasks with different priorities
|
||||
await helpers.taskMaster(
|
||||
'add-task',
|
||||
[
|
||||
'--title',
|
||||
'High priority task',
|
||||
'--description',
|
||||
'Urgent',
|
||||
'--priority',
|
||||
'high'
|
||||
],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
await helpers.taskMaster(
|
||||
'add-task',
|
||||
[
|
||||
'--title',
|
||||
'Medium priority task',
|
||||
'--description',
|
||||
'Normal',
|
||||
'--priority',
|
||||
'medium'
|
||||
],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
await helpers.taskMaster(
|
||||
'add-task',
|
||||
[
|
||||
'--title',
|
||||
'Low priority task',
|
||||
'--description',
|
||||
'Can wait',
|
||||
'--priority',
|
||||
'low'
|
||||
],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
const result = await helpers.taskMaster('list', [], { cwd: testDir });
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toMatch(/high/i);
|
||||
expect(result.stdout).toMatch(/medium/i);
|
||||
expect(result.stdout).toMatch(/low/i);
|
||||
|
||||
// Check priority breakdown
|
||||
expect(result.stdout).toContain('Priority Breakdown:');
|
||||
expect(result.stdout).toContain('High priority:');
|
||||
expect(result.stdout).toContain('Medium priority:');
|
||||
expect(result.stdout).toContain('Low priority:');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Subtasks display', () => {
|
||||
let parentTaskId;
|
||||
|
||||
beforeEach(async () => {
|
||||
// Create a parent task with subtasks
|
||||
const parentResult = await helpers.taskMaster(
|
||||
'add-task',
|
||||
['--title', 'Parent task', '--description', 'Has subtasks'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
parentTaskId = helpers.extractTaskId(parentResult.stdout);
|
||||
|
||||
// Add subtasks
|
||||
await helpers.taskMaster(
|
||||
'add-subtask',
|
||||
[
|
||||
'--parent',
|
||||
parentTaskId,
|
||||
'--title',
|
||||
'Subtask 1',
|
||||
'--description',
|
||||
'First subtask'
|
||||
],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
await helpers.taskMaster(
|
||||
'add-subtask',
|
||||
[
|
||||
'--parent',
|
||||
parentTaskId,
|
||||
'--title',
|
||||
'Subtask 2',
|
||||
'--description',
|
||||
'Second subtask'
|
||||
],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
});
|
||||
|
||||
it('should not show subtasks by default', async () => {
|
||||
const result = await helpers.taskMaster('list', [], { cwd: testDir });
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('Parent task');
|
||||
expect(result.stdout).not.toContain('Subtask 1');
|
||||
expect(result.stdout).not.toContain('Subtask 2');
|
||||
});
|
||||
|
||||
it('should show subtasks with --with-subtasks flag', async () => {
|
||||
const result = await helpers.taskMaster('list', ['--with-subtasks'], {
|
||||
cwd: testDir
|
||||
});
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('Parent task');
|
||||
expect(result.stdout).toContain('Subtask 1');
|
||||
expect(result.stdout).toContain('Subtask 2');
|
||||
expect(result.stdout).toContain(`${parentTaskId}.1`);
|
||||
expect(result.stdout).toContain(`${parentTaskId}.2`);
|
||||
expect(result.stdout).toContain('└─');
|
||||
});
|
||||
|
||||
it('should include subtasks in progress calculation', async () => {
|
||||
const result = await helpers.taskMaster('list', ['--with-subtasks'], {
|
||||
cwd: testDir
|
||||
});
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('Subtasks Progress:');
|
||||
expect(result.stdout).toMatch(/Completed:\s*0\/2/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Tag filtering', () => {
|
||||
beforeEach(async () => {
|
||||
// Create a new tag
|
||||
await helpers.taskMaster(
|
||||
'add-tag',
|
||||
['feature-branch', '--description', 'Feature branch tasks'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
// Add tasks to master tag
|
||||
await helpers.taskMaster(
|
||||
'add-task',
|
||||
['--title', 'Master task 1', '--description', 'In master tag'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
// Switch to feature tag and add tasks
|
||||
await helpers.taskMaster('use-tag', ['feature-branch'], { cwd: testDir });
|
||||
await helpers.taskMaster(
|
||||
'add-task',
|
||||
[
|
||||
'--title',
|
||||
'Feature task 1',
|
||||
'--description',
|
||||
'In feature tag',
|
||||
'--tag',
|
||||
'feature-branch'
|
||||
],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
});
|
||||
|
||||
it('should list tasks from specific tag', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'list',
|
||||
['--tag', 'feature-branch'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('Feature task 1');
|
||||
expect(result.stdout).not.toContain('Master task 1');
|
||||
expect(result.stdout).toContain('[feature-branch]');
|
||||
});
|
||||
|
||||
it('should list tasks from master tag by default', async () => {
|
||||
// Switch back to master tag
|
||||
await helpers.taskMaster('use-tag', ['master'], { cwd: testDir });
|
||||
|
||||
const result = await helpers.taskMaster('list', [], { cwd: testDir });
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('Master task 1');
|
||||
expect(result.stdout).not.toContain('Feature task 1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Dependencies display', () => {
|
||||
it('should show task dependencies correctly', async () => {
|
||||
// Create dependency tasks
|
||||
const dep1 = await helpers.taskMaster(
|
||||
'add-task',
|
||||
['--title', 'Dependency 1', '--description', 'First dependency'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
const depId1 = helpers.extractTaskId(dep1.stdout);
|
||||
|
||||
const dep2 = await helpers.taskMaster(
|
||||
'add-task',
|
||||
['--title', 'Dependency 2', '--description', 'Second dependency'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
const depId2 = helpers.extractTaskId(dep2.stdout);
|
||||
|
||||
// Create task with dependencies
|
||||
await helpers.taskMaster(
|
||||
'add-task',
|
||||
[
|
||||
'--title',
|
||||
'Task with dependencies',
|
||||
'--description',
|
||||
'Depends on other tasks',
|
||||
'--dependencies',
|
||||
`${depId1},${depId2}`
|
||||
],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
const result = await helpers.taskMaster('list', [], { cwd: testDir });
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain(depId1);
|
||||
expect(result.stdout).toContain(depId2);
|
||||
});
|
||||
|
||||
it('should show dependency status with colors', async () => {
|
||||
// Create dependency task
|
||||
const dep = await helpers.taskMaster(
|
||||
'add-task',
|
||||
['--title', 'Completed dependency', '--description', 'Done'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
const depId = helpers.extractTaskId(dep.stdout);
|
||||
|
||||
// Mark dependency as done
|
||||
await helpers.taskMaster(
|
||||
'set-status',
|
||||
['--id', depId, '--status', 'done'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
// Create task with dependency
|
||||
await helpers.taskMaster(
|
||||
'add-task',
|
||||
[
|
||||
'--title',
|
||||
'Task with completed dependency',
|
||||
'--description',
|
||||
'Has satisfied dependency',
|
||||
'--dependencies',
|
||||
depId
|
||||
],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
const result = await helpers.taskMaster('list', [], { cwd: testDir });
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
// The done dependency should be shown (implementation uses color coding)
|
||||
expect(result.stdout).toContain(depId);
|
||||
});
|
||||
|
||||
it('should show dependency dashboard', async () => {
|
||||
// Create some tasks with dependencies
|
||||
const task1 = await helpers.taskMaster(
|
||||
'add-task',
|
||||
['--title', 'Independent task', '--description', 'No dependencies'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
const task2 = await helpers.taskMaster(
|
||||
'add-task',
|
||||
['--title', 'Dependency task', '--description', 'Will be depended on'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
const taskId2 = helpers.extractTaskId(task2.stdout);
|
||||
|
||||
await helpers.taskMaster(
|
||||
'add-task',
|
||||
[
|
||||
'--title',
|
||||
'Dependent task',
|
||||
'--description',
|
||||
'Depends on task 2',
|
||||
'--dependencies',
|
||||
taskId2
|
||||
],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
const result = await helpers.taskMaster('list', [], { cwd: testDir });
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('Dependency Status & Next Task');
|
||||
expect(result.stdout).toContain('Tasks with no dependencies:');
|
||||
expect(result.stdout).toContain('Tasks ready to work on:');
|
||||
expect(result.stdout).toContain('Tasks blocked by dependencies:');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Complexity display', () => {
|
||||
it('should show complexity scores when available', async () => {
|
||||
// Create tasks
|
||||
await helpers.taskMaster(
|
||||
'add-task',
|
||||
['--prompt', 'Build a complex authentication system'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
await helpers.taskMaster(
|
||||
'add-task',
|
||||
['--prompt', 'Create a simple hello world endpoint'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
// Run complexity analysis
|
||||
const analyzeResult = await helpers.taskMaster('analyze-complexity', [], {
|
||||
cwd: testDir,
|
||||
timeout: 60000
|
||||
});
|
||||
|
||||
if (analyzeResult.exitCode === 0) {
|
||||
const result = await helpers.taskMaster('list', [], { cwd: testDir });
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('Complexity');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Next task recommendation', () => {
|
||||
it('should show next task recommendation', async () => {
|
||||
// Create tasks with different priorities and dependencies
|
||||
const task1 = await helpers.taskMaster(
|
||||
'add-task',
|
||||
[
|
||||
'--title',
|
||||
'High priority task',
|
||||
'--description',
|
||||
'Should be done first',
|
||||
'--priority',
|
||||
'high'
|
||||
],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
const result = await helpers.taskMaster('list', [], { cwd: testDir });
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('Next Task to Work On');
|
||||
expect(result.stdout).toContain('Start working:');
|
||||
expect(result.stdout).toContain('task-master set-status');
|
||||
expect(result.stdout).toContain('View details:');
|
||||
expect(result.stdout).toContain('task-master show');
|
||||
});
|
||||
|
||||
it('should show no eligible task when all are blocked', async () => {
|
||||
// Create blocked task
|
||||
const task1 = await helpers.taskMaster(
|
||||
'add-task',
|
||||
['--title', 'Prerequisite', '--description', 'Must be done first'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
const taskId1 = helpers.extractTaskId(task1.stdout);
|
||||
|
||||
// Create task depending on it
|
||||
await helpers.taskMaster(
|
||||
'add-task',
|
||||
[
|
||||
'--title',
|
||||
'Blocked task',
|
||||
'--description',
|
||||
'Waiting for prerequisite',
|
||||
'--dependencies',
|
||||
taskId1
|
||||
],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
// Mark first task as done
|
||||
await helpers.taskMaster(
|
||||
'set-status',
|
||||
['--id', taskId1, '--status', 'done'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
const result = await helpers.taskMaster('list', [], { cwd: testDir });
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
// Should recommend the unblocked task
|
||||
expect(result.stdout).toContain('Next Task to Work On');
|
||||
expect(result.stdout).toContain('Blocked task');
|
||||
});
|
||||
});
|
||||
|
||||
describe('File path handling', () => {
|
||||
it('should use custom tasks file path', async () => {
|
||||
// Create custom tasks file
|
||||
const customPath = join(testDir, 'custom-tasks.json');
|
||||
writeFileSync(
|
||||
customPath,
|
||||
JSON.stringify({
|
||||
master: {
|
||||
tasks: [
|
||||
{
|
||||
id: 1,
|
||||
title: 'Custom file task',
|
||||
description: 'Task in custom file',
|
||||
status: 'pending',
|
||||
priority: 'medium',
|
||||
dependencies: []
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
const result = await helpers.taskMaster('list', ['--file', customPath], {
|
||||
cwd: testDir
|
||||
});
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('Custom file task');
|
||||
expect(result.stdout).toContain(`Listing tasks from: ${customPath}`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error handling', () => {
|
||||
it('should handle missing tasks file gracefully', async () => {
|
||||
const nonExistentPath = join(testDir, 'non-existent.json');
|
||||
const result = await helpers.taskMaster(
|
||||
'list',
|
||||
['--file', nonExistentPath],
|
||||
{ cwd: testDir, allowFailure: true }
|
||||
);
|
||||
|
||||
expect(result.exitCode).not.toBe(0);
|
||||
expect(result.stderr).toContain('Error');
|
||||
});
|
||||
|
||||
it('should handle invalid JSON in tasks file', async () => {
|
||||
const invalidPath = join(testDir, 'invalid.json');
|
||||
writeFileSync(invalidPath, '{ invalid json }');
|
||||
|
||||
const result = await helpers.taskMaster('list', ['--file', invalidPath], {
|
||||
cwd: testDir,
|
||||
allowFailure: true
|
||||
});
|
||||
|
||||
expect(result.exitCode).not.toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Performance with many tasks', () => {
|
||||
it('should handle listing 50+ tasks efficiently', async () => {
|
||||
// Create many tasks
|
||||
const promises = [];
|
||||
for (let i = 1; i <= 50; i++) {
|
||||
promises.push(
|
||||
helpers.taskMaster(
|
||||
'add-task',
|
||||
['--title', `Task ${i}`, '--description', `Description ${i}`],
|
||||
{ cwd: testDir }
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
const startTime = Date.now();
|
||||
const result = await helpers.taskMaster('list', [], { cwd: testDir });
|
||||
const endTime = Date.now();
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('Task 1');
|
||||
expect(result.stdout).toContain('Task 50');
|
||||
|
||||
// Should complete within reasonable time (5 seconds)
|
||||
expect(endTime - startTime).toBeLessThan(5000);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Display formatting', () => {
|
||||
it('should truncate long titles appropriately', async () => {
|
||||
const longTitle =
|
||||
'This is a very long task title that should be truncated in the display to fit within the table column width constraints';
|
||||
await helpers.taskMaster(
|
||||
'add-task',
|
||||
['--title', longTitle, '--description', 'Task with long title'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
const result = await helpers.taskMaster('list', [], { cwd: testDir });
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
// Should contain at least part of the title
|
||||
expect(result.stdout).toContain('This is a very long task title');
|
||||
});
|
||||
|
||||
it('should show suggested next steps', async () => {
|
||||
await helpers.taskMaster(
|
||||
'add-task',
|
||||
['--title', 'Sample task', '--description', 'For testing'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
const result = await helpers.taskMaster('list', [], { cwd: testDir });
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('Suggested Next Steps:');
|
||||
expect(result.stdout).toContain('task-master next');
|
||||
expect(result.stdout).toContain('task-master expand');
|
||||
expect(result.stdout).toContain('task-master set-status');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -3,7 +3,14 @@
|
||||
* Tests all aspects of PRD parsing including task generation, research mode, and various formats
|
||||
*/
|
||||
|
||||
const { mkdtempSync, existsSync, readFileSync, rmSync, writeFileSync, mkdirSync } = require('fs');
|
||||
const {
|
||||
mkdtempSync,
|
||||
existsSync,
|
||||
readFileSync,
|
||||
rmSync,
|
||||
writeFileSync,
|
||||
mkdirSync
|
||||
} = require('fs');
|
||||
const { join } = require('path');
|
||||
const { tmpdir } = require('os');
|
||||
|
||||
@@ -14,11 +21,11 @@ describe('parse-prd command', () => {
|
||||
beforeEach(async () => {
|
||||
// Create test directory
|
||||
testDir = mkdtempSync(join(tmpdir(), 'task-master-parse-prd-'));
|
||||
|
||||
|
||||
// Initialize test helpers
|
||||
const context = global.createTestContext('parse-prd');
|
||||
helpers = context.helpers;
|
||||
|
||||
|
||||
// Copy .env file if it exists
|
||||
const mainEnvPath = join(__dirname, '../../../../.env');
|
||||
const testEnvPath = join(testDir, '.env');
|
||||
@@ -26,9 +33,11 @@ describe('parse-prd command', () => {
|
||||
const envContent = readFileSync(mainEnvPath, 'utf8');
|
||||
writeFileSync(testEnvPath, envContent);
|
||||
}
|
||||
|
||||
|
||||
// Initialize task-master project
|
||||
const initResult = await helpers.taskMaster('init', ['-y'], { cwd: testDir });
|
||||
const initResult = await helpers.taskMaster('init', ['-y'], {
|
||||
cwd: testDir
|
||||
});
|
||||
expect(initResult).toHaveExitCode(0);
|
||||
});
|
||||
|
||||
@@ -49,23 +58,22 @@ describe('parse-prd command', () => {
|
||||
- Login with JWT tokens
|
||||
- Password reset functionality
|
||||
- User profile management`;
|
||||
|
||||
|
||||
const prdPath = join(testDir, 'test-prd.txt');
|
||||
writeFileSync(prdPath, prdContent);
|
||||
|
||||
const result = await helpers.taskMaster(
|
||||
'parse-prd',
|
||||
[prdPath],
|
||||
{ cwd: testDir, timeout: 45000 }
|
||||
);
|
||||
|
||||
|
||||
const result = await helpers.taskMaster('parse-prd', [prdPath], {
|
||||
cwd: testDir,
|
||||
timeout: 45000
|
||||
});
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('Tasks generated successfully');
|
||||
|
||||
|
||||
// Verify tasks.json was created
|
||||
const tasksPath = join(testDir, '.taskmaster/tasks/tasks.json');
|
||||
expect(existsSync(tasksPath)).toBe(true);
|
||||
|
||||
|
||||
const tasks = JSON.parse(readFileSync(tasksPath, 'utf8'));
|
||||
expect(tasks.master.tasks.length).toBeGreaterThan(0);
|
||||
}, 60000);
|
||||
@@ -76,13 +84,12 @@ describe('parse-prd command', () => {
|
||||
const defaultPrdPath = join(testDir, '.taskmaster/prd.txt');
|
||||
mkdirSync(join(testDir, '.taskmaster'), { recursive: true });
|
||||
writeFileSync(defaultPrdPath, prdContent);
|
||||
|
||||
const result = await helpers.taskMaster(
|
||||
'parse-prd',
|
||||
[],
|
||||
{ cwd: testDir, timeout: 45000 }
|
||||
);
|
||||
|
||||
|
||||
const result = await helpers.taskMaster('parse-prd', [], {
|
||||
cwd: testDir,
|
||||
timeout: 45000
|
||||
});
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('Using default PRD file');
|
||||
}, 60000);
|
||||
@@ -91,13 +98,13 @@ describe('parse-prd command', () => {
|
||||
const prdContent = 'Create a REST API for blog management';
|
||||
const prdPath = join(testDir, 'api-prd.txt');
|
||||
writeFileSync(prdPath, prdContent);
|
||||
|
||||
|
||||
const result = await helpers.taskMaster(
|
||||
'parse-prd',
|
||||
['--input', prdPath],
|
||||
{ cwd: testDir, timeout: 45000 }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('Tasks generated successfully');
|
||||
}, 60000);
|
||||
@@ -105,18 +112,19 @@ describe('parse-prd command', () => {
|
||||
|
||||
describe('Task generation options', () => {
|
||||
it('should generate custom number of tasks', async () => {
|
||||
const prdContent = 'Build a comprehensive e-commerce platform with all features';
|
||||
const prdContent =
|
||||
'Build a comprehensive e-commerce platform with all features';
|
||||
const prdPath = join(testDir, 'ecommerce-prd.txt');
|
||||
writeFileSync(prdPath, prdContent);
|
||||
|
||||
|
||||
const result = await helpers.taskMaster(
|
||||
'parse-prd',
|
||||
[prdPath, '--num-tasks', '5'],
|
||||
{ cwd: testDir, timeout: 45000 }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
|
||||
const tasksPath = join(testDir, '.taskmaster/tasks/tasks.json');
|
||||
const tasks = JSON.parse(readFileSync(tasksPath, 'utf8'));
|
||||
// AI might generate slightly more or less, but should be close to 5
|
||||
@@ -128,18 +136,18 @@ describe('parse-prd command', () => {
|
||||
const prdContent = 'Build a chat application';
|
||||
const prdPath = join(testDir, 'chat-prd.txt');
|
||||
writeFileSync(prdPath, prdContent);
|
||||
|
||||
|
||||
const customOutput = join(testDir, 'custom-tasks.json');
|
||||
|
||||
|
||||
const result = await helpers.taskMaster(
|
||||
'parse-prd',
|
||||
[prdPath, '--output', customOutput],
|
||||
{ cwd: testDir, timeout: 45000 }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(existsSync(customOutput)).toBe(true);
|
||||
|
||||
|
||||
const tasks = JSON.parse(readFileSync(customOutput, 'utf8'));
|
||||
expect(tasks.master.tasks.length).toBeGreaterThan(0);
|
||||
}, 60000);
|
||||
@@ -151,21 +159,21 @@ describe('parse-prd command', () => {
|
||||
const initialPrd = 'Build feature A';
|
||||
const prdPath1 = join(testDir, 'initial.txt');
|
||||
writeFileSync(prdPath1, initialPrd);
|
||||
|
||||
|
||||
await helpers.taskMaster('parse-prd', [prdPath1], { cwd: testDir });
|
||||
|
||||
|
||||
// Create new PRD
|
||||
const newPrd = 'Build feature B';
|
||||
const prdPath2 = join(testDir, 'new.txt');
|
||||
writeFileSync(prdPath2, newPrd);
|
||||
|
||||
|
||||
// Parse with force flag
|
||||
const result = await helpers.taskMaster(
|
||||
'parse-prd',
|
||||
[prdPath2, '--force'],
|
||||
{ cwd: testDir, timeout: 45000 }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).not.toContain('overwrite existing tasks?');
|
||||
}, 90000);
|
||||
@@ -175,59 +183,62 @@ describe('parse-prd command', () => {
|
||||
const initialPrd = 'Build authentication system';
|
||||
const prdPath1 = join(testDir, 'auth-prd.txt');
|
||||
writeFileSync(prdPath1, initialPrd);
|
||||
|
||||
|
||||
await helpers.taskMaster('parse-prd', [prdPath1], { cwd: testDir });
|
||||
|
||||
|
||||
const tasksPath = join(testDir, '.taskmaster/tasks/tasks.json');
|
||||
const initialTasks = JSON.parse(readFileSync(tasksPath, 'utf8'));
|
||||
const initialCount = initialTasks.master.tasks.length;
|
||||
|
||||
|
||||
// Create additional PRD
|
||||
const additionalPrd = 'Build user profile features';
|
||||
const prdPath2 = join(testDir, 'profile-prd.txt');
|
||||
writeFileSync(prdPath2, additionalPrd);
|
||||
|
||||
|
||||
// Parse with append flag
|
||||
const result = await helpers.taskMaster(
|
||||
'parse-prd',
|
||||
[prdPath2, '--append'],
|
||||
{ cwd: testDir, timeout: 45000 }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('Appending to existing tasks');
|
||||
|
||||
|
||||
const finalTasks = JSON.parse(readFileSync(tasksPath, 'utf8'));
|
||||
expect(finalTasks.master.tasks.length).toBeGreaterThan(initialCount);
|
||||
|
||||
|
||||
// Verify IDs are sequential
|
||||
const maxId = Math.max(...finalTasks.master.tasks.map(t => t.id));
|
||||
const maxId = Math.max(...finalTasks.master.tasks.map((t) => t.id));
|
||||
expect(maxId).toBe(finalTasks.master.tasks.length);
|
||||
}, 90000);
|
||||
});
|
||||
|
||||
describe('Research mode', () => {
|
||||
it('should use research mode with --research flag', async () => {
|
||||
const prdContent = 'Build a machine learning pipeline for recommendation system';
|
||||
const prdContent =
|
||||
'Build a machine learning pipeline for recommendation system';
|
||||
const prdPath = join(testDir, 'ml-prd.txt');
|
||||
writeFileSync(prdPath, prdContent);
|
||||
|
||||
|
||||
const result = await helpers.taskMaster(
|
||||
'parse-prd',
|
||||
[prdPath, '--research'],
|
||||
{ cwd: testDir, timeout: 90000 }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('Using Perplexity AI for research-backed task generation');
|
||||
|
||||
expect(result.stdout).toContain(
|
||||
'Using Perplexity AI for research-backed task generation'
|
||||
);
|
||||
|
||||
// Research mode should produce more detailed tasks
|
||||
const tasksPath = join(testDir, '.taskmaster/tasks/tasks.json');
|
||||
const tasks = JSON.parse(readFileSync(tasksPath, 'utf8'));
|
||||
|
||||
|
||||
// Check that tasks have detailed implementation details
|
||||
const hasDetailedTasks = tasks.master.tasks.some(t =>
|
||||
t.details && t.details.length > 200
|
||||
const hasDetailedTasks = tasks.master.tasks.some(
|
||||
(t) => t.details && t.details.length > 200
|
||||
);
|
||||
expect(hasDetailedTasks).toBe(true);
|
||||
}, 120000);
|
||||
@@ -237,22 +248,22 @@ describe('parse-prd command', () => {
|
||||
it('should parse PRD to specific tag', async () => {
|
||||
// Create a new tag
|
||||
await helpers.taskMaster('add-tag', ['feature-x'], { cwd: testDir });
|
||||
|
||||
|
||||
const prdContent = 'Build feature X components';
|
||||
const prdPath = join(testDir, 'feature-x-prd.txt');
|
||||
writeFileSync(prdPath, prdContent);
|
||||
|
||||
|
||||
const result = await helpers.taskMaster(
|
||||
'parse-prd',
|
||||
[prdPath, '--tag', 'feature-x'],
|
||||
{ cwd: testDir, timeout: 45000 }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
|
||||
const tasksPath = join(testDir, '.taskmaster/tasks/tasks.json');
|
||||
const tasks = JSON.parse(readFileSync(tasksPath, 'utf8'));
|
||||
|
||||
|
||||
expect(tasks['feature-x']).toBeDefined();
|
||||
expect(tasks['feature-x'].tasks.length).toBeGreaterThan(0);
|
||||
}, 60000);
|
||||
@@ -274,25 +285,25 @@ Build a task management system with the following features:
|
||||
- REST API backend
|
||||
- React frontend
|
||||
- PostgreSQL database`;
|
||||
|
||||
|
||||
const prdPath = join(testDir, 'markdown-prd.md');
|
||||
writeFileSync(prdPath, prdContent);
|
||||
|
||||
const result = await helpers.taskMaster(
|
||||
'parse-prd',
|
||||
[prdPath],
|
||||
{ cwd: testDir, timeout: 45000 }
|
||||
);
|
||||
|
||||
|
||||
const result = await helpers.taskMaster('parse-prd', [prdPath], {
|
||||
cwd: testDir,
|
||||
timeout: 45000
|
||||
});
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
|
||||
const tasksPath = join(testDir, '.taskmaster/tasks/tasks.json');
|
||||
const tasks = JSON.parse(readFileSync(tasksPath, 'utf8'));
|
||||
|
||||
|
||||
// Should parse technical requirements into tasks
|
||||
const hasApiTask = tasks.master.tasks.some(t =>
|
||||
t.title.toLowerCase().includes('api') ||
|
||||
t.description.toLowerCase().includes('api')
|
||||
const hasApiTask = tasks.master.tasks.some(
|
||||
(t) =>
|
||||
t.title.toLowerCase().includes('api') ||
|
||||
t.description.toLowerCase().includes('api')
|
||||
);
|
||||
expect(hasApiTask).toBe(true);
|
||||
}, 60000);
|
||||
@@ -310,26 +321,26 @@ DELETE /api/users/:id - Delete user
|
||||
\`\`\`
|
||||
|
||||
Each endpoint should have proper error handling and validation.`;
|
||||
|
||||
|
||||
const prdPath = join(testDir, 'api-prd.txt');
|
||||
writeFileSync(prdPath, prdContent);
|
||||
|
||||
const result = await helpers.taskMaster(
|
||||
'parse-prd',
|
||||
[prdPath],
|
||||
{ cwd: testDir, timeout: 45000 }
|
||||
);
|
||||
|
||||
|
||||
const result = await helpers.taskMaster('parse-prd', [prdPath], {
|
||||
cwd: testDir,
|
||||
timeout: 45000
|
||||
});
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
|
||||
const tasksPath = join(testDir, '.taskmaster/tasks/tasks.json');
|
||||
const tasks = JSON.parse(readFileSync(tasksPath, 'utf8'));
|
||||
|
||||
|
||||
// Should create tasks for API endpoints
|
||||
const hasEndpointTasks = tasks.master.tasks.some(t =>
|
||||
t.title.includes('endpoint') ||
|
||||
t.description.includes('endpoint') ||
|
||||
t.details.includes('/api/')
|
||||
const hasEndpointTasks = tasks.master.tasks.some(
|
||||
(t) =>
|
||||
t.title.includes('endpoint') ||
|
||||
t.description.includes('endpoint') ||
|
||||
t.details.includes('/api/')
|
||||
);
|
||||
expect(hasEndpointTasks).toBe(true);
|
||||
}, 60000);
|
||||
@@ -342,7 +353,7 @@ Each endpoint should have proper error handling and validation.`;
|
||||
['non-existent-file.txt'],
|
||||
{ cwd: testDir, allowFailure: true }
|
||||
);
|
||||
|
||||
|
||||
expect(result.exitCode).not.toBe(0);
|
||||
expect(result.stderr).toContain('not found');
|
||||
});
|
||||
@@ -350,23 +361,20 @@ Each endpoint should have proper error handling and validation.`;
|
||||
it('should fail with empty PRD file', async () => {
|
||||
const emptyPrdPath = join(testDir, 'empty.txt');
|
||||
writeFileSync(emptyPrdPath, '');
|
||||
|
||||
const result = await helpers.taskMaster(
|
||||
'parse-prd',
|
||||
[emptyPrdPath],
|
||||
{ cwd: testDir, allowFailure: true }
|
||||
);
|
||||
|
||||
|
||||
const result = await helpers.taskMaster('parse-prd', [emptyPrdPath], {
|
||||
cwd: testDir,
|
||||
allowFailure: true
|
||||
});
|
||||
|
||||
expect(result.exitCode).not.toBe(0);
|
||||
});
|
||||
|
||||
it('should show help when no PRD specified and no default exists', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'parse-prd',
|
||||
[],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
const result = await helpers.taskMaster('parse-prd', [], {
|
||||
cwd: testDir
|
||||
});
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('Parse PRD Help');
|
||||
expect(result.stdout).toContain('No PRD file specified');
|
||||
@@ -384,10 +392,10 @@ Each endpoint should have proper error handling and validation.`;
|
||||
largePrd += `- Requirement B for feature ${i}\n`;
|
||||
largePrd += `- Integration with feature ${i - 1}\n\n`;
|
||||
}
|
||||
|
||||
|
||||
const prdPath = join(testDir, 'large-prd.txt');
|
||||
writeFileSync(prdPath, largePrd);
|
||||
|
||||
|
||||
const startTime = Date.now();
|
||||
const result = await helpers.taskMaster(
|
||||
'parse-prd',
|
||||
@@ -395,10 +403,10 @@ Each endpoint should have proper error handling and validation.`;
|
||||
{ cwd: testDir, timeout: 120000 }
|
||||
);
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(duration).toBeLessThan(120000); // Should complete within 2 minutes
|
||||
|
||||
|
||||
const tasksPath = join(testDir, '.taskmaster/tasks/tasks.json');
|
||||
const tasks = JSON.parse(readFileSync(tasksPath, 'utf8'));
|
||||
expect(tasks.master.tasks.length).toBeGreaterThan(10);
|
||||
@@ -411,22 +419,21 @@ Build a system with:
|
||||
- UTF-8 support: ñáéíóú αβγδε 中文字符
|
||||
- Special symbols: @#$%^&*()_+{}[]|\\:;"'<>,.?/
|
||||
- Emoji support: 🚀 📊 💻 ✅`;
|
||||
|
||||
|
||||
const prdPath = join(testDir, 'special-chars-prd.txt');
|
||||
writeFileSync(prdPath, prdContent, 'utf8');
|
||||
|
||||
const result = await helpers.taskMaster(
|
||||
'parse-prd',
|
||||
[prdPath],
|
||||
{ cwd: testDir, timeout: 45000 }
|
||||
);
|
||||
|
||||
|
||||
const result = await helpers.taskMaster('parse-prd', [prdPath], {
|
||||
cwd: testDir,
|
||||
timeout: 45000
|
||||
});
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
|
||||
const tasksPath = join(testDir, '.taskmaster/tasks/tasks.json');
|
||||
const tasksContent = readFileSync(tasksPath, 'utf8');
|
||||
const tasks = JSON.parse(tasksContent);
|
||||
|
||||
|
||||
// Verify special characters are preserved
|
||||
expect(tasksContent).toContain('UTF-8');
|
||||
}, 60000);
|
||||
@@ -437,13 +444,13 @@ Build a system with:
|
||||
const prdContent = 'Build a simple blog system';
|
||||
const prdPath = join(testDir, 'blog-prd.txt');
|
||||
writeFileSync(prdPath, prdContent);
|
||||
|
||||
|
||||
// Parse PRD
|
||||
await helpers.taskMaster('parse-prd', [prdPath], { cwd: testDir });
|
||||
|
||||
|
||||
// List tasks
|
||||
const listResult = await helpers.taskMaster('list', [], { cwd: testDir });
|
||||
|
||||
|
||||
expect(listResult).toHaveExitCode(0);
|
||||
expect(listResult.stdout).toContain('ID');
|
||||
expect(listResult.stdout).toContain('Title');
|
||||
@@ -454,19 +461,18 @@ Build a system with:
|
||||
const prdContent = 'Build user authentication';
|
||||
const prdPath = join(testDir, 'auth-prd.txt');
|
||||
writeFileSync(prdPath, prdContent);
|
||||
|
||||
|
||||
// Parse PRD
|
||||
await helpers.taskMaster('parse-prd', [prdPath], { cwd: testDir });
|
||||
|
||||
|
||||
// Expand first task
|
||||
const expandResult = await helpers.taskMaster(
|
||||
'expand',
|
||||
['--id', '1'],
|
||||
{ cwd: testDir, timeout: 45000 }
|
||||
);
|
||||
|
||||
const expandResult = await helpers.taskMaster('expand', ['--id', '1'], {
|
||||
cwd: testDir,
|
||||
timeout: 45000
|
||||
});
|
||||
|
||||
expect(expandResult).toHaveExitCode(0);
|
||||
expect(expandResult.stdout).toContain('Expanded task');
|
||||
}, 90000);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
516
tests/e2e/tests/commands/remove-task.test.js
Normal file
516
tests/e2e/tests/commands/remove-task.test.js
Normal file
@@ -0,0 +1,516 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from '@jest/globals';
|
||||
import { mkdtempSync } from 'fs';
|
||||
import { tmpdir } from 'os';
|
||||
import { join } from 'path';
|
||||
import { rmSync, existsSync, readFileSync } from 'fs';
|
||||
|
||||
describe('task-master remove-task', () => {
|
||||
let testDir;
|
||||
let helpers;
|
||||
|
||||
beforeEach(() => {
|
||||
// Create test directory
|
||||
testDir = mkdtempSync(join(tmpdir(), 'tm-test-remove-'));
|
||||
process.chdir(testDir);
|
||||
|
||||
// Get helpers from global context
|
||||
helpers = global.testHelpers;
|
||||
|
||||
// Copy .env if exists
|
||||
const envPath = join(process.cwd(), '../../.env');
|
||||
if (existsSync(envPath)) {
|
||||
const envContent = readFileSync(envPath, 'utf-8');
|
||||
helpers.writeFile('.env', envContent);
|
||||
}
|
||||
|
||||
// Initialize task-master project
|
||||
const initResult = helpers.taskMaster('init', ['-y']);
|
||||
expect(initResult).toHaveExitCode(0);
|
||||
|
||||
// Ensure tasks.json exists
|
||||
const tasksPath = join(testDir, '.taskmaster/tasks/tasks.json');
|
||||
if (!helpers.fileExists(tasksPath)) {
|
||||
helpers.writeFile(tasksPath, JSON.stringify({ tasks: [] }, null, 2));
|
||||
}
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Clean up test directory
|
||||
process.chdir('..');
|
||||
rmSync(testDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
describe('Basic removal', () => {
|
||||
it('should remove a single task', () => {
|
||||
// Create task
|
||||
const addResult = helpers.taskMaster('add-task', [
|
||||
'Task to remove',
|
||||
'-m'
|
||||
]);
|
||||
expect(addResult).toHaveExitCode(0);
|
||||
const taskId = helpers.extractTaskId(addResult.stdout);
|
||||
|
||||
// Remove task
|
||||
const result = helpers.taskMaster('remove-task', [taskId, '-y']);
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('Task removed successfully');
|
||||
expect(result.stdout).toContain(taskId);
|
||||
|
||||
// Verify task is gone
|
||||
const showResult = helpers.taskMaster('show', [taskId], {
|
||||
allowFailure: true
|
||||
});
|
||||
expect(showResult.exitCode).not.toBe(0);
|
||||
});
|
||||
|
||||
it('should prompt for confirmation without -y flag', () => {
|
||||
// Create task
|
||||
const addResult = helpers.taskMaster('add-task', ['Test task', '-m']);
|
||||
const taskId = helpers.extractTaskId(addResult.stdout);
|
||||
|
||||
// Try to remove without confirmation (should fail or prompt)
|
||||
const result = helpers.taskMaster('remove-task', [taskId], {
|
||||
input: 'n\n' // Simulate saying "no" to confirmation
|
||||
});
|
||||
|
||||
// Task should still exist
|
||||
const showResult = helpers.taskMaster('show', [taskId]);
|
||||
expect(showResult).toHaveExitCode(0);
|
||||
});
|
||||
|
||||
it('should remove task with subtasks', () => {
|
||||
// Create parent task
|
||||
const parentResult = helpers.taskMaster('add-task', [
|
||||
'Parent task',
|
||||
'-m'
|
||||
]);
|
||||
const parentId = helpers.extractTaskId(parentResult.stdout);
|
||||
|
||||
// Add subtasks
|
||||
helpers.taskMaster('expand', ['-i', parentId, '-n', '3'], {
|
||||
timeout: 60000
|
||||
});
|
||||
|
||||
// Remove parent task
|
||||
const result = helpers.taskMaster('remove-task', [parentId, '-y']);
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('and 3 subtasks');
|
||||
|
||||
// Verify all are gone
|
||||
const showResult = helpers.taskMaster('show', [parentId], {
|
||||
allowFailure: true
|
||||
});
|
||||
expect(showResult.exitCode).not.toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Bulk removal', () => {
|
||||
it('should remove multiple tasks', () => {
|
||||
// Create multiple tasks
|
||||
const ids = [];
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const result = helpers.taskMaster('add-task', [`Task ${i + 1}`, '-m']);
|
||||
ids.push(helpers.extractTaskId(result.stdout));
|
||||
}
|
||||
|
||||
// Remove all
|
||||
const result = helpers.taskMaster('remove-task', [ids.join(','), '-y']);
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('3 tasks removed');
|
||||
|
||||
// Verify all are gone
|
||||
for (const id of ids) {
|
||||
const showResult = helpers.taskMaster('show', [id], {
|
||||
allowFailure: true
|
||||
});
|
||||
expect(showResult.exitCode).not.toBe(0);
|
||||
}
|
||||
});
|
||||
|
||||
it('should remove tasks by range', () => {
|
||||
// Create sequential tasks
|
||||
const ids = [];
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const result = helpers.taskMaster('add-task', [`Task ${i + 1}`, '-m']);
|
||||
ids.push(helpers.extractTaskId(result.stdout));
|
||||
}
|
||||
|
||||
// Remove middle range
|
||||
const result = helpers.taskMaster('remove-task', [
|
||||
'--from',
|
||||
ids[1],
|
||||
'--to',
|
||||
ids[3],
|
||||
'-y'
|
||||
]);
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('3 tasks removed');
|
||||
|
||||
// Verify edge tasks still exist
|
||||
const show0 = helpers.taskMaster('show', [ids[0]]);
|
||||
expect(show0).toHaveExitCode(0);
|
||||
|
||||
const show4 = helpers.taskMaster('show', [ids[4]]);
|
||||
expect(show4).toHaveExitCode(0);
|
||||
|
||||
// Verify middle tasks are gone
|
||||
for (let i = 1; i <= 3; i++) {
|
||||
const showResult = helpers.taskMaster('show', [ids[i]], {
|
||||
allowFailure: true
|
||||
});
|
||||
expect(showResult.exitCode).not.toBe(0);
|
||||
}
|
||||
});
|
||||
|
||||
it('should remove all tasks with --all flag', () => {
|
||||
// Create multiple tasks
|
||||
for (let i = 0; i < 3; i++) {
|
||||
helpers.taskMaster('add-task', [`Task ${i + 1}`, '-m']);
|
||||
}
|
||||
|
||||
// Remove all
|
||||
const result = helpers.taskMaster('remove-task', ['--all', '-y']);
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('All tasks removed');
|
||||
|
||||
// Verify empty
|
||||
const listResult = helpers.taskMaster('list');
|
||||
expect(listResult.stdout).toContain('No tasks found');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Dependency handling', () => {
|
||||
it('should warn when removing task with dependents', () => {
|
||||
// Create dependency chain
|
||||
const task1 = helpers.taskMaster('add-task', ['Base task', '-m']);
|
||||
const id1 = helpers.extractTaskId(task1.stdout);
|
||||
|
||||
const task2 = helpers.taskMaster('add-task', [
|
||||
'Dependent task',
|
||||
'-m',
|
||||
'-d',
|
||||
id1
|
||||
]);
|
||||
const id2 = helpers.extractTaskId(task2.stdout);
|
||||
|
||||
// Try to remove base task
|
||||
const result = helpers.taskMaster('remove-task', [id1, '-y']);
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('Warning');
|
||||
expect(result.stdout).toContain('dependent tasks');
|
||||
expect(result.stdout).toContain(id2);
|
||||
});
|
||||
|
||||
it('should handle cascade removal with --cascade', () => {
|
||||
// Create dependency chain
|
||||
const task1 = helpers.taskMaster('add-task', ['Base task', '-m']);
|
||||
const id1 = helpers.extractTaskId(task1.stdout);
|
||||
|
||||
const task2 = helpers.taskMaster('add-task', [
|
||||
'Dependent 1',
|
||||
'-m',
|
||||
'-d',
|
||||
id1
|
||||
]);
|
||||
const id2 = helpers.extractTaskId(task2.stdout);
|
||||
|
||||
const task3 = helpers.taskMaster('add-task', [
|
||||
'Dependent 2',
|
||||
'-m',
|
||||
'-d',
|
||||
id2
|
||||
]);
|
||||
const id3 = helpers.extractTaskId(task3.stdout);
|
||||
|
||||
// Remove with cascade
|
||||
const result = helpers.taskMaster('remove-task', [
|
||||
id1,
|
||||
'--cascade',
|
||||
'-y'
|
||||
]);
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('3 tasks removed');
|
||||
expect(result.stdout).toContain('cascade');
|
||||
|
||||
// Verify all are gone
|
||||
for (const id of [id1, id2, id3]) {
|
||||
const showResult = helpers.taskMaster('show', [id], {
|
||||
allowFailure: true
|
||||
});
|
||||
expect(showResult.exitCode).not.toBe(0);
|
||||
}
|
||||
});
|
||||
|
||||
it('should update dependencies when removing task', () => {
|
||||
// Create chain: task1 -> task2 -> task3
|
||||
const task1 = helpers.taskMaster('add-task', ['Task 1', '-m']);
|
||||
const id1 = helpers.extractTaskId(task1.stdout);
|
||||
|
||||
const task2 = helpers.taskMaster('add-task', ['Task 2', '-m', '-d', id1]);
|
||||
const id2 = helpers.extractTaskId(task2.stdout);
|
||||
|
||||
const task3 = helpers.taskMaster('add-task', ['Task 3', '-m', '-d', id2]);
|
||||
const id3 = helpers.extractTaskId(task3.stdout);
|
||||
|
||||
// Remove middle task
|
||||
const result = helpers.taskMaster('remove-task', [id2, '-y']);
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
// Task 3 should now depend directly on task 1
|
||||
const showResult = helpers.taskMaster('show', [id3]);
|
||||
expect(showResult).toHaveExitCode(0);
|
||||
expect(showResult.stdout).toContain('Dependencies:');
|
||||
expect(showResult.stdout).toContain(id1);
|
||||
expect(showResult.stdout).not.toContain(id2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Status filtering', () => {
|
||||
it('should remove only completed tasks', () => {
|
||||
// Create tasks with different statuses
|
||||
const pending = helpers.taskMaster('add-task', ['Pending task', '-m']);
|
||||
const pendingId = helpers.extractTaskId(pending.stdout);
|
||||
|
||||
const done1 = helpers.taskMaster('add-task', ['Done task 1', '-m']);
|
||||
const doneId1 = helpers.extractTaskId(done1.stdout);
|
||||
helpers.taskMaster('set-status', [doneId1, 'done']);
|
||||
|
||||
const done2 = helpers.taskMaster('add-task', ['Done task 2', '-m']);
|
||||
const doneId2 = helpers.extractTaskId(done2.stdout);
|
||||
helpers.taskMaster('set-status', [doneId2, 'done']);
|
||||
|
||||
// Remove only done tasks
|
||||
const result = helpers.taskMaster('remove-task', [
|
||||
'--status',
|
||||
'done',
|
||||
'-y'
|
||||
]);
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('2 tasks removed');
|
||||
|
||||
// Verify pending task still exists
|
||||
const showResult = helpers.taskMaster('show', [pendingId]);
|
||||
expect(showResult).toHaveExitCode(0);
|
||||
|
||||
// Verify done tasks are gone
|
||||
for (const id of [doneId1, doneId2]) {
|
||||
const show = helpers.taskMaster('show', [id], {
|
||||
allowFailure: true
|
||||
});
|
||||
expect(show.exitCode).not.toBe(0);
|
||||
}
|
||||
});
|
||||
|
||||
it('should remove cancelled and deferred tasks', () => {
|
||||
// Create tasks
|
||||
const cancelled = helpers.taskMaster('add-task', ['Cancelled', '-m']);
|
||||
const cancelledId = helpers.extractTaskId(cancelled.stdout);
|
||||
helpers.taskMaster('set-status', [cancelledId, 'cancelled']);
|
||||
|
||||
const deferred = helpers.taskMaster('add-task', ['Deferred', '-m']);
|
||||
const deferredId = helpers.extractTaskId(deferred.stdout);
|
||||
helpers.taskMaster('set-status', [deferredId, 'deferred']);
|
||||
|
||||
const active = helpers.taskMaster('add-task', ['Active', '-m']);
|
||||
const activeId = helpers.extractTaskId(active.stdout);
|
||||
|
||||
// Remove cancelled and deferred
|
||||
const result = helpers.taskMaster('remove-task', [
|
||||
'--status',
|
||||
'cancelled,deferred',
|
||||
'-y'
|
||||
]);
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('2 tasks removed');
|
||||
|
||||
// Verify active task remains
|
||||
const showResult = helpers.taskMaster('show', [activeId]);
|
||||
expect(showResult).toHaveExitCode(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Tag context', () => {
|
||||
it('should remove tasks from specific tag', () => {
|
||||
// Create tag
|
||||
helpers.taskMaster('add-tag', ['feature']);
|
||||
|
||||
// Add tasks to different tags
|
||||
const master = helpers.taskMaster('add-task', ['Master task', '-m']);
|
||||
const masterId = helpers.extractTaskId(master.stdout);
|
||||
|
||||
helpers.taskMaster('use-tag', ['feature']);
|
||||
const feature = helpers.taskMaster('add-task', ['Feature task', '-m']);
|
||||
const featureId = helpers.extractTaskId(feature.stdout);
|
||||
|
||||
// Remove from feature tag
|
||||
const result = helpers.taskMaster('remove-task', [
|
||||
featureId,
|
||||
'--tag',
|
||||
'feature',
|
||||
'-y'
|
||||
]);
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
// Verify master task still exists
|
||||
helpers.taskMaster('use-tag', ['master']);
|
||||
const showResult = helpers.taskMaster('show', [masterId]);
|
||||
expect(showResult).toHaveExitCode(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Undo functionality', () => {
|
||||
it('should create backup before removal', () => {
|
||||
// Create task
|
||||
const task = helpers.taskMaster('add-task', ['Task to backup', '-m']);
|
||||
const taskId = helpers.extractTaskId(task.stdout);
|
||||
|
||||
// Remove task
|
||||
const result = helpers.taskMaster('remove-task', [taskId, '-y']);
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('Backup created');
|
||||
|
||||
// Check for backup file
|
||||
const backupDir = join(testDir, '.taskmaster/backups');
|
||||
expect(existsSync(backupDir)).toBe(true);
|
||||
});
|
||||
|
||||
it('should show undo instructions', () => {
|
||||
// Create and remove task
|
||||
const task = helpers.taskMaster('add-task', ['Test task', '-m']);
|
||||
const taskId = helpers.extractTaskId(task.stdout);
|
||||
|
||||
const result = helpers.taskMaster('remove-task', [taskId, '-y']);
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('To undo this operation');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Subtask removal', () => {
|
||||
it('should remove individual subtask', () => {
|
||||
// Create parent with subtasks
|
||||
const parent = helpers.taskMaster('add-task', ['Parent', '-m']);
|
||||
const parentId = helpers.extractTaskId(parent.stdout);
|
||||
|
||||
helpers.taskMaster('expand', ['-i', parentId, '-n', '3'], {
|
||||
timeout: 60000
|
||||
});
|
||||
|
||||
// Remove middle subtask
|
||||
const subtaskId = `${parentId}.2`;
|
||||
const result = helpers.taskMaster('remove-task', [subtaskId, '-y']);
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('Subtask removed');
|
||||
|
||||
// Verify parent still has 2 subtasks
|
||||
const showResult = helpers.taskMaster('show', [parentId]);
|
||||
expect(showResult).toHaveExitCode(0);
|
||||
expect(showResult.stdout).toContain('Subtasks (2)');
|
||||
});
|
||||
|
||||
it('should renumber remaining subtasks', () => {
|
||||
// Create parent with subtasks
|
||||
const parent = helpers.taskMaster('add-task', ['Parent', '-m']);
|
||||
const parentId = helpers.extractTaskId(parent.stdout);
|
||||
|
||||
helpers.taskMaster('expand', ['-i', parentId, '-n', '3'], {
|
||||
timeout: 60000
|
||||
});
|
||||
|
||||
// Remove first subtask
|
||||
const result = helpers.taskMaster('remove-task', [`${parentId}.1`, '-y']);
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
// Check remaining subtasks are renumbered
|
||||
const showResult = helpers.taskMaster('show', [parentId]);
|
||||
expect(showResult).toHaveExitCode(0);
|
||||
expect(showResult.stdout).toContain(`${parentId}.1`);
|
||||
expect(showResult.stdout).toContain(`${parentId}.2`);
|
||||
expect(showResult.stdout).not.toContain(`${parentId}.3`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error handling', () => {
|
||||
it('should handle non-existent task ID', () => {
|
||||
const result = helpers.taskMaster('remove-task', ['999', '-y'], {
|
||||
allowFailure: true
|
||||
});
|
||||
expect(result.exitCode).not.toBe(0);
|
||||
expect(result.stderr).toMatch(/Task.*not found/i);
|
||||
});
|
||||
|
||||
it('should handle invalid task ID format', () => {
|
||||
const result = helpers.taskMaster('remove-task', ['invalid-id', '-y'], {
|
||||
allowFailure: true
|
||||
});
|
||||
expect(result.exitCode).not.toBe(0);
|
||||
expect(result.stderr).toContain('Invalid task ID');
|
||||
});
|
||||
|
||||
it('should prevent removing all tasks without confirmation', () => {
|
||||
// Create tasks
|
||||
helpers.taskMaster('add-task', ['Task 1', '-m']);
|
||||
helpers.taskMaster('add-task', ['Task 2', '-m']);
|
||||
|
||||
// Try to remove all without -y
|
||||
const result = helpers.taskMaster('remove-task', ['--all'], {
|
||||
input: 'n\n'
|
||||
});
|
||||
|
||||
// Tasks should still exist
|
||||
const listResult = helpers.taskMaster('list');
|
||||
expect(listResult.stdout).not.toContain('No tasks found');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Performance', () => {
|
||||
it('should handle bulk removal efficiently', () => {
|
||||
// Create many tasks
|
||||
const ids = [];
|
||||
for (let i = 0; i < 50; i++) {
|
||||
const result = helpers.taskMaster('add-task', [`Task ${i + 1}`, '-m']);
|
||||
ids.push(helpers.extractTaskId(result.stdout));
|
||||
}
|
||||
|
||||
// Remove all at once
|
||||
const startTime = Date.now();
|
||||
const result = helpers.taskMaster('remove-task', ['--all', '-y']);
|
||||
const endTime = Date.now();
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('50 tasks removed');
|
||||
expect(endTime - startTime).toBeLessThan(5000);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Output options', () => {
|
||||
it('should support quiet mode', () => {
|
||||
const task = helpers.taskMaster('add-task', ['Test task', '-m']);
|
||||
const taskId = helpers.extractTaskId(task.stdout);
|
||||
|
||||
const result = helpers.taskMaster('remove-task', [taskId, '-y', '-q']);
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout.split('\n').length).toBeLessThan(3);
|
||||
});
|
||||
|
||||
it('should support JSON output', () => {
|
||||
// Create tasks
|
||||
const task1 = helpers.taskMaster('add-task', ['Task 1', '-m']);
|
||||
const id1 = helpers.extractTaskId(task1.stdout);
|
||||
|
||||
const task2 = helpers.taskMaster('add-task', ['Task 2', '-m']);
|
||||
const id2 = helpers.extractTaskId(task2.stdout);
|
||||
|
||||
const result = helpers.taskMaster('remove-task', [
|
||||
`${id1},${id2}`,
|
||||
'-y',
|
||||
'--json'
|
||||
]);
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
const json = JSON.parse(result.stdout);
|
||||
expect(json.removed).toBe(2);
|
||||
expect(json.tasks).toHaveLength(2);
|
||||
expect(json.backup).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -37,13 +37,13 @@ export default async function testResearchSave(logger, helpers, context) {
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Command failed: ${result.stderr}`);
|
||||
}
|
||||
|
||||
|
||||
// Verify file was created
|
||||
const outputPath = `${testDir}/oauth-guide.md`;
|
||||
if (!helpers.fileExists(outputPath)) {
|
||||
throw new Error('Research output file was not created');
|
||||
}
|
||||
|
||||
|
||||
// Check file content
|
||||
const content = helpers.readFile(outputPath);
|
||||
if (!content.includes('OAuth') || !content.includes('Node.js')) {
|
||||
@@ -60,22 +60,28 @@ export default async function testResearchSave(logger, helpers, context) {
|
||||
{ cwd: testDir }
|
||||
);
|
||||
const taskId = helpers.extractTaskId(taskResult.stdout);
|
||||
|
||||
|
||||
const result = await helpers.taskMaster(
|
||||
'research-save',
|
||||
['--task', taskId, 'JWT vs OAuth comparison for REST APIs', '--output', 'auth-research.md'],
|
||||
[
|
||||
'--task',
|
||||
taskId,
|
||||
'JWT vs OAuth comparison for REST APIs',
|
||||
'--output',
|
||||
'auth-research.md'
|
||||
],
|
||||
{ cwd: testDir, timeout: 120000 }
|
||||
);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Command failed: ${result.stderr}`);
|
||||
}
|
||||
|
||||
|
||||
// Check saved content includes task context
|
||||
const content = helpers.readFile(`${testDir}/auth-research.md`);
|
||||
if (!content.includes('JWT') || !content.includes('OAuth')) {
|
||||
throw new Error('Research does not cover requested topics');
|
||||
}
|
||||
|
||||
|
||||
// Should reference the task
|
||||
if (!content.includes(taskId) && !content.includes('Task #')) {
|
||||
throw new Error('Saved research does not reference the task context');
|
||||
@@ -86,25 +92,30 @@ export default async function testResearchSave(logger, helpers, context) {
|
||||
await runTest('Save to knowledge base', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'research-save',
|
||||
['Database indexing strategies', '--knowledge-base', '--category', 'database'],
|
||||
[
|
||||
'Database indexing strategies',
|
||||
'--knowledge-base',
|
||||
'--category',
|
||||
'database'
|
||||
],
|
||||
{ cwd: testDir, timeout: 120000 }
|
||||
);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Command failed: ${result.stderr}`);
|
||||
}
|
||||
|
||||
|
||||
// Check knowledge base directory
|
||||
const kbPath = `${testDir}/.taskmaster/knowledge-base/database`;
|
||||
if (!helpers.fileExists(kbPath)) {
|
||||
throw new Error('Knowledge base category directory not created');
|
||||
}
|
||||
|
||||
|
||||
// Should create a file with timestamp or ID
|
||||
const files = helpers.listFiles(kbPath);
|
||||
if (files.length === 0) {
|
||||
throw new Error('No files created in knowledge base');
|
||||
}
|
||||
|
||||
|
||||
// Verify content
|
||||
const savedFile = files[0];
|
||||
const content = helpers.readFile(`${kbPath}/${savedFile}`);
|
||||
@@ -117,13 +128,19 @@ export default async function testResearchSave(logger, helpers, context) {
|
||||
await runTest('Save with custom format', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'research-save',
|
||||
['React performance optimization', '--output', 'react-perf.json', '--format', 'json'],
|
||||
[
|
||||
'React performance optimization',
|
||||
'--output',
|
||||
'react-perf.json',
|
||||
'--format',
|
||||
'json'
|
||||
],
|
||||
{ cwd: testDir, timeout: 120000 }
|
||||
);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Command failed: ${result.stderr}`);
|
||||
}
|
||||
|
||||
|
||||
// Verify JSON format
|
||||
const content = helpers.readFile(`${testDir}/react-perf.json`);
|
||||
let parsed;
|
||||
@@ -132,14 +149,16 @@ export default async function testResearchSave(logger, helpers, context) {
|
||||
} catch (e) {
|
||||
throw new Error('Output is not valid JSON');
|
||||
}
|
||||
|
||||
|
||||
// Check JSON structure
|
||||
if (!parsed.topic || !parsed.content || !parsed.timestamp) {
|
||||
throw new Error('JSON output missing expected fields');
|
||||
}
|
||||
|
||||
if (!parsed.content.toLowerCase().includes('react') ||
|
||||
!parsed.content.toLowerCase().includes('performance')) {
|
||||
|
||||
if (
|
||||
!parsed.content.toLowerCase().includes('react') ||
|
||||
!parsed.content.toLowerCase().includes('performance')
|
||||
) {
|
||||
throw new Error('JSON content not relevant to query');
|
||||
}
|
||||
});
|
||||
@@ -150,26 +169,33 @@ export default async function testResearchSave(logger, helpers, context) {
|
||||
'research-save',
|
||||
[
|
||||
'Microservices communication patterns',
|
||||
'--output', 'microservices.md',
|
||||
'--metadata', 'author=TaskMaster',
|
||||
'--metadata', 'tags=architecture,microservices',
|
||||
'--metadata', 'version=1.0'
|
||||
'--output',
|
||||
'microservices.md',
|
||||
'--metadata',
|
||||
'author=TaskMaster',
|
||||
'--metadata',
|
||||
'tags=architecture,microservices',
|
||||
'--metadata',
|
||||
'version=1.0'
|
||||
],
|
||||
{ cwd: testDir, timeout: 120000 }
|
||||
);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Command failed: ${result.stderr}`);
|
||||
}
|
||||
|
||||
|
||||
// Check file content for metadata
|
||||
const content = helpers.readFile(`${testDir}/microservices.md`);
|
||||
|
||||
|
||||
// Should include metadata in frontmatter or header
|
||||
if (!content.includes('author') && !content.includes('Author')) {
|
||||
throw new Error('Metadata not included in saved file');
|
||||
}
|
||||
|
||||
if (!content.includes('microservice') || !content.includes('communication')) {
|
||||
|
||||
if (
|
||||
!content.includes('microservice') ||
|
||||
!content.includes('communication')
|
||||
) {
|
||||
throw new Error('Research content not relevant');
|
||||
}
|
||||
});
|
||||
@@ -177,18 +203,24 @@ export default async function testResearchSave(logger, helpers, context) {
|
||||
// Test 6: Append to existing file
|
||||
await runTest('Append to existing research file', async () => {
|
||||
// Create initial file
|
||||
const initialContent = '# API Research\n\n## Previous Research\n\nInitial content here.\n\n';
|
||||
const initialContent =
|
||||
'# API Research\n\n## Previous Research\n\nInitial content here.\n\n';
|
||||
helpers.writeFile(`${testDir}/api-research.md`, initialContent);
|
||||
|
||||
|
||||
const result = await helpers.taskMaster(
|
||||
'research-save',
|
||||
['GraphQL schema design best practices', '--output', 'api-research.md', '--append'],
|
||||
[
|
||||
'GraphQL schema design best practices',
|
||||
'--output',
|
||||
'api-research.md',
|
||||
'--append'
|
||||
],
|
||||
{ cwd: testDir, timeout: 120000 }
|
||||
);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Command failed: ${result.stderr}`);
|
||||
}
|
||||
|
||||
|
||||
// Check file was appended
|
||||
const content = helpers.readFile(`${testDir}/api-research.md`);
|
||||
if (!content.includes('Previous Research')) {
|
||||
@@ -203,24 +235,30 @@ export default async function testResearchSave(logger, helpers, context) {
|
||||
await runTest('Save with source references', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'research-save',
|
||||
['TypeScript decorators guide', '--output', 'decorators.md', '--include-references'],
|
||||
[
|
||||
'TypeScript decorators guide',
|
||||
'--output',
|
||||
'decorators.md',
|
||||
'--include-references'
|
||||
],
|
||||
{ cwd: testDir, timeout: 120000 }
|
||||
);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Command failed: ${result.stderr}`);
|
||||
}
|
||||
|
||||
|
||||
// Check for references section
|
||||
const content = helpers.readFile(`${testDir}/decorators.md`);
|
||||
if (!content.includes('TypeScript') || !content.includes('decorator')) {
|
||||
throw new Error('Research content not relevant');
|
||||
}
|
||||
|
||||
|
||||
// Should include references or sources
|
||||
const hasReferences = content.includes('Reference') ||
|
||||
content.includes('Source') ||
|
||||
content.includes('Further reading') ||
|
||||
content.includes('Links');
|
||||
const hasReferences =
|
||||
content.includes('Reference') ||
|
||||
content.includes('Source') ||
|
||||
content.includes('Further reading') ||
|
||||
content.includes('Links');
|
||||
if (!hasReferences) {
|
||||
throw new Error('No references section included');
|
||||
}
|
||||
@@ -233,7 +271,7 @@ export default async function testResearchSave(logger, helpers, context) {
|
||||
'Kubernetes deployment strategies',
|
||||
'CI/CD pipeline setup'
|
||||
];
|
||||
|
||||
|
||||
const result = await helpers.taskMaster(
|
||||
'research-save',
|
||||
['--batch', '--output-dir', 'devops-research', ...topics],
|
||||
@@ -242,28 +280,37 @@ export default async function testResearchSave(logger, helpers, context) {
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Command failed: ${result.stderr}`);
|
||||
}
|
||||
|
||||
|
||||
// Check directory was created
|
||||
const outputDir = `${testDir}/devops-research`;
|
||||
if (!helpers.fileExists(outputDir)) {
|
||||
throw new Error('Output directory not created');
|
||||
}
|
||||
|
||||
|
||||
// Should have files for each topic
|
||||
const files = helpers.listFiles(outputDir);
|
||||
if (files.length < topics.length) {
|
||||
throw new Error(`Expected ${topics.length} files, found ${files.length}`);
|
||||
throw new Error(
|
||||
`Expected ${topics.length} files, found ${files.length}`
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// Verify each file has relevant content
|
||||
let foundDocker = false, foundK8s = false, foundCICD = false;
|
||||
files.forEach(file => {
|
||||
let foundDocker = false,
|
||||
foundK8s = false,
|
||||
foundCICD = false;
|
||||
files.forEach((file) => {
|
||||
const content = helpers.readFile(`${outputDir}/${file}`).toLowerCase();
|
||||
if (content.includes('docker')) foundDocker = true;
|
||||
if (content.includes('kubernetes')) foundK8s = true;
|
||||
if (content.includes('ci') || content.includes('cd') || content.includes('pipeline')) foundCICD = true;
|
||||
if (
|
||||
content.includes('ci') ||
|
||||
content.includes('cd') ||
|
||||
content.includes('pipeline')
|
||||
)
|
||||
foundCICD = true;
|
||||
});
|
||||
|
||||
|
||||
if (!foundDocker || !foundK8s || !foundCICD) {
|
||||
throw new Error('Not all topics were researched and saved');
|
||||
}
|
||||
@@ -290,21 +337,24 @@ Category: {{CATEGORY}}
|
||||
{{NOTES}}
|
||||
`;
|
||||
helpers.writeFile(`${testDir}/research-template.md`, template);
|
||||
|
||||
|
||||
const result = await helpers.taskMaster(
|
||||
'research-save',
|
||||
[
|
||||
'Redis caching strategies',
|
||||
'--output', 'redis-research.md',
|
||||
'--template', 'research-template.md',
|
||||
'--category', 'performance'
|
||||
'--output',
|
||||
'redis-research.md',
|
||||
'--template',
|
||||
'research-template.md',
|
||||
'--category',
|
||||
'performance'
|
||||
],
|
||||
{ cwd: testDir, timeout: 120000 }
|
||||
);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Command failed: ${result.stderr}`);
|
||||
}
|
||||
|
||||
|
||||
// Check template was used
|
||||
const content = helpers.readFile(`${testDir}/redis-research.md`);
|
||||
if (!content.includes('Redis caching strategies')) {
|
||||
@@ -313,7 +363,10 @@ Category: {{CATEGORY}}
|
||||
if (!content.includes('Category: performance')) {
|
||||
throw new Error('Template category not filled');
|
||||
}
|
||||
if (!content.includes('Key Takeaways') || !content.includes('Implementation Notes')) {
|
||||
if (
|
||||
!content.includes('Key Takeaways') ||
|
||||
!content.includes('Implementation Notes')
|
||||
) {
|
||||
throw new Error('Template structure not preserved');
|
||||
}
|
||||
});
|
||||
@@ -339,13 +392,15 @@ Category: {{CATEGORY}}
|
||||
{ cwd: testDir }
|
||||
);
|
||||
const taskId = helpers.extractTaskId(taskResult.stdout);
|
||||
|
||||
|
||||
const result = await helpers.taskMaster(
|
||||
'research-save',
|
||||
[
|
||||
'--task', taskId,
|
||||
'--task',
|
||||
taskId,
|
||||
'Caching strategies comparison',
|
||||
'--output', 'caching-research.md',
|
||||
'--output',
|
||||
'caching-research.md',
|
||||
'--link-to-task'
|
||||
],
|
||||
{ cwd: testDir, timeout: 120000 }
|
||||
@@ -353,11 +408,15 @@ Category: {{CATEGORY}}
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Command failed: ${result.stderr}`);
|
||||
}
|
||||
|
||||
|
||||
// Check task was updated with research link
|
||||
const showResult = await helpers.taskMaster('show', [taskId], { cwd: testDir });
|
||||
if (!showResult.stdout.includes('caching-research.md') &&
|
||||
!showResult.stdout.includes('Research')) {
|
||||
const showResult = await helpers.taskMaster('show', [taskId], {
|
||||
cwd: testDir
|
||||
});
|
||||
if (
|
||||
!showResult.stdout.includes('caching-research.md') &&
|
||||
!showResult.stdout.includes('Research')
|
||||
) {
|
||||
throw new Error('Task not updated with research link');
|
||||
}
|
||||
});
|
||||
@@ -368,7 +427,8 @@ Category: {{CATEGORY}}
|
||||
'research-save',
|
||||
[
|
||||
'Comprehensive guide to distributed systems',
|
||||
'--output', 'dist-systems.md.gz',
|
||||
'--output',
|
||||
'dist-systems.md.gz',
|
||||
'--compress'
|
||||
],
|
||||
{ cwd: testDir, timeout: 120000 }
|
||||
@@ -376,7 +436,7 @@ Category: {{CATEGORY}}
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Command failed: ${result.stderr}`);
|
||||
}
|
||||
|
||||
|
||||
// Check compressed file exists
|
||||
const compressedPath = `${testDir}/dist-systems.md.gz`;
|
||||
if (!helpers.fileExists(compressedPath)) {
|
||||
@@ -392,21 +452,28 @@ Category: {{CATEGORY}}
|
||||
['API design patterns', '--output', 'api-patterns.md', '--version'],
|
||||
{ cwd: testDir, timeout: 120000 }
|
||||
);
|
||||
|
||||
|
||||
// Save updated version
|
||||
const result = await helpers.taskMaster(
|
||||
'research-save',
|
||||
['API design patterns - updated', '--output', 'api-patterns.md', '--version'],
|
||||
[
|
||||
'API design patterns - updated',
|
||||
'--output',
|
||||
'api-patterns.md',
|
||||
'--version'
|
||||
],
|
||||
{ cwd: testDir, timeout: 120000 }
|
||||
);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Command failed: ${result.stderr}`);
|
||||
}
|
||||
|
||||
|
||||
// Check for version files
|
||||
const files = helpers.listFiles(testDir);
|
||||
const versionFiles = files.filter(f => f.includes('api-patterns') && f.includes('.v'));
|
||||
|
||||
const versionFiles = files.filter(
|
||||
(f) => f.includes('api-patterns') && f.includes('.v')
|
||||
);
|
||||
|
||||
if (versionFiles.length === 0) {
|
||||
throw new Error('No version files created');
|
||||
}
|
||||
@@ -418,18 +485,20 @@ Category: {{CATEGORY}}
|
||||
'research-save',
|
||||
[
|
||||
'Testing strategies overview',
|
||||
'--output', 'testing',
|
||||
'--formats', 'md,json,txt'
|
||||
'--output',
|
||||
'testing',
|
||||
'--formats',
|
||||
'md,json,txt'
|
||||
],
|
||||
{ cwd: testDir, timeout: 120000 }
|
||||
);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Command failed: ${result.stderr}`);
|
||||
}
|
||||
|
||||
|
||||
// Check all format files exist
|
||||
const formats = ['md', 'json', 'txt'];
|
||||
formats.forEach(format => {
|
||||
formats.forEach((format) => {
|
||||
const filePath = `${testDir}/testing.${format}`;
|
||||
if (!helpers.fileExists(filePath)) {
|
||||
throw new Error(`${format} format file not created`);
|
||||
@@ -443,32 +512,46 @@ Category: {{CATEGORY}}
|
||||
'research-save',
|
||||
[
|
||||
'Machine learning deployment strategies',
|
||||
'--output', 'ml-deployment.md',
|
||||
'--output',
|
||||
'ml-deployment.md',
|
||||
'--include-summary',
|
||||
'--summary-length', '200'
|
||||
'--summary-length',
|
||||
'200'
|
||||
],
|
||||
{ cwd: testDir, timeout: 120000 }
|
||||
);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Command failed: ${result.stderr}`);
|
||||
}
|
||||
|
||||
|
||||
// Check for summary section
|
||||
const content = helpers.readFile(`${testDir}/ml-deployment.md`);
|
||||
if (!content.includes('Summary') && !content.includes('TL;DR') && !content.includes('Overview')) {
|
||||
if (
|
||||
!content.includes('Summary') &&
|
||||
!content.includes('TL;DR') &&
|
||||
!content.includes('Overview')
|
||||
) {
|
||||
throw new Error('No summary section found');
|
||||
}
|
||||
|
||||
|
||||
// Content should be about ML deployment
|
||||
if (!content.includes('machine learning') && !content.includes('ML') && !content.includes('deployment')) {
|
||||
if (
|
||||
!content.includes('machine learning') &&
|
||||
!content.includes('ML') &&
|
||||
!content.includes('deployment')
|
||||
) {
|
||||
throw new Error('Research content not relevant to query');
|
||||
}
|
||||
});
|
||||
|
||||
// Calculate summary
|
||||
const totalTests = results.tests.length;
|
||||
const passedTests = results.tests.filter(t => t.status === 'passed').length;
|
||||
const failedTests = results.tests.filter(t => t.status === 'failed').length;
|
||||
const passedTests = results.tests.filter(
|
||||
(t) => t.status === 'passed'
|
||||
).length;
|
||||
const failedTests = results.tests.filter(
|
||||
(t) => t.status === 'failed'
|
||||
).length;
|
||||
|
||||
logger.info('\n=== Research-Save Test Summary ===');
|
||||
logger.info(`Total tests: ${totalTests}`);
|
||||
@@ -481,7 +564,6 @@ Category: {{CATEGORY}}
|
||||
} else {
|
||||
logger.success('\n✅ All research-save tests passed!');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
results.status = 'failed';
|
||||
results.errors.push({
|
||||
@@ -493,4 +575,4 @@ Category: {{CATEGORY}}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,14 @@
|
||||
* Tests all aspects of AI-powered research functionality
|
||||
*/
|
||||
|
||||
const { mkdtempSync, existsSync, readFileSync, rmSync, writeFileSync, mkdirSync } = require('fs');
|
||||
const {
|
||||
mkdtempSync,
|
||||
existsSync,
|
||||
readFileSync,
|
||||
rmSync,
|
||||
writeFileSync,
|
||||
mkdirSync
|
||||
} = require('fs');
|
||||
const { join } = require('path');
|
||||
const { tmpdir } = require('os');
|
||||
|
||||
@@ -14,11 +21,11 @@ describe('research command', () => {
|
||||
beforeEach(async () => {
|
||||
// Create test directory
|
||||
testDir = mkdtempSync(join(tmpdir(), 'task-master-research-'));
|
||||
|
||||
|
||||
// Initialize test helpers
|
||||
const context = global.createTestContext('research');
|
||||
helpers = context.helpers;
|
||||
|
||||
|
||||
// Copy .env file if it exists
|
||||
const mainEnvPath = join(__dirname, '../../../../.env');
|
||||
const testEnvPath = join(testDir, '.env');
|
||||
@@ -26,9 +33,11 @@ describe('research command', () => {
|
||||
const envContent = readFileSync(mainEnvPath, 'utf8');
|
||||
writeFileSync(testEnvPath, envContent);
|
||||
}
|
||||
|
||||
|
||||
// Initialize task-master project
|
||||
const initResult = await helpers.taskMaster('init', ['-y'], { cwd: testDir });
|
||||
const initResult = await helpers.taskMaster('init', ['-y'], {
|
||||
cwd: testDir
|
||||
});
|
||||
expect(initResult).toHaveExitCode(0);
|
||||
});
|
||||
|
||||
@@ -43,15 +52,18 @@ describe('research command', () => {
|
||||
it('should perform research on a topic', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'research',
|
||||
['What are the best practices for implementing OAuth 2.0 authentication?'],
|
||||
[
|
||||
'What are the best practices for implementing OAuth 2.0 authentication?'
|
||||
],
|
||||
{ cwd: testDir, timeout: 90000 }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('Research Results');
|
||||
|
||||
|
||||
// Should contain relevant OAuth information
|
||||
const hasOAuthInfo = result.stdout.toLowerCase().includes('oauth') ||
|
||||
const hasOAuthInfo =
|
||||
result.stdout.toLowerCase().includes('oauth') ||
|
||||
result.stdout.toLowerCase().includes('authentication');
|
||||
expect(hasOAuthInfo).toBe(true);
|
||||
}, 120000);
|
||||
@@ -62,12 +74,13 @@ describe('research command', () => {
|
||||
['--topic', 'React performance optimization techniques'],
|
||||
{ cwd: testDir, timeout: 90000 }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('Research Results');
|
||||
|
||||
|
||||
// Should contain React-related information
|
||||
const hasReactInfo = result.stdout.toLowerCase().includes('react') ||
|
||||
const hasReactInfo =
|
||||
result.stdout.toLowerCase().includes('react') ||
|
||||
result.stdout.toLowerCase().includes('performance');
|
||||
expect(hasReactInfo).toBe(true);
|
||||
}, 120000);
|
||||
@@ -78,11 +91,12 @@ describe('research command', () => {
|
||||
['Compare PostgreSQL vs MongoDB for a real-time analytics application'],
|
||||
{ cwd: testDir, timeout: 90000 }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
|
||||
// Should contain database comparison
|
||||
const hasDatabaseInfo = result.stdout.toLowerCase().includes('postgresql') ||
|
||||
const hasDatabaseInfo =
|
||||
result.stdout.toLowerCase().includes('postgresql') ||
|
||||
result.stdout.toLowerCase().includes('mongodb');
|
||||
expect(hasDatabaseInfo).toBe(true);
|
||||
}, 120000);
|
||||
@@ -97,10 +111,10 @@ describe('research command', () => {
|
||||
{ cwd: testDir, timeout: 60000 }
|
||||
);
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('Research Results');
|
||||
|
||||
|
||||
// Quick research should be faster
|
||||
expect(duration).toBeLessThan(60000);
|
||||
}, 90000);
|
||||
@@ -111,15 +125,16 @@ describe('research command', () => {
|
||||
['--topic', 'Microservices architecture patterns', '--detailed'],
|
||||
{ cwd: testDir, timeout: 120000 }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('Research Results');
|
||||
|
||||
|
||||
// Detailed research should have more content
|
||||
expect(result.stdout.length).toBeGreaterThan(500);
|
||||
|
||||
|
||||
// Should contain comprehensive information
|
||||
const hasPatterns = result.stdout.toLowerCase().includes('pattern') ||
|
||||
const hasPatterns =
|
||||
result.stdout.toLowerCase().includes('pattern') ||
|
||||
result.stdout.toLowerCase().includes('architecture');
|
||||
expect(hasPatterns).toBe(true);
|
||||
}, 150000);
|
||||
@@ -132,12 +147,13 @@ describe('research command', () => {
|
||||
['--topic', 'GraphQL best practices', '--sources'],
|
||||
{ cwd: testDir, timeout: 90000 }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('Research Results');
|
||||
|
||||
|
||||
// Should include source references
|
||||
const hasSources = result.stdout.includes('Source:') ||
|
||||
const hasSources =
|
||||
result.stdout.includes('Source:') ||
|
||||
result.stdout.includes('Reference:') ||
|
||||
result.stdout.includes('http');
|
||||
expect(hasSources).toBe(true);
|
||||
@@ -147,19 +163,19 @@ describe('research command', () => {
|
||||
describe('Research output options', () => {
|
||||
it('should save research to file with --save flag', async () => {
|
||||
const outputPath = join(testDir, 'research-output.md');
|
||||
|
||||
|
||||
const result = await helpers.taskMaster(
|
||||
'research',
|
||||
['--topic', 'Docker container security', '--save', outputPath],
|
||||
{ cwd: testDir, timeout: 90000 }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('Research saved to');
|
||||
|
||||
|
||||
// Verify file was created
|
||||
expect(existsSync(outputPath)).toBe(true);
|
||||
|
||||
|
||||
// Verify file contains research content
|
||||
const content = readFileSync(outputPath, 'utf8');
|
||||
expect(content).toContain('Docker');
|
||||
@@ -172,9 +188,9 @@ describe('research command', () => {
|
||||
['--topic', 'WebSocket implementation', '--output', 'json'],
|
||||
{ cwd: testDir, timeout: 90000 }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
|
||||
// Output should be valid JSON
|
||||
const jsonOutput = JSON.parse(result.stdout);
|
||||
expect(jsonOutput.topic).toBeDefined();
|
||||
@@ -188,11 +204,12 @@ describe('research command', () => {
|
||||
['--topic', 'CI/CD pipeline best practices'],
|
||||
{ cwd: testDir, timeout: 90000 }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
|
||||
// Should contain markdown formatting
|
||||
const hasMarkdown = result.stdout.includes('#') ||
|
||||
const hasMarkdown =
|
||||
result.stdout.includes('#') ||
|
||||
result.stdout.includes('*') ||
|
||||
result.stdout.includes('-');
|
||||
expect(hasMarkdown).toBe(true);
|
||||
@@ -203,10 +220,15 @@ describe('research command', () => {
|
||||
it('should research coding patterns', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'research',
|
||||
['--topic', 'Singleton pattern in JavaScript', '--category', 'patterns'],
|
||||
[
|
||||
'--topic',
|
||||
'Singleton pattern in JavaScript',
|
||||
'--category',
|
||||
'patterns'
|
||||
],
|
||||
{ cwd: testDir, timeout: 90000 }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout.toLowerCase()).toContain('singleton');
|
||||
expect(result.stdout.toLowerCase()).toContain('pattern');
|
||||
@@ -218,7 +240,7 @@ describe('research command', () => {
|
||||
['--topic', 'OWASP Top 10 vulnerabilities', '--category', 'security'],
|
||||
{ cwd: testDir, timeout: 90000 }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout.toLowerCase()).toContain('security');
|
||||
expect(result.stdout.toUpperCase()).toContain('OWASP');
|
||||
@@ -230,7 +252,7 @@ describe('research command', () => {
|
||||
['--topic', 'Database query optimization', '--category', 'performance'],
|
||||
{ cwd: testDir, timeout: 90000 }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout.toLowerCase()).toContain('optimization');
|
||||
expect(result.stdout.toLowerCase()).toContain('performance');
|
||||
@@ -246,14 +268,14 @@ describe('research command', () => {
|
||||
{ cwd: testDir }
|
||||
);
|
||||
const taskId = helpers.extractTaskId(addResult.stdout);
|
||||
|
||||
|
||||
// Research for the task
|
||||
const result = await helpers.taskMaster(
|
||||
'research',
|
||||
['--task', taskId, '--topic', 'WebSocket vs Server-Sent Events'],
|
||||
{ cwd: testDir, timeout: 90000 }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('Research Results');
|
||||
expect(result.stdout.toLowerCase()).toContain('websocket');
|
||||
@@ -267,19 +289,27 @@ describe('research command', () => {
|
||||
{ cwd: testDir }
|
||||
);
|
||||
const taskId = helpers.extractTaskId(addResult.stdout);
|
||||
|
||||
|
||||
// Research and append to task
|
||||
const result = await helpers.taskMaster(
|
||||
'research',
|
||||
['--task', taskId, '--topic', 'Prometheus vs ELK stack', '--append-to-task'],
|
||||
[
|
||||
'--task',
|
||||
taskId,
|
||||
'--topic',
|
||||
'Prometheus vs ELK stack',
|
||||
'--append-to-task'
|
||||
],
|
||||
{ cwd: testDir, timeout: 90000 }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('Research appended to task');
|
||||
|
||||
|
||||
// Verify task has research notes
|
||||
const showResult = await helpers.taskMaster('show', [taskId], { cwd: testDir });
|
||||
const showResult = await helpers.taskMaster('show', [taskId], {
|
||||
cwd: testDir
|
||||
});
|
||||
expect(showResult.stdout.toLowerCase()).toContain('prometheus');
|
||||
}, 120000);
|
||||
});
|
||||
@@ -292,13 +322,12 @@ describe('research command', () => {
|
||||
['--topic', 'GraphQL subscriptions'],
|
||||
{ cwd: testDir, timeout: 60000 }
|
||||
);
|
||||
|
||||
await helpers.taskMaster(
|
||||
'research',
|
||||
['--topic', 'Redis pub/sub'],
|
||||
{ cwd: testDir, timeout: 60000 }
|
||||
);
|
||||
|
||||
|
||||
await helpers.taskMaster('research', ['--topic', 'Redis pub/sub'], {
|
||||
cwd: testDir,
|
||||
timeout: 60000
|
||||
});
|
||||
|
||||
// Check research history
|
||||
const historyPath = join(testDir, '.taskmaster/research-history.json');
|
||||
if (existsSync(historyPath)) {
|
||||
@@ -314,14 +343,12 @@ describe('research command', () => {
|
||||
['--topic', 'Kubernetes deployment strategies'],
|
||||
{ cwd: testDir, timeout: 60000 }
|
||||
);
|
||||
|
||||
|
||||
// List history
|
||||
const result = await helpers.taskMaster(
|
||||
'research',
|
||||
['--history'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
const result = await helpers.taskMaster('research', ['--history'], {
|
||||
cwd: testDir
|
||||
});
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('Research History');
|
||||
}, 90000);
|
||||
@@ -329,12 +356,11 @@ describe('research command', () => {
|
||||
|
||||
describe('Error handling', () => {
|
||||
it('should fail without topic', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'research',
|
||||
[],
|
||||
{ cwd: testDir, allowFailure: true }
|
||||
);
|
||||
|
||||
const result = await helpers.taskMaster('research', [], {
|
||||
cwd: testDir,
|
||||
allowFailure: true
|
||||
});
|
||||
|
||||
expect(result.exitCode).not.toBe(0);
|
||||
expect(result.stderr).toContain('topic');
|
||||
});
|
||||
@@ -345,7 +371,7 @@ describe('research command', () => {
|
||||
['--topic', 'Test topic', '--output', 'invalid-format'],
|
||||
{ cwd: testDir, allowFailure: true }
|
||||
);
|
||||
|
||||
|
||||
expect(result.exitCode).not.toBe(0);
|
||||
expect(result.stderr).toContain('Invalid output format');
|
||||
});
|
||||
@@ -358,7 +384,7 @@ describe('research command', () => {
|
||||
['--topic', 'Test with potential network issues'],
|
||||
{ cwd: testDir, timeout: 30000, allowFailure: true }
|
||||
);
|
||||
|
||||
|
||||
// Should either succeed or fail gracefully
|
||||
if (result.exitCode !== 0) {
|
||||
expect(result.stderr).toBeTruthy();
|
||||
@@ -372,10 +398,15 @@ describe('research command', () => {
|
||||
it('should research implementation details', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'research',
|
||||
['--topic', 'JWT implementation in Node.js', '--focus', 'implementation'],
|
||||
[
|
||||
'--topic',
|
||||
'JWT implementation in Node.js',
|
||||
'--focus',
|
||||
'implementation'
|
||||
],
|
||||
{ cwd: testDir, timeout: 90000 }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout.toLowerCase()).toContain('implementation');
|
||||
expect(result.stdout.toLowerCase()).toContain('code');
|
||||
@@ -387,7 +418,7 @@ describe('research command', () => {
|
||||
['--topic', 'REST API versioning', '--focus', 'best-practices'],
|
||||
{ cwd: testDir, timeout: 90000 }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout.toLowerCase()).toContain('best practice');
|
||||
}, 120000);
|
||||
@@ -398,7 +429,7 @@ describe('research command', () => {
|
||||
['--topic', 'Vue vs React vs Angular', '--focus', 'comparison'],
|
||||
{ cwd: testDir, timeout: 90000 }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
const output = result.stdout.toLowerCase();
|
||||
expect(output).toContain('vue');
|
||||
@@ -414,7 +445,7 @@ describe('research command', () => {
|
||||
['--topic', 'Machine learning basics', '--max-length', '500'],
|
||||
{ cwd: testDir, timeout: 60000 }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
// Research output should be concise
|
||||
expect(result.stdout.length).toBeLessThan(2000); // Accounting for formatting
|
||||
@@ -426,10 +457,11 @@ describe('research command', () => {
|
||||
['--topic', 'Latest JavaScript features', '--year', '2024'],
|
||||
{ cwd: testDir, timeout: 90000 }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
// Should focus on recent content
|
||||
const hasRecentInfo = result.stdout.includes('2024') ||
|
||||
const hasRecentInfo =
|
||||
result.stdout.includes('2024') ||
|
||||
result.stdout.toLowerCase().includes('latest') ||
|
||||
result.stdout.toLowerCase().includes('recent');
|
||||
expect(hasRecentInfo).toBe(true);
|
||||
@@ -439,27 +471,25 @@ describe('research command', () => {
|
||||
describe('Research caching', () => {
|
||||
it('should cache and reuse research results', async () => {
|
||||
const topic = 'Redis caching strategies';
|
||||
|
||||
|
||||
// First research
|
||||
const startTime1 = Date.now();
|
||||
const result1 = await helpers.taskMaster(
|
||||
'research',
|
||||
['--topic', topic],
|
||||
{ cwd: testDir, timeout: 90000 }
|
||||
);
|
||||
const result1 = await helpers.taskMaster('research', ['--topic', topic], {
|
||||
cwd: testDir,
|
||||
timeout: 90000
|
||||
});
|
||||
const duration1 = Date.now() - startTime1;
|
||||
expect(result1).toHaveExitCode(0);
|
||||
|
||||
|
||||
// Second research (should be cached)
|
||||
const startTime2 = Date.now();
|
||||
const result2 = await helpers.taskMaster(
|
||||
'research',
|
||||
['--topic', topic],
|
||||
{ cwd: testDir, timeout: 30000 }
|
||||
);
|
||||
const result2 = await helpers.taskMaster('research', ['--topic', topic], {
|
||||
cwd: testDir,
|
||||
timeout: 30000
|
||||
});
|
||||
const duration2 = Date.now() - startTime2;
|
||||
expect(result2).toHaveExitCode(0);
|
||||
|
||||
|
||||
// Cached result should be much faster
|
||||
if (result2.stdout.includes('(cached)')) {
|
||||
expect(duration2).toBeLessThan(duration1 / 2);
|
||||
@@ -468,23 +498,22 @@ describe('research command', () => {
|
||||
|
||||
it('should bypass cache with --no-cache flag', async () => {
|
||||
const topic = 'Docker best practices';
|
||||
|
||||
|
||||
// First research
|
||||
await helpers.taskMaster(
|
||||
'research',
|
||||
['--topic', topic],
|
||||
{ cwd: testDir, timeout: 60000 }
|
||||
);
|
||||
|
||||
await helpers.taskMaster('research', ['--topic', topic], {
|
||||
cwd: testDir,
|
||||
timeout: 60000
|
||||
});
|
||||
|
||||
// Second research without cache
|
||||
const result = await helpers.taskMaster(
|
||||
'research',
|
||||
['--topic', topic, '--no-cache'],
|
||||
{ cwd: testDir, timeout: 90000 }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).not.toContain('(cached)');
|
||||
}, 180000);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
466
tests/e2e/tests/commands/set-status.test.js
Normal file
466
tests/e2e/tests/commands/set-status.test.js
Normal file
@@ -0,0 +1,466 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from '@jest/globals';
|
||||
import { mkdtempSync } from 'fs';
|
||||
import { tmpdir } from 'os';
|
||||
import { join } from 'path';
|
||||
import { rmSync, existsSync, readFileSync } from 'fs';
|
||||
|
||||
describe('task-master set-status', () => {
|
||||
let testDir;
|
||||
let helpers;
|
||||
|
||||
beforeEach(() => {
|
||||
// Create test directory
|
||||
testDir = mkdtempSync(join(tmpdir(), 'tm-test-set-status-'));
|
||||
process.chdir(testDir);
|
||||
|
||||
// Get helpers from global context
|
||||
helpers = global.testHelpers;
|
||||
|
||||
// Copy .env if exists
|
||||
const envPath = join(process.cwd(), '../../.env');
|
||||
if (existsSync(envPath)) {
|
||||
const envContent = readFileSync(envPath, 'utf-8');
|
||||
helpers.writeFile('.env', envContent);
|
||||
}
|
||||
|
||||
// Initialize task-master project
|
||||
const initResult = helpers.taskMaster('init', ['-y']);
|
||||
expect(initResult).toHaveExitCode(0);
|
||||
|
||||
// Ensure tasks.json exists
|
||||
const tasksPath = join(testDir, '.taskmaster/tasks/tasks.json');
|
||||
if (!helpers.fileExists(tasksPath)) {
|
||||
helpers.writeFile(tasksPath, JSON.stringify({ tasks: [] }, null, 2));
|
||||
}
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Clean up test directory
|
||||
process.chdir('..');
|
||||
rmSync(testDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
describe('Basic status changes', () => {
|
||||
it('should change task status to in-progress', () => {
|
||||
// Create a test task
|
||||
const addResult = helpers.taskMaster('add-task', ['Test task', '-m']);
|
||||
expect(addResult).toHaveExitCode(0);
|
||||
const taskId = helpers.extractTaskId(addResult.stdout);
|
||||
|
||||
// Set status to in-progress
|
||||
const result = helpers.taskMaster('set-status', [taskId, 'in-progress']);
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('Status updated');
|
||||
expect(result.stdout).toContain('in-progress');
|
||||
|
||||
// Verify status changed
|
||||
const showResult = helpers.taskMaster('show', [taskId]);
|
||||
expect(showResult.stdout).toContain('Status: in-progress');
|
||||
});
|
||||
|
||||
it('should change task status to done', () => {
|
||||
// Create task
|
||||
const addResult = helpers.taskMaster('add-task', [
|
||||
'Task to complete',
|
||||
'-m'
|
||||
]);
|
||||
const taskId = helpers.extractTaskId(addResult.stdout);
|
||||
|
||||
// Set status to done
|
||||
const result = helpers.taskMaster('set-status', [taskId, 'done']);
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('✓ Completed');
|
||||
|
||||
// Verify
|
||||
const showResult = helpers.taskMaster('show', [taskId]);
|
||||
expect(showResult.stdout).toContain('Status: done');
|
||||
});
|
||||
|
||||
it('should support all valid statuses', () => {
|
||||
const statuses = [
|
||||
'pending',
|
||||
'in-progress',
|
||||
'done',
|
||||
'blocked',
|
||||
'deferred',
|
||||
'cancelled'
|
||||
];
|
||||
|
||||
for (const status of statuses) {
|
||||
const addResult = helpers.taskMaster('add-task', [
|
||||
`Task for ${status}`,
|
||||
'-m'
|
||||
]);
|
||||
const taskId = helpers.extractTaskId(addResult.stdout);
|
||||
|
||||
const result = helpers.taskMaster('set-status', [taskId, status]);
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout.toLowerCase()).toContain(status);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Subtask status changes', () => {
|
||||
it('should change subtask status', () => {
|
||||
// Create parent task with subtasks
|
||||
const parentResult = helpers.taskMaster('add-task', [
|
||||
'Parent task',
|
||||
'-m'
|
||||
]);
|
||||
const parentId = helpers.extractTaskId(parentResult.stdout);
|
||||
|
||||
// Expand to add subtasks
|
||||
const expandResult = helpers.taskMaster(
|
||||
'expand',
|
||||
['-i', parentId, '-n', '2'],
|
||||
{ timeout: 60000 }
|
||||
);
|
||||
expect(expandResult).toHaveExitCode(0);
|
||||
|
||||
// Set subtask status
|
||||
const subtaskId = `${parentId}.1`;
|
||||
const result = helpers.taskMaster('set-status', [subtaskId, 'done']);
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('Subtask completed');
|
||||
|
||||
// Verify parent task shows progress
|
||||
const showResult = helpers.taskMaster('show', [parentId]);
|
||||
expect(showResult.stdout).toMatch(/Progress:.*1\/2/);
|
||||
});
|
||||
|
||||
it('should update parent status when all subtasks complete', () => {
|
||||
// Create parent task with subtasks
|
||||
const parentResult = helpers.taskMaster('add-task', [
|
||||
'Parent task',
|
||||
'-m'
|
||||
]);
|
||||
const parentId = helpers.extractTaskId(parentResult.stdout);
|
||||
|
||||
// Add subtasks
|
||||
helpers.taskMaster('expand', ['-i', parentId, '-n', '2'], {
|
||||
timeout: 60000
|
||||
});
|
||||
|
||||
// Complete all subtasks
|
||||
helpers.taskMaster('set-status', [`${parentId}.1`, 'done']);
|
||||
const result = helpers.taskMaster('set-status', [
|
||||
`${parentId}.2`,
|
||||
'done'
|
||||
]);
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('All subtasks completed');
|
||||
expect(result.stdout).toContain(
|
||||
'Parent task automatically marked as done'
|
||||
);
|
||||
|
||||
// Verify parent is done
|
||||
const showResult = helpers.taskMaster('show', [parentId]);
|
||||
expect(showResult.stdout).toContain('Status: done');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Bulk status updates', () => {
|
||||
it('should update status for multiple tasks', () => {
|
||||
// Create multiple tasks
|
||||
const task1 = helpers.taskMaster('add-task', ['Task 1', '-m']);
|
||||
const id1 = helpers.extractTaskId(task1.stdout);
|
||||
|
||||
const task2 = helpers.taskMaster('add-task', ['Task 2', '-m']);
|
||||
const id2 = helpers.extractTaskId(task2.stdout);
|
||||
|
||||
const task3 = helpers.taskMaster('add-task', ['Task 3', '-m']);
|
||||
const id3 = helpers.extractTaskId(task3.stdout);
|
||||
|
||||
// Update multiple tasks
|
||||
const result = helpers.taskMaster('set-status', [
|
||||
`${id1},${id2},${id3}`,
|
||||
'in-progress'
|
||||
]);
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('3 tasks updated');
|
||||
|
||||
// Verify all changed
|
||||
for (const id of [id1, id2, id3]) {
|
||||
const showResult = helpers.taskMaster('show', [id]);
|
||||
expect(showResult.stdout).toContain('Status: in-progress');
|
||||
}
|
||||
});
|
||||
|
||||
it('should update all pending tasks', () => {
|
||||
// Create tasks with mixed statuses
|
||||
const task1 = helpers.taskMaster('add-task', ['Pending 1', '-m']);
|
||||
const task2 = helpers.taskMaster('add-task', ['Pending 2', '-m']);
|
||||
|
||||
const task3 = helpers.taskMaster('add-task', ['Already done', '-m']);
|
||||
const id3 = helpers.extractTaskId(task3.stdout);
|
||||
helpers.taskMaster('set-status', [id3, 'done']);
|
||||
|
||||
// Update all pending tasks
|
||||
const result = helpers.taskMaster('set-status', [
|
||||
'--all',
|
||||
'in-progress',
|
||||
'--filter-status',
|
||||
'pending'
|
||||
]);
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('2 tasks updated');
|
||||
|
||||
// Verify already done task unchanged
|
||||
const showResult = helpers.taskMaster('show', [id3]);
|
||||
expect(showResult.stdout).toContain('Status: done');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Dependency handling', () => {
|
||||
it('should warn when setting blocked task to in-progress', () => {
|
||||
// Create dependency
|
||||
const dep = helpers.taskMaster('add-task', ['Dependency', '-m']);
|
||||
const depId = helpers.extractTaskId(dep.stdout);
|
||||
|
||||
// Create blocked task
|
||||
const task = helpers.taskMaster('add-task', [
|
||||
'Blocked task',
|
||||
'-m',
|
||||
'-d',
|
||||
depId
|
||||
]);
|
||||
const taskId = helpers.extractTaskId(task.stdout);
|
||||
|
||||
// Try to set to in-progress
|
||||
const result = helpers.taskMaster('set-status', [taskId, 'in-progress']);
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('Warning');
|
||||
expect(result.stdout).toContain('has incomplete dependencies');
|
||||
});
|
||||
|
||||
it('should unblock dependent tasks when dependency completes', () => {
|
||||
// Create dependency chain
|
||||
const task1 = helpers.taskMaster('add-task', ['First task', '-m']);
|
||||
const id1 = helpers.extractTaskId(task1.stdout);
|
||||
|
||||
const task2 = helpers.taskMaster('add-task', [
|
||||
'Dependent task',
|
||||
'-m',
|
||||
'-d',
|
||||
id1
|
||||
]);
|
||||
const id2 = helpers.extractTaskId(task2.stdout);
|
||||
|
||||
// Complete first task
|
||||
const result = helpers.taskMaster('set-status', [id1, 'done']);
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('Unblocked tasks:');
|
||||
expect(result.stdout).toContain(`${id2} - Dependent task`);
|
||||
});
|
||||
|
||||
it('should handle force flag for blocked tasks', () => {
|
||||
// Create blocked task
|
||||
const dep = helpers.taskMaster('add-task', ['Incomplete dep', '-m']);
|
||||
const depId = helpers.extractTaskId(dep.stdout);
|
||||
|
||||
const task = helpers.taskMaster('add-task', [
|
||||
'Force complete',
|
||||
'-m',
|
||||
'-d',
|
||||
depId
|
||||
]);
|
||||
const taskId = helpers.extractTaskId(task.stdout);
|
||||
|
||||
// Force complete despite dependencies
|
||||
const result = helpers.taskMaster('set-status', [
|
||||
taskId,
|
||||
'done',
|
||||
'--force'
|
||||
]);
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('Force completing');
|
||||
expect(result.stdout).not.toContain('Warning');
|
||||
|
||||
// Verify it's done
|
||||
const showResult = helpers.taskMaster('show', [taskId]);
|
||||
expect(showResult.stdout).toContain('Status: done');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Status transitions', () => {
|
||||
it('should prevent invalid status transitions', () => {
|
||||
// Create completed task
|
||||
const task = helpers.taskMaster('add-task', ['Completed task', '-m']);
|
||||
const taskId = helpers.extractTaskId(task.stdout);
|
||||
helpers.taskMaster('set-status', [taskId, 'done']);
|
||||
|
||||
// Try to set back to pending
|
||||
const result = helpers.taskMaster('set-status', [taskId, 'pending']);
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('Warning');
|
||||
expect(result.stdout).toContain('Unusual status transition');
|
||||
});
|
||||
|
||||
it('should allow reopening cancelled tasks', () => {
|
||||
// Create and cancel task
|
||||
const task = helpers.taskMaster('add-task', ['Cancelled task', '-m']);
|
||||
const taskId = helpers.extractTaskId(task.stdout);
|
||||
helpers.taskMaster('set-status', [taskId, 'cancelled']);
|
||||
|
||||
// Reopen task
|
||||
const result = helpers.taskMaster('set-status', [taskId, 'pending']);
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('Task reopened');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Tag context', () => {
|
||||
it('should update status for task in specific tag', () => {
|
||||
// Create tag and task
|
||||
helpers.taskMaster('add-tag', ['feature']);
|
||||
helpers.taskMaster('use-tag', ['feature']);
|
||||
|
||||
const task = helpers.taskMaster('add-task', ['Feature task', '-m']);
|
||||
const taskId = helpers.extractTaskId(task.stdout);
|
||||
|
||||
// Update status with tag context
|
||||
const result = helpers.taskMaster('set-status', [
|
||||
taskId,
|
||||
'done',
|
||||
'--tag',
|
||||
'feature'
|
||||
]);
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('[feature]');
|
||||
expect(result.stdout).toContain('Status updated');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Interactive features', () => {
|
||||
it('should show next task suggestion after completing', () => {
|
||||
// Create multiple tasks
|
||||
helpers.taskMaster('add-task', ['Task 1', '-m', '-p', 'high']);
|
||||
const task2 = helpers.taskMaster('add-task', [
|
||||
'Task 2',
|
||||
'-m',
|
||||
'-p',
|
||||
'high'
|
||||
]);
|
||||
const id2 = helpers.extractTaskId(task2.stdout);
|
||||
|
||||
// Complete first task
|
||||
const result = helpers.taskMaster('set-status', [id2, 'done']);
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('Next suggested task:');
|
||||
expect(result.stdout).toContain('Task 1');
|
||||
});
|
||||
|
||||
it('should provide time tracking prompts', () => {
|
||||
// Create task
|
||||
const task = helpers.taskMaster('add-task', ['Timed task', '-m']);
|
||||
const taskId = helpers.extractTaskId(task.stdout);
|
||||
|
||||
// Start task
|
||||
const startResult = helpers.taskMaster('set-status', [
|
||||
taskId,
|
||||
'in-progress'
|
||||
]);
|
||||
expect(startResult).toHaveExitCode(0);
|
||||
expect(startResult.stdout).toContain('Started at:');
|
||||
|
||||
// Complete task
|
||||
const endResult = helpers.taskMaster('set-status', [taskId, 'done']);
|
||||
expect(endResult).toHaveExitCode(0);
|
||||
expect(endResult.stdout).toContain('Time spent:');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error handling', () => {
|
||||
it('should handle invalid task ID', () => {
|
||||
const result = helpers.taskMaster('set-status', ['999', 'done'], {
|
||||
allowFailure: true
|
||||
});
|
||||
expect(result.exitCode).not.toBe(0);
|
||||
expect(result.stderr).toMatch(/Task.*not found/i);
|
||||
});
|
||||
|
||||
it('should handle invalid status value', () => {
|
||||
const task = helpers.taskMaster('add-task', ['Test task', '-m']);
|
||||
const taskId = helpers.extractTaskId(task.stdout);
|
||||
|
||||
const result = helpers.taskMaster(
|
||||
'set-status',
|
||||
[taskId, 'invalid-status'],
|
||||
{ allowFailure: true }
|
||||
);
|
||||
expect(result.exitCode).not.toBe(0);
|
||||
expect(result.stderr).toContain('Invalid status');
|
||||
expect(result.stderr).toContain('pending, in-progress, done');
|
||||
});
|
||||
|
||||
it('should handle missing required arguments', () => {
|
||||
const result = helpers.taskMaster('set-status', [], {
|
||||
allowFailure: true
|
||||
});
|
||||
expect(result.exitCode).not.toBe(0);
|
||||
expect(result.stderr).toContain('required');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Batch operations', () => {
|
||||
it('should handle range-based updates', () => {
|
||||
// Create sequential tasks
|
||||
const ids = [];
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const result = helpers.taskMaster('add-task', [`Task ${i + 1}`, '-m']);
|
||||
ids.push(helpers.extractTaskId(result.stdout));
|
||||
}
|
||||
|
||||
// Update range
|
||||
const result = helpers.taskMaster('set-status', [
|
||||
'--from',
|
||||
ids[1],
|
||||
'--to',
|
||||
ids[3],
|
||||
'in-progress'
|
||||
]);
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('3 tasks updated');
|
||||
|
||||
// Verify middle tasks updated
|
||||
for (let i = 1; i <= 3; i++) {
|
||||
const showResult = helpers.taskMaster('show', [ids[i]]);
|
||||
expect(showResult.stdout).toContain('Status: in-progress');
|
||||
}
|
||||
|
||||
// Verify edge tasks not updated
|
||||
const show0 = helpers.taskMaster('show', [ids[0]]);
|
||||
expect(show0.stdout).toContain('Status: pending');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Output options', () => {
|
||||
it('should support quiet mode', () => {
|
||||
const task = helpers.taskMaster('add-task', ['Test task', '-m']);
|
||||
const taskId = helpers.extractTaskId(task.stdout);
|
||||
|
||||
const result = helpers.taskMaster('set-status', [taskId, 'done', '-q']);
|
||||
expect(result).toHaveExitCode(0);
|
||||
// Quiet mode should have minimal output
|
||||
expect(result.stdout.split('\n').length).toBeLessThan(3);
|
||||
});
|
||||
|
||||
it('should support JSON output', () => {
|
||||
const task = helpers.taskMaster('add-task', ['Test task', '-m']);
|
||||
const taskId = helpers.extractTaskId(task.stdout);
|
||||
|
||||
const result = helpers.taskMaster('set-status', [
|
||||
taskId,
|
||||
'done',
|
||||
'--json'
|
||||
]);
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
const json = JSON.parse(result.stdout);
|
||||
expect(json.updated).toBe(1);
|
||||
expect(json.tasks[0].id).toBe(parseInt(taskId));
|
||||
expect(json.tasks[0].status).toBe('done');
|
||||
});
|
||||
});
|
||||
});
|
||||
411
tests/e2e/tests/commands/show.test.js
Normal file
411
tests/e2e/tests/commands/show.test.js
Normal file
@@ -0,0 +1,411 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from '@jest/globals';
|
||||
import { mkdtempSync } from 'fs';
|
||||
import { tmpdir } from 'os';
|
||||
import { join } from 'path';
|
||||
import { rmSync, existsSync, readFileSync } from 'fs';
|
||||
|
||||
describe('task-master show', () => {
|
||||
let testDir;
|
||||
let helpers;
|
||||
|
||||
beforeEach(() => {
|
||||
// Create test directory
|
||||
testDir = mkdtempSync(join(tmpdir(), 'tm-test-show-'));
|
||||
process.chdir(testDir);
|
||||
|
||||
// Get helpers from global context
|
||||
helpers = global.testHelpers;
|
||||
|
||||
// Copy .env if exists
|
||||
const envPath = join(process.cwd(), '../../.env');
|
||||
if (existsSync(envPath)) {
|
||||
const envContent = readFileSync(envPath, 'utf-8');
|
||||
helpers.writeFile('.env', envContent);
|
||||
}
|
||||
|
||||
// Initialize task-master project
|
||||
const initResult = helpers.taskMaster('init', ['-y']);
|
||||
expect(initResult).toHaveExitCode(0);
|
||||
|
||||
// Ensure tasks.json exists
|
||||
const tasksPath = join(testDir, '.taskmaster/tasks/tasks.json');
|
||||
if (!helpers.fileExists(tasksPath)) {
|
||||
helpers.writeFile(tasksPath, JSON.stringify({ tasks: [] }, null, 2));
|
||||
}
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Clean up test directory
|
||||
process.chdir('..');
|
||||
rmSync(testDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
describe('Basic functionality', () => {
|
||||
it('should show task details by ID', () => {
|
||||
// Create a test task
|
||||
const addResult = helpers.taskMaster('add-task', [
|
||||
'Test task for show command',
|
||||
'-m',
|
||||
'-p',
|
||||
'high'
|
||||
]);
|
||||
expect(addResult).toHaveExitCode(0);
|
||||
const taskId = helpers.extractTaskId(addResult.stdout);
|
||||
|
||||
// Show task details
|
||||
const result = helpers.taskMaster('show', [taskId]);
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('Test task for show command');
|
||||
expect(result.stdout).toContain(`Task ID: ${taskId}`);
|
||||
expect(result.stdout).toContain('Priority: high');
|
||||
expect(result.stdout).toContain('Status: pending');
|
||||
});
|
||||
|
||||
it('should show error for non-existent task ID', () => {
|
||||
const result = helpers.taskMaster('show', ['999'], {
|
||||
allowFailure: true
|
||||
});
|
||||
expect(result.exitCode).not.toBe(0);
|
||||
expect(result.stderr).toMatch(/Task.*not found|does not exist/i);
|
||||
});
|
||||
|
||||
it('should show task with all metadata', () => {
|
||||
// Create task with dependencies and tags
|
||||
const dep1 = helpers.taskMaster('add-task', ['Dependency 1', '-m']);
|
||||
const depId1 = helpers.extractTaskId(dep1.stdout);
|
||||
|
||||
const addResult = helpers.taskMaster('add-task', [
|
||||
'Complex task',
|
||||
'-m',
|
||||
'-p',
|
||||
'medium',
|
||||
'-d',
|
||||
depId1,
|
||||
'--tags',
|
||||
'backend,api'
|
||||
]);
|
||||
const taskId = helpers.extractTaskId(addResult.stdout);
|
||||
|
||||
const result = helpers.taskMaster('show', [taskId]);
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('Dependencies:');
|
||||
expect(result.stdout).toContain(depId1);
|
||||
expect(result.stdout).toContain('Tags: backend, api');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Subtask display', () => {
|
||||
it('should show task with subtasks', () => {
|
||||
// Create parent task
|
||||
const parentResult = helpers.taskMaster('add-task', [
|
||||
'Parent task with subtasks',
|
||||
'-m'
|
||||
]);
|
||||
const parentId = helpers.extractTaskId(parentResult.stdout);
|
||||
|
||||
// Expand to add subtasks
|
||||
const expandResult = helpers.taskMaster(
|
||||
'expand',
|
||||
['-i', parentId, '-n', '3'],
|
||||
{ timeout: 60000 }
|
||||
);
|
||||
expect(expandResult).toHaveExitCode(0);
|
||||
|
||||
// Show parent task
|
||||
const result = helpers.taskMaster('show', [parentId]);
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('Subtasks (3):');
|
||||
expect(result.stdout).toMatch(/\d+\.1.*pending/);
|
||||
expect(result.stdout).toMatch(/\d+\.2.*pending/);
|
||||
expect(result.stdout).toMatch(/\d+\.3.*pending/);
|
||||
});
|
||||
|
||||
it('should show subtask details directly', () => {
|
||||
// Create parent task with subtasks
|
||||
const parentResult = helpers.taskMaster('add-task', [
|
||||
'Parent task',
|
||||
'-m'
|
||||
]);
|
||||
const parentId = helpers.extractTaskId(parentResult.stdout);
|
||||
|
||||
const expandResult = helpers.taskMaster(
|
||||
'expand',
|
||||
['-i', parentId, '-n', '2'],
|
||||
{ timeout: 60000 }
|
||||
);
|
||||
expect(expandResult).toHaveExitCode(0);
|
||||
|
||||
// Show specific subtask
|
||||
const subtaskId = `${parentId}.1`;
|
||||
const result = helpers.taskMaster('show', [subtaskId]);
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain(`Subtask ID: ${subtaskId}`);
|
||||
expect(result.stdout).toContain(`Parent Task: ${parentId}`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Dependency visualization', () => {
|
||||
it('should show dependency graph', () => {
|
||||
// Create dependency chain
|
||||
const task1 = helpers.taskMaster('add-task', ['Task 1', '-m']);
|
||||
const id1 = helpers.extractTaskId(task1.stdout);
|
||||
|
||||
const task2 = helpers.taskMaster('add-task', ['Task 2', '-m', '-d', id1]);
|
||||
const id2 = helpers.extractTaskId(task2.stdout);
|
||||
|
||||
const task3 = helpers.taskMaster('add-task', [
|
||||
'Task 3',
|
||||
'-m',
|
||||
'-d',
|
||||
`${id1},${id2}`
|
||||
]);
|
||||
const id3 = helpers.extractTaskId(task3.stdout);
|
||||
|
||||
// Show task with dependencies
|
||||
const result = helpers.taskMaster('show', [id3]);
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('Dependencies:');
|
||||
expect(result.stdout).toContain(`${id1} - Task 1`);
|
||||
expect(result.stdout).toContain(`${id2} - Task 2`);
|
||||
expect(result.stdout).toMatch(/Status:.*pending/);
|
||||
});
|
||||
|
||||
it('should show tasks depending on current task', () => {
|
||||
// Create dependency chain
|
||||
const task1 = helpers.taskMaster('add-task', ['Base task', '-m']);
|
||||
const id1 = helpers.extractTaskId(task1.stdout);
|
||||
|
||||
const task2 = helpers.taskMaster('add-task', [
|
||||
'Dependent task',
|
||||
'-m',
|
||||
'-d',
|
||||
id1
|
||||
]);
|
||||
const id2 = helpers.extractTaskId(task2.stdout);
|
||||
|
||||
// Show base task
|
||||
const result = helpers.taskMaster('show', [id1]);
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('Tasks depending on this:');
|
||||
expect(result.stdout).toContain(`${id2} - Dependent task`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Status and progress', () => {
|
||||
it('should show task progress for parent with subtasks', () => {
|
||||
// Create parent task with subtasks
|
||||
const parentResult = helpers.taskMaster('add-task', [
|
||||
'Parent task',
|
||||
'-m'
|
||||
]);
|
||||
const parentId = helpers.extractTaskId(parentResult.stdout);
|
||||
|
||||
// Expand to add subtasks
|
||||
helpers.taskMaster('expand', ['-i', parentId, '-n', '3'], {
|
||||
timeout: 60000
|
||||
});
|
||||
|
||||
// Mark one subtask as done
|
||||
helpers.taskMaster('set-status', [`${parentId}.1`, 'done']);
|
||||
|
||||
// Show parent task
|
||||
const result = helpers.taskMaster('show', [parentId]);
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toMatch(/Progress:.*1\/3.*33%/);
|
||||
expect(result.stdout).toContain('└─ ✓'); // Done subtask indicator
|
||||
});
|
||||
|
||||
it('should show different status indicators', () => {
|
||||
// Create tasks with different statuses
|
||||
const tasks = [
|
||||
{ status: 'pending', title: 'Pending task' },
|
||||
{ status: 'in-progress', title: 'In progress task' },
|
||||
{ status: 'done', title: 'Done task' },
|
||||
{ status: 'blocked', title: 'Blocked task' },
|
||||
{ status: 'deferred', title: 'Deferred task' },
|
||||
{ status: 'cancelled', title: 'Cancelled task' }
|
||||
];
|
||||
|
||||
for (const { status, title } of tasks) {
|
||||
const addResult = helpers.taskMaster('add-task', [title, '-m']);
|
||||
const taskId = helpers.extractTaskId(addResult.stdout);
|
||||
|
||||
if (status !== 'pending') {
|
||||
helpers.taskMaster('set-status', [taskId, status]);
|
||||
}
|
||||
|
||||
const showResult = helpers.taskMaster('show', [taskId]);
|
||||
expect(showResult).toHaveExitCode(0);
|
||||
expect(showResult.stdout).toContain(`Status: ${status}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Complexity information', () => {
|
||||
it('should show complexity score when available', () => {
|
||||
// Create a complex task
|
||||
const addResult = helpers.taskMaster('add-task', [
|
||||
'Build a distributed microservices architecture with Kubernetes',
|
||||
'-m'
|
||||
]);
|
||||
const taskId = helpers.extractTaskId(addResult.stdout);
|
||||
|
||||
// Analyze complexity
|
||||
const analyzeResult = helpers.taskMaster(
|
||||
'analyze-complexity',
|
||||
['-i', taskId],
|
||||
{ timeout: 60000 }
|
||||
);
|
||||
|
||||
if (analyzeResult.exitCode === 0) {
|
||||
const result = helpers.taskMaster('show', [taskId]);
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toMatch(/Complexity Score:.*\d+/);
|
||||
expect(result.stdout).toContain('Recommended subtasks:');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Research and documentation', () => {
|
||||
it('should show research notes if available', () => {
|
||||
// Create task
|
||||
const addResult = helpers.taskMaster('add-task', ['Research task', '-m']);
|
||||
const taskId = helpers.extractTaskId(addResult.stdout);
|
||||
|
||||
// Add research notes (would normally be done via research command)
|
||||
// For now, we'll check that the section appears
|
||||
const result = helpers.taskMaster('show', [taskId]);
|
||||
expect(result).toHaveExitCode(0);
|
||||
// The show command should have a section for research notes
|
||||
// even if empty
|
||||
});
|
||||
});
|
||||
|
||||
describe('Tag context', () => {
|
||||
it('should show task from specific tag', () => {
|
||||
// Create a new tag
|
||||
helpers.taskMaster('add-tag', ['feature-branch']);
|
||||
|
||||
// Add task to feature tag
|
||||
helpers.taskMaster('use-tag', ['feature-branch']);
|
||||
const addResult = helpers.taskMaster('add-task', ['Feature task', '-m']);
|
||||
const taskId = helpers.extractTaskId(addResult.stdout);
|
||||
|
||||
// Show task with tag context
|
||||
const result = helpers.taskMaster('show', [
|
||||
taskId,
|
||||
'--tag',
|
||||
'feature-branch'
|
||||
]);
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('Feature task');
|
||||
expect(result.stdout).toContain('[feature-branch]');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Output formats', () => {
|
||||
it('should show task in JSON format', () => {
|
||||
// Create task
|
||||
const addResult = helpers.taskMaster('add-task', [
|
||||
'JSON format test',
|
||||
'-m'
|
||||
]);
|
||||
const taskId = helpers.extractTaskId(addResult.stdout);
|
||||
|
||||
// Show in JSON format
|
||||
const result = helpers.taskMaster('show', [taskId, '--json']);
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
// Parse JSON output
|
||||
const jsonOutput = JSON.parse(result.stdout);
|
||||
expect(jsonOutput.id).toBe(parseInt(taskId));
|
||||
expect(jsonOutput.title).toBe('JSON format test');
|
||||
expect(jsonOutput.status).toBe('pending');
|
||||
});
|
||||
|
||||
it('should show minimal output with quiet flag', () => {
|
||||
// Create task
|
||||
const addResult = helpers.taskMaster('add-task', [
|
||||
'Quiet mode test',
|
||||
'-m'
|
||||
]);
|
||||
const taskId = helpers.extractTaskId(addResult.stdout);
|
||||
|
||||
// Show in quiet mode
|
||||
const result = helpers.taskMaster('show', [taskId, '-q']);
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('Quiet mode test');
|
||||
// Should have less output than normal
|
||||
expect(result.stdout.split('\n').length).toBeLessThan(20);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Navigation suggestions', () => {
|
||||
it('should show next/previous task suggestions', () => {
|
||||
// Create multiple tasks
|
||||
const task1 = helpers.taskMaster('add-task', ['First task', '-m']);
|
||||
const id1 = helpers.extractTaskId(task1.stdout);
|
||||
|
||||
const task2 = helpers.taskMaster('add-task', ['Second task', '-m']);
|
||||
const id2 = helpers.extractTaskId(task2.stdout);
|
||||
|
||||
const task3 = helpers.taskMaster('add-task', ['Third task', '-m']);
|
||||
const id3 = helpers.extractTaskId(task3.stdout);
|
||||
|
||||
// Show middle task
|
||||
const result = helpers.taskMaster('show', [id2]);
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('Navigation:');
|
||||
expect(result.stdout).toContain(`Previous: ${id1}`);
|
||||
expect(result.stdout).toContain(`Next: ${id3}`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error handling', () => {
|
||||
it('should handle invalid task ID format', () => {
|
||||
const result = helpers.taskMaster('show', ['invalid-id'], {
|
||||
allowFailure: true
|
||||
});
|
||||
expect(result.exitCode).not.toBe(0);
|
||||
expect(result.stderr).toContain('Invalid task ID');
|
||||
});
|
||||
|
||||
it('should handle missing tasks file', () => {
|
||||
const result = helpers.taskMaster(
|
||||
'show',
|
||||
['1', '--file', 'non-existent.json'],
|
||||
{ allowFailure: true }
|
||||
);
|
||||
expect(result.exitCode).not.toBe(0);
|
||||
expect(result.stderr).toContain('Error');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Performance', () => {
|
||||
it('should show task with many subtasks efficiently', () => {
|
||||
// Create parent task
|
||||
const parentResult = helpers.taskMaster('add-task', [
|
||||
'Large parent task',
|
||||
'-m'
|
||||
]);
|
||||
const parentId = helpers.extractTaskId(parentResult.stdout);
|
||||
|
||||
// Expand with many subtasks
|
||||
const expandResult = helpers.taskMaster(
|
||||
'expand',
|
||||
['-i', parentId, '-n', '10'],
|
||||
{ timeout: 120000 }
|
||||
);
|
||||
expect(expandResult).toHaveExitCode(0);
|
||||
|
||||
// Show should handle many subtasks efficiently
|
||||
const startTime = Date.now();
|
||||
const result = helpers.taskMaster('show', [parentId]);
|
||||
const endTime = Date.now();
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('Subtasks (10):');
|
||||
expect(endTime - startTime).toBeLessThan(2000); // Should be fast
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -3,7 +3,14 @@
|
||||
* Tests all aspects of subtask updates including AI-powered updates
|
||||
*/
|
||||
|
||||
const { mkdtempSync, existsSync, readFileSync, rmSync, writeFileSync, mkdirSync } = require('fs');
|
||||
const {
|
||||
mkdtempSync,
|
||||
existsSync,
|
||||
readFileSync,
|
||||
rmSync,
|
||||
writeFileSync,
|
||||
mkdirSync
|
||||
} = require('fs');
|
||||
const { join } = require('path');
|
||||
const { tmpdir } = require('os');
|
||||
|
||||
@@ -16,11 +23,11 @@ describe('update-subtask command', () => {
|
||||
beforeEach(async () => {
|
||||
// Create test directory
|
||||
testDir = mkdtempSync(join(tmpdir(), 'task-master-update-subtask-'));
|
||||
|
||||
|
||||
// Initialize test helpers
|
||||
const context = global.createTestContext('update-subtask');
|
||||
helpers = context.helpers;
|
||||
|
||||
|
||||
// Copy .env file if it exists
|
||||
const mainEnvPath = join(__dirname, '../../../../.env');
|
||||
const testEnvPath = join(testDir, '.env');
|
||||
@@ -28,11 +35,13 @@ describe('update-subtask command', () => {
|
||||
const envContent = readFileSync(mainEnvPath, 'utf8');
|
||||
writeFileSync(testEnvPath, envContent);
|
||||
}
|
||||
|
||||
|
||||
// Initialize task-master project
|
||||
const initResult = await helpers.taskMaster('init', ['-y'], { cwd: testDir });
|
||||
const initResult = await helpers.taskMaster('init', ['-y'], {
|
||||
cwd: testDir
|
||||
});
|
||||
expect(initResult).toHaveExitCode(0);
|
||||
|
||||
|
||||
// Create a parent task with subtask
|
||||
const parentResult = await helpers.taskMaster(
|
||||
'add-task',
|
||||
@@ -40,7 +49,7 @@ describe('update-subtask command', () => {
|
||||
{ cwd: testDir }
|
||||
);
|
||||
parentTaskId = helpers.extractTaskId(parentResult.stdout);
|
||||
|
||||
|
||||
// Create a subtask
|
||||
const subtaskResult = await helpers.taskMaster(
|
||||
'add-subtask',
|
||||
@@ -66,12 +75,14 @@ describe('update-subtask command', () => {
|
||||
[subtaskId, 'Updated subtask title'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('Updated subtask');
|
||||
|
||||
|
||||
// Verify update
|
||||
const showResult = await helpers.taskMaster('show', [parentTaskId], { cwd: testDir });
|
||||
const showResult = await helpers.taskMaster('show', [parentTaskId], {
|
||||
cwd: testDir
|
||||
});
|
||||
expect(showResult.stdout).toContain('Updated subtask title');
|
||||
});
|
||||
|
||||
@@ -81,11 +92,13 @@ describe('update-subtask command', () => {
|
||||
[subtaskId, '--notes', 'Implementation details: Use async/await'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
|
||||
// Verify notes were added
|
||||
const showResult = await helpers.taskMaster('show', [parentTaskId], { cwd: testDir });
|
||||
const showResult = await helpers.taskMaster('show', [parentTaskId], {
|
||||
cwd: testDir
|
||||
});
|
||||
expect(showResult.stdout).toContain('async/await');
|
||||
});
|
||||
|
||||
@@ -95,11 +108,13 @@ describe('update-subtask command', () => {
|
||||
[subtaskId, '--status', 'completed'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
|
||||
// Verify status update
|
||||
const showResult = await helpers.taskMaster('show', [parentTaskId], { cwd: testDir });
|
||||
const showResult = await helpers.taskMaster('show', [parentTaskId], {
|
||||
cwd: testDir
|
||||
});
|
||||
expect(showResult.stdout.toLowerCase()).toContain('completed');
|
||||
});
|
||||
});
|
||||
@@ -111,16 +126,18 @@ describe('update-subtask command', () => {
|
||||
[subtaskId, '--prompt', 'Add implementation steps and best practices'],
|
||||
{ cwd: testDir, timeout: 45000 }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('Updated subtask');
|
||||
|
||||
|
||||
// Verify AI enhanced the subtask
|
||||
const tasksPath = join(testDir, '.taskmaster/tasks/tasks.json');
|
||||
const tasks = JSON.parse(readFileSync(tasksPath, 'utf8'));
|
||||
const parentTask = tasks.master.tasks.find(t => t.id === parseInt(parentTaskId));
|
||||
const subtask = parentTask.subtasks.find(s => s.id === subtaskId);
|
||||
|
||||
const parentTask = tasks.master.tasks.find(
|
||||
(t) => t.id === parseInt(parentTaskId)
|
||||
);
|
||||
const subtask = parentTask.subtasks.find((s) => s.id === subtaskId);
|
||||
|
||||
// Should have more detailed content
|
||||
expect(subtask.title.length).toBeGreaterThan(20);
|
||||
}, 60000);
|
||||
@@ -128,15 +145,22 @@ describe('update-subtask command', () => {
|
||||
it('should enhance subtask with technical details', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'update-subtask',
|
||||
[subtaskId, '--prompt', 'Add technical requirements and edge cases to consider'],
|
||||
[
|
||||
subtaskId,
|
||||
'--prompt',
|
||||
'Add technical requirements and edge cases to consider'
|
||||
],
|
||||
{ cwd: testDir, timeout: 45000 }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
|
||||
// Check that subtask was enhanced
|
||||
const showResult = await helpers.taskMaster('show', [parentTaskId], { cwd: testDir });
|
||||
const hasEnhancement = showResult.stdout.toLowerCase().includes('requirement') ||
|
||||
const showResult = await helpers.taskMaster('show', [parentTaskId], {
|
||||
cwd: testDir
|
||||
});
|
||||
const hasEnhancement =
|
||||
showResult.stdout.toLowerCase().includes('requirement') ||
|
||||
showResult.stdout.toLowerCase().includes('edge case') ||
|
||||
showResult.stdout.toLowerCase().includes('consider');
|
||||
expect(hasEnhancement).toBe(true);
|
||||
@@ -147,17 +171,21 @@ describe('update-subtask command', () => {
|
||||
'update-subtask',
|
||||
[
|
||||
subtaskId,
|
||||
'--prompt', 'Add industry best practices for error handling',
|
||||
'--prompt',
|
||||
'Add industry best practices for error handling',
|
||||
'--research'
|
||||
],
|
||||
{ cwd: testDir, timeout: 90000 }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
|
||||
// Research mode should add comprehensive content
|
||||
const showResult = await helpers.taskMaster('show', [parentTaskId], { cwd: testDir });
|
||||
const hasResearchContent = showResult.stdout.toLowerCase().includes('error') ||
|
||||
const showResult = await helpers.taskMaster('show', [parentTaskId], {
|
||||
cwd: testDir
|
||||
});
|
||||
const hasResearchContent =
|
||||
showResult.stdout.toLowerCase().includes('error') ||
|
||||
showResult.stdout.toLowerCase().includes('handling') ||
|
||||
showResult.stdout.toLowerCase().includes('practice');
|
||||
expect(hasResearchContent).toBe(true);
|
||||
@@ -174,25 +202,27 @@ describe('update-subtask command', () => {
|
||||
);
|
||||
const match = subtask2Result.stdout.match(/subtask #?(\d+\.\d+)/i);
|
||||
const subtaskId2 = match ? match[1] : '1.2';
|
||||
|
||||
|
||||
// Update first subtask
|
||||
await helpers.taskMaster(
|
||||
'update-subtask',
|
||||
[subtaskId, 'First subtask updated'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
|
||||
// Update second subtask
|
||||
const result = await helpers.taskMaster(
|
||||
'update-subtask',
|
||||
[subtaskId2, 'Second subtask updated'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
|
||||
// Verify both updates
|
||||
const showResult = await helpers.taskMaster('show', [parentTaskId], { cwd: testDir });
|
||||
const showResult = await helpers.taskMaster('show', [parentTaskId], {
|
||||
cwd: testDir
|
||||
});
|
||||
expect(showResult.stdout).toContain('First subtask updated');
|
||||
expect(showResult.stdout).toContain('Second subtask updated');
|
||||
});
|
||||
@@ -205,11 +235,13 @@ describe('update-subtask command', () => {
|
||||
[subtaskId, '--priority', 'high'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
|
||||
// Verify priority was set
|
||||
const showResult = await helpers.taskMaster('show', [parentTaskId], { cwd: testDir });
|
||||
const showResult = await helpers.taskMaster('show', [parentTaskId], {
|
||||
cwd: testDir
|
||||
});
|
||||
expect(showResult.stdout.toLowerCase()).toContain('high');
|
||||
});
|
||||
|
||||
@@ -219,11 +251,13 @@ describe('update-subtask command', () => {
|
||||
[subtaskId, '--estimated-time', '2h'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
|
||||
// Verify estimated time was set
|
||||
const showResult = await helpers.taskMaster('show', [parentTaskId], { cwd: testDir });
|
||||
const showResult = await helpers.taskMaster('show', [parentTaskId], {
|
||||
cwd: testDir
|
||||
});
|
||||
expect(showResult.stdout).toContain('2h');
|
||||
});
|
||||
|
||||
@@ -233,11 +267,13 @@ describe('update-subtask command', () => {
|
||||
[subtaskId, '--assignee', 'john.doe@example.com'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
|
||||
// Verify assignee was set
|
||||
const showResult = await helpers.taskMaster('show', [parentTaskId], { cwd: testDir });
|
||||
const showResult = await helpers.taskMaster('show', [parentTaskId], {
|
||||
cwd: testDir
|
||||
});
|
||||
expect(showResult.stdout).toContain('john.doe');
|
||||
});
|
||||
});
|
||||
@@ -249,15 +285,18 @@ describe('update-subtask command', () => {
|
||||
[
|
||||
subtaskId,
|
||||
'New comprehensive title',
|
||||
'--notes', 'Additional implementation details'
|
||||
'--notes',
|
||||
'Additional implementation details'
|
||||
],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
|
||||
// Verify both updates
|
||||
const showResult = await helpers.taskMaster('show', [parentTaskId], { cwd: testDir });
|
||||
const showResult = await helpers.taskMaster('show', [parentTaskId], {
|
||||
cwd: testDir
|
||||
});
|
||||
expect(showResult.stdout).toContain('New comprehensive title');
|
||||
expect(showResult.stdout).toContain('Additional implementation details');
|
||||
});
|
||||
@@ -267,18 +306,23 @@ describe('update-subtask command', () => {
|
||||
'update-subtask',
|
||||
[
|
||||
subtaskId,
|
||||
'--status', 'in_progress',
|
||||
'--prompt', 'Add acceptance criteria'
|
||||
'--status',
|
||||
'in_progress',
|
||||
'--prompt',
|
||||
'Add acceptance criteria'
|
||||
],
|
||||
{ cwd: testDir, timeout: 45000 }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
|
||||
// Verify both manual and AI updates
|
||||
const showResult = await helpers.taskMaster('show', [parentTaskId], { cwd: testDir });
|
||||
const showResult = await helpers.taskMaster('show', [parentTaskId], {
|
||||
cwd: testDir
|
||||
});
|
||||
expect(showResult.stdout.toLowerCase()).toContain('in_progress');
|
||||
const hasAcceptanceCriteria = showResult.stdout.toLowerCase().includes('acceptance') ||
|
||||
const hasAcceptanceCriteria =
|
||||
showResult.stdout.toLowerCase().includes('acceptance') ||
|
||||
showResult.stdout.toLowerCase().includes('criteria');
|
||||
expect(hasAcceptanceCriteria).toBe(true);
|
||||
}, 60000);
|
||||
@@ -292,18 +336,20 @@ describe('update-subtask command', () => {
|
||||
[subtaskId, '--notes', 'Initial notes.'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
|
||||
// Then append
|
||||
const result = await helpers.taskMaster(
|
||||
'update-subtask',
|
||||
[subtaskId, '--append-notes', '\nAdditional considerations.'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
|
||||
// Verify notes were appended
|
||||
const showResult = await helpers.taskMaster('show', [parentTaskId], { cwd: testDir });
|
||||
const showResult = await helpers.taskMaster('show', [parentTaskId], {
|
||||
cwd: testDir
|
||||
});
|
||||
expect(showResult.stdout).toContain('Initial notes');
|
||||
expect(showResult.stdout).toContain('Additional considerations');
|
||||
});
|
||||
@@ -319,18 +365,20 @@ describe('update-subtask command', () => {
|
||||
);
|
||||
const match = nestedResult.stdout.match(/subtask #?(\d+\.\d+\.\d+)/i);
|
||||
const nestedId = match ? match[1] : '1.1.1';
|
||||
|
||||
|
||||
// Update nested subtask
|
||||
const result = await helpers.taskMaster(
|
||||
'update-subtask',
|
||||
[nestedId, 'Updated nested subtask'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
|
||||
// Verify update
|
||||
const showResult = await helpers.taskMaster('show', [parentTaskId], { cwd: testDir });
|
||||
const showResult = await helpers.taskMaster('show', [parentTaskId], {
|
||||
cwd: testDir
|
||||
});
|
||||
expect(showResult.stdout).toContain('Updated nested subtask');
|
||||
});
|
||||
});
|
||||
@@ -339,7 +387,7 @@ describe('update-subtask command', () => {
|
||||
it('should update subtask in specific tag', async () => {
|
||||
// Create a tag and add task to it
|
||||
await helpers.taskMaster('add-tag', ['feature-y'], { cwd: testDir });
|
||||
|
||||
|
||||
// Create task in tag
|
||||
const tagTaskResult = await helpers.taskMaster(
|
||||
'add-task',
|
||||
@@ -347,7 +395,7 @@ describe('update-subtask command', () => {
|
||||
{ cwd: testDir }
|
||||
);
|
||||
const tagTaskId = helpers.extractTaskId(tagTaskResult.stdout);
|
||||
|
||||
|
||||
// Add subtask to tagged task
|
||||
const tagSubtaskResult = await helpers.taskMaster(
|
||||
'add-subtask',
|
||||
@@ -356,16 +404,16 @@ describe('update-subtask command', () => {
|
||||
);
|
||||
const match = tagSubtaskResult.stdout.match(/subtask #?(\d+\.\d+)/i);
|
||||
const tagSubtaskId = match ? match[1] : '1.1';
|
||||
|
||||
|
||||
// Update subtask in specific tag
|
||||
const result = await helpers.taskMaster(
|
||||
'update-subtask',
|
||||
[tagSubtaskId, 'Updated in feature tag', '--tag', 'feature-y'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
|
||||
// Verify update in correct tag
|
||||
const showResult = await helpers.taskMaster(
|
||||
'show',
|
||||
@@ -383,9 +431,9 @@ describe('update-subtask command', () => {
|
||||
[subtaskId, 'JSON test update', '--output', 'json'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
|
||||
// Output should be valid JSON
|
||||
const jsonOutput = JSON.parse(result.stdout);
|
||||
expect(jsonOutput.success).toBe(true);
|
||||
@@ -402,7 +450,7 @@ describe('update-subtask command', () => {
|
||||
['99.99', 'This should fail'],
|
||||
{ cwd: testDir, allowFailure: true }
|
||||
);
|
||||
|
||||
|
||||
expect(result.exitCode).not.toBe(0);
|
||||
expect(result.stderr).toContain('not found');
|
||||
});
|
||||
@@ -413,7 +461,7 @@ describe('update-subtask command', () => {
|
||||
['invalid-id', 'This should fail'],
|
||||
{ cwd: testDir, allowFailure: true }
|
||||
);
|
||||
|
||||
|
||||
expect(result.exitCode).not.toBe(0);
|
||||
expect(result.stderr).toContain('Invalid subtask ID');
|
||||
});
|
||||
@@ -424,7 +472,7 @@ describe('update-subtask command', () => {
|
||||
[subtaskId, '--priority', 'invalid-priority'],
|
||||
{ cwd: testDir, allowFailure: true }
|
||||
);
|
||||
|
||||
|
||||
expect(result.exitCode).not.toBe(0);
|
||||
expect(result.stderr).toContain('Invalid priority');
|
||||
});
|
||||
@@ -435,7 +483,7 @@ describe('update-subtask command', () => {
|
||||
[subtaskId, '--status', 'invalid-status'],
|
||||
{ cwd: testDir, allowFailure: true }
|
||||
);
|
||||
|
||||
|
||||
expect(result.exitCode).not.toBe(0);
|
||||
expect(result.stderr).toContain('Invalid status');
|
||||
});
|
||||
@@ -444,53 +492,60 @@ describe('update-subtask command', () => {
|
||||
describe('Performance and edge cases', () => {
|
||||
it('should handle very long subtask titles', async () => {
|
||||
const longTitle = 'This is a very detailed subtask title. '.repeat(10);
|
||||
|
||||
|
||||
const result = await helpers.taskMaster(
|
||||
'update-subtask',
|
||||
[subtaskId, longTitle],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
|
||||
// Verify long title was saved
|
||||
const tasksPath = join(testDir, '.taskmaster/tasks/tasks.json');
|
||||
const tasks = JSON.parse(readFileSync(tasksPath, 'utf8'));
|
||||
const parentTask = tasks.master.tasks.find(t => t.id === parseInt(parentTaskId));
|
||||
const subtask = parentTask.subtasks.find(s => s.id === subtaskId);
|
||||
const parentTask = tasks.master.tasks.find(
|
||||
(t) => t.id === parseInt(parentTaskId)
|
||||
);
|
||||
const subtask = parentTask.subtasks.find((s) => s.id === subtaskId);
|
||||
expect(subtask.title).toBe(longTitle);
|
||||
});
|
||||
|
||||
it('should update subtask without affecting parent task', async () => {
|
||||
const originalParentTitle = 'Parent task';
|
||||
|
||||
|
||||
// Update subtask
|
||||
const result = await helpers.taskMaster(
|
||||
'update-subtask',
|
||||
[subtaskId, 'Completely different subtask'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
|
||||
// Verify parent task remains unchanged
|
||||
const showResult = await helpers.taskMaster('show', [parentTaskId], { cwd: testDir });
|
||||
const showResult = await helpers.taskMaster('show', [parentTaskId], {
|
||||
cwd: testDir
|
||||
});
|
||||
expect(showResult.stdout).toContain(originalParentTitle);
|
||||
});
|
||||
|
||||
it('should handle subtask updates with special characters', async () => {
|
||||
const specialTitle = 'Subtask with special chars: @#$% & "quotes" \'apostrophes\'';
|
||||
|
||||
const specialTitle =
|
||||
'Subtask with special chars: @#$% & "quotes" \'apostrophes\'';
|
||||
|
||||
const result = await helpers.taskMaster(
|
||||
'update-subtask',
|
||||
[subtaskId, specialTitle],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
|
||||
// Verify special characters were preserved
|
||||
const showResult = await helpers.taskMaster('show', [parentTaskId], { cwd: testDir });
|
||||
const showResult = await helpers.taskMaster('show', [parentTaskId], {
|
||||
cwd: testDir
|
||||
});
|
||||
expect(showResult.stdout).toContain('@#$%');
|
||||
});
|
||||
});
|
||||
@@ -502,13 +557,15 @@ describe('update-subtask command', () => {
|
||||
[subtaskId, 'Dry run test', '--dry-run'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('DRY RUN');
|
||||
expect(result.stdout).toContain('Would update');
|
||||
|
||||
|
||||
// Verify subtask was NOT actually updated
|
||||
const showResult = await helpers.taskMaster('show', [parentTaskId], { cwd: testDir });
|
||||
const showResult = await helpers.taskMaster('show', [parentTaskId], {
|
||||
cwd: testDir
|
||||
});
|
||||
expect(showResult.stdout).not.toContain('Dry run test');
|
||||
expect(showResult.stdout).toContain('Initial subtask');
|
||||
});
|
||||
@@ -522,44 +579,47 @@ describe('update-subtask command', () => {
|
||||
[subtaskId, '--prompt', 'Add detailed implementation steps'],
|
||||
{ cwd: testDir, timeout: 45000 }
|
||||
);
|
||||
|
||||
|
||||
// Expand parent task
|
||||
const expandResult = await helpers.taskMaster(
|
||||
'expand',
|
||||
['--id', parentTaskId],
|
||||
{ cwd: testDir, timeout: 45000 }
|
||||
);
|
||||
|
||||
|
||||
expect(expandResult).toHaveExitCode(0);
|
||||
expect(expandResult.stdout).toContain('Expanded task');
|
||||
|
||||
|
||||
// Should include updated subtask information
|
||||
const showResult = await helpers.taskMaster('show', [parentTaskId], { cwd: testDir });
|
||||
const hasImplementationSteps = showResult.stdout.toLowerCase().includes('implementation') ||
|
||||
const showResult = await helpers.taskMaster('show', [parentTaskId], {
|
||||
cwd: testDir
|
||||
});
|
||||
const hasImplementationSteps =
|
||||
showResult.stdout.toLowerCase().includes('implementation') ||
|
||||
showResult.stdout.toLowerCase().includes('step');
|
||||
expect(hasImplementationSteps).toBe(true);
|
||||
}, 90000);
|
||||
|
||||
it('should update subtask after parent task status change', async () => {
|
||||
// Change parent task status
|
||||
await helpers.taskMaster(
|
||||
'set-status',
|
||||
[parentTaskId, 'in_progress'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
await helpers.taskMaster('set-status', [parentTaskId, 'in_progress'], {
|
||||
cwd: testDir
|
||||
});
|
||||
|
||||
// Update subtask
|
||||
const result = await helpers.taskMaster(
|
||||
'update-subtask',
|
||||
[subtaskId, '--status', 'in_progress'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
|
||||
// Verify both statuses
|
||||
const showResult = await helpers.taskMaster('show', [parentTaskId], { cwd: testDir });
|
||||
const showResult = await helpers.taskMaster('show', [parentTaskId], {
|
||||
cwd: testDir
|
||||
});
|
||||
expect(showResult.stdout.toLowerCase()).toContain('in_progress');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,7 +3,14 @@
|
||||
* Tests all aspects of single task updates including AI-powered updates
|
||||
*/
|
||||
|
||||
const { mkdtempSync, existsSync, readFileSync, rmSync, writeFileSync, mkdirSync } = require('fs');
|
||||
const {
|
||||
mkdtempSync,
|
||||
existsSync,
|
||||
readFileSync,
|
||||
rmSync,
|
||||
writeFileSync,
|
||||
mkdirSync
|
||||
} = require('fs');
|
||||
const { join } = require('path');
|
||||
const { tmpdir } = require('os');
|
||||
|
||||
@@ -15,11 +22,11 @@ describe('update-task command', () => {
|
||||
beforeEach(async () => {
|
||||
// Create test directory
|
||||
testDir = mkdtempSync(join(tmpdir(), 'task-master-update-task-'));
|
||||
|
||||
|
||||
// Initialize test helpers
|
||||
const context = global.createTestContext('update-task');
|
||||
helpers = context.helpers;
|
||||
|
||||
|
||||
// Copy .env file if it exists
|
||||
const mainEnvPath = join(__dirname, '../../../../.env');
|
||||
const testEnvPath = join(testDir, '.env');
|
||||
@@ -27,11 +34,13 @@ describe('update-task command', () => {
|
||||
const envContent = readFileSync(mainEnvPath, 'utf8');
|
||||
writeFileSync(testEnvPath, envContent);
|
||||
}
|
||||
|
||||
|
||||
// Initialize task-master project
|
||||
const initResult = await helpers.taskMaster('init', ['-y'], { cwd: testDir });
|
||||
const initResult = await helpers.taskMaster('init', ['-y'], {
|
||||
cwd: testDir
|
||||
});
|
||||
expect(initResult).toHaveExitCode(0);
|
||||
|
||||
|
||||
// Create a test task for updates
|
||||
const addResult = await helpers.taskMaster(
|
||||
'add-task',
|
||||
@@ -55,12 +64,14 @@ describe('update-task command', () => {
|
||||
[taskId, '--description', 'Updated task description with more details'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('Updated task');
|
||||
|
||||
|
||||
// Verify update
|
||||
const showResult = await helpers.taskMaster('show', [taskId], { cwd: testDir });
|
||||
const showResult = await helpers.taskMaster('show', [taskId], {
|
||||
cwd: testDir
|
||||
});
|
||||
expect(showResult.stdout).toContain('Updated task description');
|
||||
});
|
||||
|
||||
@@ -70,11 +81,13 @@ describe('update-task command', () => {
|
||||
[taskId, '--title', 'Completely new title'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
|
||||
// Verify update
|
||||
const showResult = await helpers.taskMaster('show', [taskId], { cwd: testDir });
|
||||
const showResult = await helpers.taskMaster('show', [taskId], {
|
||||
cwd: testDir
|
||||
});
|
||||
expect(showResult.stdout).toContain('Completely new title');
|
||||
});
|
||||
|
||||
@@ -84,11 +97,13 @@ describe('update-task command', () => {
|
||||
[taskId, '--priority', 'high'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
|
||||
// Verify update
|
||||
const showResult = await helpers.taskMaster('show', [taskId], { cwd: testDir });
|
||||
const showResult = await helpers.taskMaster('show', [taskId], {
|
||||
cwd: testDir
|
||||
});
|
||||
expect(showResult.stdout.toLowerCase()).toContain('high');
|
||||
});
|
||||
|
||||
@@ -98,11 +113,13 @@ describe('update-task command', () => {
|
||||
[taskId, '--details', 'Implementation notes: Use async/await pattern'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
|
||||
// Verify update
|
||||
const showResult = await helpers.taskMaster('show', [taskId], { cwd: testDir });
|
||||
const showResult = await helpers.taskMaster('show', [taskId], {
|
||||
cwd: testDir
|
||||
});
|
||||
expect(showResult.stdout).toContain('async/await');
|
||||
});
|
||||
});
|
||||
@@ -114,13 +131,16 @@ describe('update-task command', () => {
|
||||
[taskId, '--prompt', 'Add security considerations and best practices'],
|
||||
{ cwd: testDir, timeout: 45000 }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('Updated task');
|
||||
|
||||
|
||||
// Verify AI added security content
|
||||
const showResult = await helpers.taskMaster('show', [taskId], { cwd: testDir });
|
||||
const hasSecurityInfo = showResult.stdout.toLowerCase().includes('security') ||
|
||||
const showResult = await helpers.taskMaster('show', [taskId], {
|
||||
cwd: testDir
|
||||
});
|
||||
const hasSecurityInfo =
|
||||
showResult.stdout.toLowerCase().includes('security') ||
|
||||
showResult.stdout.toLowerCase().includes('practice');
|
||||
expect(hasSecurityInfo).toBe(true);
|
||||
}, 60000);
|
||||
@@ -128,17 +148,23 @@ describe('update-task command', () => {
|
||||
it('should enhance task with AI suggestions', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'update-task',
|
||||
[taskId, '--prompt', 'Break this down into subtasks and add implementation details'],
|
||||
[
|
||||
taskId,
|
||||
'--prompt',
|
||||
'Break this down into subtasks and add implementation details'
|
||||
],
|
||||
{ cwd: testDir, timeout: 45000 }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
|
||||
// Check that task was enhanced
|
||||
const tasksPath = join(testDir, '.taskmaster/tasks/tasks.json');
|
||||
const tasks = JSON.parse(readFileSync(tasksPath, 'utf8'));
|
||||
const updatedTask = tasks.master.tasks.find(t => t.id === parseInt(taskId));
|
||||
|
||||
const updatedTask = tasks.master.tasks.find(
|
||||
(t) => t.id === parseInt(taskId)
|
||||
);
|
||||
|
||||
// Should have more detailed content
|
||||
expect(updatedTask.details.length).toBeGreaterThan(50);
|
||||
}, 60000);
|
||||
@@ -147,17 +173,20 @@ describe('update-task command', () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'update-task',
|
||||
[
|
||||
taskId,
|
||||
'--prompt', 'Add current industry best practices for authentication',
|
||||
taskId,
|
||||
'--prompt',
|
||||
'Add current industry best practices for authentication',
|
||||
'--research'
|
||||
],
|
||||
{ cwd: testDir, timeout: 90000 }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
|
||||
// Research mode should add comprehensive content
|
||||
const showResult = await helpers.taskMaster('show', [taskId], { cwd: testDir });
|
||||
const showResult = await helpers.taskMaster('show', [taskId], {
|
||||
cwd: testDir
|
||||
});
|
||||
expect(showResult.stdout.length).toBeGreaterThan(500);
|
||||
}, 120000);
|
||||
});
|
||||
@@ -168,18 +197,24 @@ describe('update-task command', () => {
|
||||
'update-task',
|
||||
[
|
||||
taskId,
|
||||
'--title', 'New comprehensive title',
|
||||
'--description', 'New detailed description',
|
||||
'--priority', 'high',
|
||||
'--details', 'Additional implementation notes'
|
||||
'--title',
|
||||
'New comprehensive title',
|
||||
'--description',
|
||||
'New detailed description',
|
||||
'--priority',
|
||||
'high',
|
||||
'--details',
|
||||
'Additional implementation notes'
|
||||
],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
|
||||
// Verify all updates
|
||||
const showResult = await helpers.taskMaster('show', [taskId], { cwd: testDir });
|
||||
const showResult = await helpers.taskMaster('show', [taskId], {
|
||||
cwd: testDir
|
||||
});
|
||||
expect(showResult.stdout).toContain('New comprehensive title');
|
||||
expect(showResult.stdout).toContain('New detailed description');
|
||||
expect(showResult.stdout.toLowerCase()).toContain('high');
|
||||
@@ -191,18 +226,23 @@ describe('update-task command', () => {
|
||||
'update-task',
|
||||
[
|
||||
taskId,
|
||||
'--priority', 'high',
|
||||
'--prompt', 'Add technical requirements and dependencies'
|
||||
'--priority',
|
||||
'high',
|
||||
'--prompt',
|
||||
'Add technical requirements and dependencies'
|
||||
],
|
||||
{ cwd: testDir, timeout: 45000 }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
|
||||
// Verify both manual and AI updates
|
||||
const showResult = await helpers.taskMaster('show', [taskId], { cwd: testDir });
|
||||
const showResult = await helpers.taskMaster('show', [taskId], {
|
||||
cwd: testDir
|
||||
});
|
||||
expect(showResult.stdout.toLowerCase()).toContain('high');
|
||||
const hasTechnicalInfo = showResult.stdout.toLowerCase().includes('requirement') ||
|
||||
const hasTechnicalInfo =
|
||||
showResult.stdout.toLowerCase().includes('requirement') ||
|
||||
showResult.stdout.toLowerCase().includes('dependenc');
|
||||
expect(hasTechnicalInfo).toBe(true);
|
||||
}, 60000);
|
||||
@@ -215,11 +255,13 @@ describe('update-task command', () => {
|
||||
[taskId, '--add-tags', 'backend,api,urgent'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
|
||||
// Verify tags were added
|
||||
const showResult = await helpers.taskMaster('show', [taskId], { cwd: testDir });
|
||||
const showResult = await helpers.taskMaster('show', [taskId], {
|
||||
cwd: testDir
|
||||
});
|
||||
expect(showResult.stdout).toContain('backend');
|
||||
expect(showResult.stdout).toContain('api');
|
||||
expect(showResult.stdout).toContain('urgent');
|
||||
@@ -232,18 +274,20 @@ describe('update-task command', () => {
|
||||
[taskId, '--add-tags', 'frontend,ui,design'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
|
||||
// Then remove some
|
||||
const result = await helpers.taskMaster(
|
||||
'update-task',
|
||||
[taskId, '--remove-tags', 'ui,design'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
|
||||
// Verify tags were removed
|
||||
const showResult = await helpers.taskMaster('show', [taskId], { cwd: testDir });
|
||||
const showResult = await helpers.taskMaster('show', [taskId], {
|
||||
cwd: testDir
|
||||
});
|
||||
expect(showResult.stdout).toContain('frontend');
|
||||
expect(showResult.stdout).not.toContain('ui');
|
||||
expect(showResult.stdout).not.toContain('design');
|
||||
@@ -253,17 +297,19 @@ describe('update-task command', () => {
|
||||
const futureDate = new Date();
|
||||
futureDate.setDate(futureDate.getDate() + 7);
|
||||
const dateStr = futureDate.toISOString().split('T')[0];
|
||||
|
||||
|
||||
const result = await helpers.taskMaster(
|
||||
'update-task',
|
||||
[taskId, '--due-date', dateStr],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
|
||||
// Verify due date was set
|
||||
const showResult = await helpers.taskMaster('show', [taskId], { cwd: testDir });
|
||||
const showResult = await helpers.taskMaster('show', [taskId], {
|
||||
cwd: testDir
|
||||
});
|
||||
expect(showResult.stdout).toContain(dateStr);
|
||||
});
|
||||
|
||||
@@ -273,11 +319,13 @@ describe('update-task command', () => {
|
||||
[taskId, '--estimated-time', '4h'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
|
||||
// Verify estimated time was set
|
||||
const showResult = await helpers.taskMaster('show', [taskId], { cwd: testDir });
|
||||
const showResult = await helpers.taskMaster('show', [taskId], {
|
||||
cwd: testDir
|
||||
});
|
||||
expect(showResult.stdout).toContain('4h');
|
||||
});
|
||||
});
|
||||
@@ -289,11 +337,13 @@ describe('update-task command', () => {
|
||||
[taskId, '--status', 'in_progress'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
|
||||
// Verify status change
|
||||
const showResult = await helpers.taskMaster('show', [taskId], { cwd: testDir });
|
||||
const showResult = await helpers.taskMaster('show', [taskId], {
|
||||
cwd: testDir
|
||||
});
|
||||
expect(showResult.stdout.toLowerCase()).toContain('in_progress');
|
||||
});
|
||||
|
||||
@@ -303,25 +353,35 @@ describe('update-task command', () => {
|
||||
[taskId, '--status', 'completed'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
|
||||
// Verify completion
|
||||
const showResult = await helpers.taskMaster('show', [taskId], { cwd: testDir });
|
||||
const showResult = await helpers.taskMaster('show', [taskId], {
|
||||
cwd: testDir
|
||||
});
|
||||
expect(showResult.stdout.toLowerCase()).toContain('completed');
|
||||
});
|
||||
|
||||
it('should mark task as blocked with reason', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'update-task',
|
||||
[taskId, '--status', 'blocked', '--blocked-reason', 'Waiting for API access'],
|
||||
[
|
||||
taskId,
|
||||
'--status',
|
||||
'blocked',
|
||||
'--blocked-reason',
|
||||
'Waiting for API access'
|
||||
],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
|
||||
// Verify blocked status and reason
|
||||
const showResult = await helpers.taskMaster('show', [taskId], { cwd: testDir });
|
||||
const showResult = await helpers.taskMaster('show', [taskId], {
|
||||
cwd: testDir
|
||||
});
|
||||
expect(showResult.stdout.toLowerCase()).toContain('blocked');
|
||||
expect(showResult.stdout).toContain('Waiting for API access');
|
||||
});
|
||||
@@ -334,11 +394,13 @@ describe('update-task command', () => {
|
||||
[taskId, '--append-description', '\nAdditional requirements added.'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
|
||||
// Verify description was appended
|
||||
const showResult = await helpers.taskMaster('show', [taskId], { cwd: testDir });
|
||||
const showResult = await helpers.taskMaster('show', [taskId], {
|
||||
cwd: testDir
|
||||
});
|
||||
expect(showResult.stdout).toContain('Task to be updated');
|
||||
expect(showResult.stdout).toContain('Additional requirements added');
|
||||
});
|
||||
@@ -350,18 +412,20 @@ describe('update-task command', () => {
|
||||
[taskId, '--details', 'Initial implementation notes.'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
|
||||
// Then append
|
||||
const result = await helpers.taskMaster(
|
||||
'update-task',
|
||||
[taskId, '--append-details', '\nPerformance considerations added.'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
|
||||
// Verify details were appended
|
||||
const showResult = await helpers.taskMaster('show', [taskId], { cwd: testDir });
|
||||
const showResult = await helpers.taskMaster('show', [taskId], {
|
||||
cwd: testDir
|
||||
});
|
||||
expect(showResult.stdout).toContain('Initial implementation notes');
|
||||
expect(showResult.stdout).toContain('Performance considerations added');
|
||||
});
|
||||
@@ -376,20 +440,30 @@ describe('update-task command', () => {
|
||||
['--prompt', 'Task in feature-x', '--tag', 'feature-x'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
|
||||
// Get task ID from feature-x tag
|
||||
const listResult = await helpers.taskMaster('list', ['--tag', 'feature-x'], { cwd: testDir });
|
||||
const listResult = await helpers.taskMaster(
|
||||
'list',
|
||||
['--tag', 'feature-x'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
const featureTaskId = helpers.extractTaskId(listResult.stdout);
|
||||
|
||||
|
||||
// Update task in specific tag
|
||||
const result = await helpers.taskMaster(
|
||||
'update-task',
|
||||
[featureTaskId, '--description', 'Updated in feature tag', '--tag', 'feature-x'],
|
||||
[
|
||||
featureTaskId,
|
||||
'--description',
|
||||
'Updated in feature tag',
|
||||
'--tag',
|
||||
'feature-x'
|
||||
],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
|
||||
// Verify update in correct tag
|
||||
const showResult = await helpers.taskMaster(
|
||||
'show',
|
||||
@@ -407,9 +481,9 @@ describe('update-task command', () => {
|
||||
[taskId, '--description', 'JSON test update', '--output', 'json'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
|
||||
// Output should be valid JSON
|
||||
const jsonOutput = JSON.parse(result.stdout);
|
||||
expect(jsonOutput.success).toBe(true);
|
||||
@@ -425,7 +499,7 @@ describe('update-task command', () => {
|
||||
['99999', '--description', 'This should fail'],
|
||||
{ cwd: testDir, allowFailure: true }
|
||||
);
|
||||
|
||||
|
||||
expect(result.exitCode).not.toBe(0);
|
||||
expect(result.stderr).toContain('not found');
|
||||
});
|
||||
@@ -436,7 +510,7 @@ describe('update-task command', () => {
|
||||
[taskId, '--priority', 'invalid-priority'],
|
||||
{ cwd: testDir, allowFailure: true }
|
||||
);
|
||||
|
||||
|
||||
expect(result.exitCode).not.toBe(0);
|
||||
expect(result.stderr).toContain('Invalid priority');
|
||||
});
|
||||
@@ -447,18 +521,17 @@ describe('update-task command', () => {
|
||||
[taskId, '--status', 'invalid-status'],
|
||||
{ cwd: testDir, allowFailure: true }
|
||||
);
|
||||
|
||||
|
||||
expect(result.exitCode).not.toBe(0);
|
||||
expect(result.stderr).toContain('Invalid status');
|
||||
});
|
||||
|
||||
it('should fail without any update parameters', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'update-task',
|
||||
[taskId],
|
||||
{ cwd: testDir, allowFailure: true }
|
||||
);
|
||||
|
||||
const result = await helpers.taskMaster('update-task', [taskId], {
|
||||
cwd: testDir,
|
||||
allowFailure: true
|
||||
});
|
||||
|
||||
expect(result.exitCode).not.toBe(0);
|
||||
expect(result.stderr).toContain('No updates specified');
|
||||
});
|
||||
@@ -466,20 +539,24 @@ describe('update-task command', () => {
|
||||
|
||||
describe('Performance and edge cases', () => {
|
||||
it('should handle very long descriptions', async () => {
|
||||
const longDescription = 'This is a very detailed description. '.repeat(50);
|
||||
|
||||
const longDescription = 'This is a very detailed description. '.repeat(
|
||||
50
|
||||
);
|
||||
|
||||
const result = await helpers.taskMaster(
|
||||
'update-task',
|
||||
[taskId, '--description', longDescription],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
|
||||
// Verify long description was saved
|
||||
const tasksPath = join(testDir, '.taskmaster/tasks/tasks.json');
|
||||
const tasks = JSON.parse(readFileSync(tasksPath, 'utf8'));
|
||||
const updatedTask = tasks.master.tasks.find(t => t.id === parseInt(taskId));
|
||||
const updatedTask = tasks.master.tasks.find(
|
||||
(t) => t.id === parseInt(taskId)
|
||||
);
|
||||
expect(updatedTask.description).toBe(longDescription);
|
||||
});
|
||||
|
||||
@@ -491,24 +568,26 @@ describe('update-task command', () => {
|
||||
{ cwd: testDir }
|
||||
);
|
||||
const depId = helpers.extractTaskId(depResult.stdout);
|
||||
|
||||
|
||||
await helpers.taskMaster(
|
||||
'add-dependency',
|
||||
['--id', taskId, '--depends-on', depId],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
|
||||
// Update the task
|
||||
const result = await helpers.taskMaster(
|
||||
'update-task',
|
||||
[taskId, '--description', 'Updated with dependencies intact'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
|
||||
// Verify dependency is preserved
|
||||
const showResult = await helpers.taskMaster('show', [taskId], { cwd: testDir });
|
||||
const showResult = await helpers.taskMaster('show', [taskId], {
|
||||
cwd: testDir
|
||||
});
|
||||
expect(showResult.stdout).toContain(depId);
|
||||
});
|
||||
});
|
||||
@@ -520,13 +599,15 @@ describe('update-task command', () => {
|
||||
[taskId, '--description', 'Dry run test', '--dry-run'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('DRY RUN');
|
||||
expect(result.stdout).toContain('Would update');
|
||||
|
||||
|
||||
// Verify task was NOT actually updated
|
||||
const showResult = await helpers.taskMaster('show', [taskId], { cwd: testDir });
|
||||
const showResult = await helpers.taskMaster('show', [taskId], {
|
||||
cwd: testDir
|
||||
});
|
||||
expect(showResult.stdout).not.toContain('Dry run test');
|
||||
});
|
||||
});
|
||||
@@ -539,16 +620,16 @@ describe('update-task command', () => {
|
||||
[taskId, '--prompt', 'Add implementation steps'],
|
||||
{ cwd: testDir, timeout: 45000 }
|
||||
);
|
||||
|
||||
|
||||
// Then expand it
|
||||
const expandResult = await helpers.taskMaster(
|
||||
'expand',
|
||||
['--id', taskId],
|
||||
{ cwd: testDir, timeout: 45000 }
|
||||
);
|
||||
|
||||
|
||||
expect(expandResult).toHaveExitCode(0);
|
||||
expect(expandResult.stdout).toContain('Expanded task');
|
||||
}, 90000);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,7 +3,14 @@
|
||||
* Tests all aspects of bulk task updates including AI-powered updates
|
||||
*/
|
||||
|
||||
const { mkdtempSync, existsSync, readFileSync, rmSync, writeFileSync, mkdirSync } = require('fs');
|
||||
const {
|
||||
mkdtempSync,
|
||||
existsSync,
|
||||
readFileSync,
|
||||
rmSync,
|
||||
writeFileSync,
|
||||
mkdirSync
|
||||
} = require('fs');
|
||||
const { join } = require('path');
|
||||
const { tmpdir } = require('os');
|
||||
|
||||
@@ -14,11 +21,11 @@ describe('update-tasks command', () => {
|
||||
beforeEach(async () => {
|
||||
// Create test directory
|
||||
testDir = mkdtempSync(join(tmpdir(), 'task-master-update-tasks-'));
|
||||
|
||||
|
||||
// Initialize test helpers
|
||||
const context = global.createTestContext('update-tasks');
|
||||
helpers = context.helpers;
|
||||
|
||||
|
||||
// Copy .env file if it exists
|
||||
const mainEnvPath = join(__dirname, '../../../../.env');
|
||||
const testEnvPath = join(testDir, '.env');
|
||||
@@ -26,11 +33,13 @@ describe('update-tasks command', () => {
|
||||
const envContent = readFileSync(mainEnvPath, 'utf8');
|
||||
writeFileSync(testEnvPath, envContent);
|
||||
}
|
||||
|
||||
|
||||
// Initialize task-master project
|
||||
const initResult = await helpers.taskMaster('init', ['-y'], { cwd: testDir });
|
||||
const initResult = await helpers.taskMaster('init', ['-y'], {
|
||||
cwd: testDir
|
||||
});
|
||||
expect(initResult).toHaveExitCode(0);
|
||||
|
||||
|
||||
// Create some test tasks for bulk updates
|
||||
const tasksPath = join(testDir, '.taskmaster/tasks/tasks.json');
|
||||
const tasksData = {
|
||||
@@ -38,27 +47,27 @@ describe('update-tasks command', () => {
|
||||
tasks: [
|
||||
{
|
||||
id: 1,
|
||||
title: "Setup authentication",
|
||||
description: "Implement user authentication",
|
||||
priority: "medium",
|
||||
status: "pending",
|
||||
details: "Basic auth implementation"
|
||||
title: 'Setup authentication',
|
||||
description: 'Implement user authentication',
|
||||
priority: 'medium',
|
||||
status: 'pending',
|
||||
details: 'Basic auth implementation'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "Create database schema",
|
||||
description: "Design database structure",
|
||||
priority: "high",
|
||||
status: "pending",
|
||||
details: "PostgreSQL schema"
|
||||
title: 'Create database schema',
|
||||
description: 'Design database structure',
|
||||
priority: 'high',
|
||||
status: 'pending',
|
||||
details: 'PostgreSQL schema'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: "Build API endpoints",
|
||||
description: "RESTful API development",
|
||||
priority: "medium",
|
||||
status: "in_progress",
|
||||
details: "Express.js endpoints"
|
||||
title: 'Build API endpoints',
|
||||
description: 'RESTful API development',
|
||||
priority: 'medium',
|
||||
status: 'in_progress',
|
||||
details: 'Express.js endpoints'
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -81,18 +90,18 @@ describe('update-tasks command', () => {
|
||||
['--prompt', 'Add security considerations to all tasks'],
|
||||
{ cwd: testDir, timeout: 45000 }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('Updated');
|
||||
expect(result.stdout).toContain('task');
|
||||
|
||||
|
||||
// Verify tasks were updated
|
||||
const tasksPath = join(testDir, '.taskmaster/tasks/tasks.json');
|
||||
const tasks = JSON.parse(readFileSync(tasksPath, 'utf8'));
|
||||
|
||||
|
||||
// Check that tasks have been modified (details should mention security)
|
||||
const hasSecurityUpdates = tasks.master.tasks.some(t =>
|
||||
t.details && t.details.toLowerCase().includes('security')
|
||||
const hasSecurityUpdates = tasks.master.tasks.some(
|
||||
(t) => t.details && t.details.toLowerCase().includes('security')
|
||||
);
|
||||
expect(hasSecurityUpdates).toBe(true);
|
||||
}, 60000);
|
||||
@@ -103,7 +112,7 @@ describe('update-tasks command', () => {
|
||||
['--ids', '1,3', '--prompt', 'Add performance optimization notes'],
|
||||
{ cwd: testDir, timeout: 45000 }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('Updated 2 task');
|
||||
}, 60000);
|
||||
@@ -114,18 +123,24 @@ describe('update-tasks command', () => {
|
||||
['--status', 'pending', '--prompt', 'Add estimated time requirements'],
|
||||
{ cwd: testDir, timeout: 45000 }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
// Should update tasks 1 and 2 (pending status)
|
||||
expect(result.stdout).toContain('Updated 2 task');
|
||||
|
||||
|
||||
const tasksPath = join(testDir, '.taskmaster/tasks/tasks.json');
|
||||
const tasks = JSON.parse(readFileSync(tasksPath, 'utf8'));
|
||||
|
||||
|
||||
// Verify only pending tasks were updated
|
||||
const pendingTasks = tasks.master.tasks.filter(t => t.status === 'pending');
|
||||
const hasTimeEstimates = pendingTasks.some(t =>
|
||||
t.details && (t.details.includes('time') || t.details.includes('hour') || t.details.includes('day'))
|
||||
const pendingTasks = tasks.master.tasks.filter(
|
||||
(t) => t.status === 'pending'
|
||||
);
|
||||
const hasTimeEstimates = pendingTasks.some(
|
||||
(t) =>
|
||||
t.details &&
|
||||
(t.details.includes('time') ||
|
||||
t.details.includes('hour') ||
|
||||
t.details.includes('day'))
|
||||
);
|
||||
expect(hasTimeEstimates).toBe(true);
|
||||
}, 60000);
|
||||
@@ -136,7 +151,7 @@ describe('update-tasks command', () => {
|
||||
['--priority', 'medium', '--prompt', 'Add testing requirements'],
|
||||
{ cwd: testDir, timeout: 45000 }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
// Should update tasks 1 and 3 (medium priority)
|
||||
expect(result.stdout).toContain('Updated 2 task');
|
||||
@@ -147,25 +162,22 @@ describe('update-tasks command', () => {
|
||||
it('should update tasks with research-backed information', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'update-tasks',
|
||||
[
|
||||
'--ids', '1',
|
||||
'--prompt', 'Add OAuth2 best practices',
|
||||
'--research'
|
||||
],
|
||||
['--ids', '1', '--prompt', 'Add OAuth2 best practices', '--research'],
|
||||
{ cwd: testDir, timeout: 90000 }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('Updated');
|
||||
|
||||
|
||||
// Research mode should produce more detailed updates
|
||||
const tasksPath = join(testDir, '.taskmaster/tasks/tasks.json');
|
||||
const tasks = JSON.parse(readFileSync(tasksPath, 'utf8'));
|
||||
const authTask = tasks.master.tasks.find(t => t.id === 1);
|
||||
|
||||
const authTask = tasks.master.tasks.find((t) => t.id === 1);
|
||||
|
||||
// Check for detailed OAuth2 information
|
||||
expect(authTask.details.length).toBeGreaterThan(100);
|
||||
const hasOAuthInfo = authTask.details.toLowerCase().includes('oauth') ||
|
||||
const hasOAuthInfo =
|
||||
authTask.details.toLowerCase().includes('oauth') ||
|
||||
authTask.details.toLowerCase().includes('authorization');
|
||||
expect(hasOAuthInfo).toBe(true);
|
||||
}, 120000);
|
||||
@@ -179,34 +191,37 @@ describe('update-tasks command', () => {
|
||||
currentTasks.master.tasks.push(
|
||||
{
|
||||
id: 4,
|
||||
title: "Security audit",
|
||||
description: "Perform security review",
|
||||
priority: "high",
|
||||
status: "pending",
|
||||
details: "Initial security check"
|
||||
title: 'Security audit',
|
||||
description: 'Perform security review',
|
||||
priority: 'high',
|
||||
status: 'pending',
|
||||
details: 'Initial security check'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
title: "Performance testing",
|
||||
description: "Load testing",
|
||||
priority: "high",
|
||||
status: "in_progress",
|
||||
details: "Using JMeter"
|
||||
title: 'Performance testing',
|
||||
description: 'Load testing',
|
||||
priority: 'high',
|
||||
status: 'in_progress',
|
||||
details: 'Using JMeter'
|
||||
}
|
||||
);
|
||||
writeFileSync(tasksPath, JSON.stringify(currentTasks, null, 2));
|
||||
|
||||
|
||||
// Update only high priority pending tasks
|
||||
const result = await helpers.taskMaster(
|
||||
'update-tasks',
|
||||
[
|
||||
'--status', 'pending',
|
||||
'--priority', 'high',
|
||||
'--prompt', 'Add compliance requirements'
|
||||
'--status',
|
||||
'pending',
|
||||
'--priority',
|
||||
'high',
|
||||
'--prompt',
|
||||
'Add compliance requirements'
|
||||
],
|
||||
{ cwd: testDir, timeout: 45000 }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
// Should only update task 2 and 4
|
||||
expect(result.stdout).toContain('Updated 2 task');
|
||||
@@ -216,12 +231,14 @@ describe('update-tasks command', () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'update-tasks',
|
||||
[
|
||||
'--status', 'completed',
|
||||
'--prompt', 'This should not update anything'
|
||||
'--status',
|
||||
'completed',
|
||||
'--prompt',
|
||||
'This should not update anything'
|
||||
],
|
||||
{ cwd: testDir, timeout: 30000 }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('No tasks found matching the criteria');
|
||||
}, 45000);
|
||||
@@ -231,32 +248,29 @@ describe('update-tasks command', () => {
|
||||
it('should update tasks in specific tag', async () => {
|
||||
// Create a new tag with tasks
|
||||
await helpers.taskMaster('add-tag', ['feature-x'], { cwd: testDir });
|
||||
|
||||
|
||||
// Add task to the tag
|
||||
await helpers.taskMaster(
|
||||
'add-task',
|
||||
['--prompt', 'Feature X implementation', '--tag', 'feature-x'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
|
||||
const result = await helpers.taskMaster(
|
||||
'update-tasks',
|
||||
[
|
||||
'--tag', 'feature-x',
|
||||
'--prompt', 'Add deployment considerations'
|
||||
],
|
||||
['--tag', 'feature-x', '--prompt', 'Add deployment considerations'],
|
||||
{ cwd: testDir, timeout: 45000 }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('Updated');
|
||||
|
||||
|
||||
// Verify task in tag was updated
|
||||
const tasksPath = join(testDir, '.taskmaster/tasks/tasks.json');
|
||||
const tasks = JSON.parse(readFileSync(tasksPath, 'utf8'));
|
||||
const featureXTasks = tasks['feature-x'].tasks;
|
||||
const hasDeploymentInfo = featureXTasks.some(t =>
|
||||
t.details && t.details.toLowerCase().includes('deploy')
|
||||
const hasDeploymentInfo = featureXTasks.some(
|
||||
(t) => t.details && t.details.toLowerCase().includes('deploy')
|
||||
);
|
||||
expect(hasDeploymentInfo).toBe(true);
|
||||
}, 60000);
|
||||
@@ -265,14 +279,14 @@ describe('update-tasks command', () => {
|
||||
// Create multiple tags
|
||||
await helpers.taskMaster('add-tag', ['backend'], { cwd: testDir });
|
||||
await helpers.taskMaster('add-tag', ['frontend'], { cwd: testDir });
|
||||
|
||||
|
||||
// Update all tasks across all tags
|
||||
const result = await helpers.taskMaster(
|
||||
'update-tasks',
|
||||
['--prompt', 'Add error handling strategies'],
|
||||
{ cwd: testDir, timeout: 45000 }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('Updated');
|
||||
}, 60000);
|
||||
@@ -283,15 +297,18 @@ describe('update-tasks command', () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'update-tasks',
|
||||
[
|
||||
'--ids', '1',
|
||||
'--prompt', 'Add monitoring requirements',
|
||||
'--output', 'json'
|
||||
'--ids',
|
||||
'1',
|
||||
'--prompt',
|
||||
'Add monitoring requirements',
|
||||
'--output',
|
||||
'json'
|
||||
],
|
||||
{ cwd: testDir, timeout: 45000 }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
|
||||
// Output should be valid JSON
|
||||
const jsonOutput = JSON.parse(result.stdout);
|
||||
expect(jsonOutput.success).toBe(true);
|
||||
@@ -302,12 +319,11 @@ describe('update-tasks command', () => {
|
||||
|
||||
describe('Error handling', () => {
|
||||
it('should fail without prompt', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'update-tasks',
|
||||
['--ids', '1'],
|
||||
{ cwd: testDir, allowFailure: true }
|
||||
);
|
||||
|
||||
const result = await helpers.taskMaster('update-tasks', ['--ids', '1'], {
|
||||
cwd: testDir,
|
||||
allowFailure: true
|
||||
});
|
||||
|
||||
expect(result.exitCode).not.toBe(0);
|
||||
expect(result.stderr).toContain('prompt');
|
||||
});
|
||||
@@ -318,7 +334,7 @@ describe('update-tasks command', () => {
|
||||
['--ids', '999,1000', '--prompt', 'Update non-existent tasks'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('No tasks found');
|
||||
});
|
||||
@@ -329,7 +345,7 @@ describe('update-tasks command', () => {
|
||||
['--status', 'invalid-status', '--prompt', 'Test invalid status'],
|
||||
{ cwd: testDir, allowFailure: true }
|
||||
);
|
||||
|
||||
|
||||
expect(result.exitCode).not.toBe(0);
|
||||
expect(result.stderr).toContain('Invalid status');
|
||||
});
|
||||
@@ -340,7 +356,7 @@ describe('update-tasks command', () => {
|
||||
['--priority', 'urgent', '--prompt', 'Test invalid priority'],
|
||||
{ cwd: testDir, allowFailure: true }
|
||||
);
|
||||
|
||||
|
||||
expect(result.exitCode).not.toBe(0);
|
||||
expect(result.stderr).toContain('Invalid priority');
|
||||
});
|
||||
@@ -351,7 +367,7 @@ describe('update-tasks command', () => {
|
||||
// Add many tasks
|
||||
const tasksPath = join(testDir, '.taskmaster/tasks/tasks.json');
|
||||
const currentTasks = JSON.parse(readFileSync(tasksPath, 'utf8'));
|
||||
|
||||
|
||||
for (let i = 4; i <= 20; i++) {
|
||||
currentTasks.master.tasks.push({
|
||||
id: i,
|
||||
@@ -363,7 +379,7 @@ describe('update-tasks command', () => {
|
||||
});
|
||||
}
|
||||
writeFileSync(tasksPath, JSON.stringify(currentTasks, null, 2));
|
||||
|
||||
|
||||
const startTime = Date.now();
|
||||
const result = await helpers.taskMaster(
|
||||
'update-tasks',
|
||||
@@ -371,7 +387,7 @@ describe('update-tasks command', () => {
|
||||
{ cwd: testDir, timeout: 120000 }
|
||||
);
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('Updated 20 task');
|
||||
expect(duration).toBeLessThan(120000); // Should complete within 2 minutes
|
||||
@@ -384,15 +400,15 @@ describe('update-tasks command', () => {
|
||||
currentTasks.master.tasks[1].dependencies = [1];
|
||||
currentTasks.master.tasks[2].dependencies = [1, 2];
|
||||
writeFileSync(tasksPath, JSON.stringify(currentTasks, null, 2));
|
||||
|
||||
|
||||
const result = await helpers.taskMaster(
|
||||
'update-tasks',
|
||||
['--prompt', 'Clarify implementation order'],
|
||||
{ cwd: testDir, timeout: 45000 }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
|
||||
// Verify dependencies are preserved
|
||||
const updatedTasks = JSON.parse(readFileSync(tasksPath, 'utf8'));
|
||||
expect(updatedTasks.master.tasks[1].dependencies).toEqual([1]);
|
||||
@@ -405,22 +421,24 @@ describe('update-tasks command', () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'update-tasks',
|
||||
[
|
||||
'--ids', '1,2',
|
||||
'--prompt', 'Add test coverage requirements',
|
||||
'--ids',
|
||||
'1,2',
|
||||
'--prompt',
|
||||
'Add test coverage requirements',
|
||||
'--dry-run'
|
||||
],
|
||||
{ cwd: testDir, timeout: 45000 }
|
||||
);
|
||||
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('DRY RUN');
|
||||
expect(result.stdout).toContain('Would update');
|
||||
|
||||
|
||||
// Verify tasks were NOT actually updated
|
||||
const tasksPath = join(testDir, '.taskmaster/tasks/tasks.json');
|
||||
const tasks = JSON.parse(readFileSync(tasksPath, 'utf8'));
|
||||
const hasTestCoverage = tasks.master.tasks.some(t =>
|
||||
t.details && t.details.toLowerCase().includes('test coverage')
|
||||
const hasTestCoverage = tasks.master.tasks.some(
|
||||
(t) => t.details && t.details.toLowerCase().includes('test coverage')
|
||||
);
|
||||
expect(hasTestCoverage).toBe(false);
|
||||
}, 60000);
|
||||
@@ -434,16 +452,15 @@ describe('update-tasks command', () => {
|
||||
['--ids', '1', '--prompt', 'Add detailed specifications'],
|
||||
{ cwd: testDir, timeout: 45000 }
|
||||
);
|
||||
|
||||
|
||||
// Then expand the updated task
|
||||
const expandResult = await helpers.taskMaster(
|
||||
'expand',
|
||||
['--id', '1'],
|
||||
{ cwd: testDir, timeout: 45000 }
|
||||
);
|
||||
|
||||
const expandResult = await helpers.taskMaster('expand', ['--id', '1'], {
|
||||
cwd: testDir,
|
||||
timeout: 45000
|
||||
});
|
||||
|
||||
expect(expandResult).toHaveExitCode(0);
|
||||
expect(expandResult.stdout).toContain('Expanded task');
|
||||
}, 90000);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
207
tests/e2e/tests/mcp/get-tasks.test.js
Normal file
207
tests/e2e/tests/mcp/get-tasks.test.js
Normal file
@@ -0,0 +1,207 @@
|
||||
import { describe, it, expect, beforeAll, afterAll } from '@jest/globals';
|
||||
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
||||
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
||||
import { dirname, join } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import fs from 'fs';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const projectRoot = join(__dirname, '../../../..');
|
||||
|
||||
describe('MCP Server - get_tasks tool', () => {
|
||||
let client;
|
||||
let transport;
|
||||
|
||||
beforeAll(async () => {
|
||||
// Create transport by spawning the server
|
||||
transport = new StdioClientTransport({
|
||||
command: 'node',
|
||||
args: ['mcp-server/server.js'],
|
||||
env: process.env,
|
||||
cwd: projectRoot
|
||||
});
|
||||
|
||||
// Create client
|
||||
client = new Client(
|
||||
{
|
||||
name: 'test-client',
|
||||
version: '1.0.0'
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
sampling: {}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Connect to server
|
||||
await client.connect(transport);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
if (client) {
|
||||
await client.close();
|
||||
}
|
||||
});
|
||||
|
||||
it('should connect to MCP server successfully', async () => {
|
||||
const tools = await client.listTools();
|
||||
expect(tools.tools).toBeDefined();
|
||||
expect(tools.tools.length).toBeGreaterThan(0);
|
||||
|
||||
const toolNames = tools.tools.map((t) => t.name);
|
||||
expect(toolNames).toContain('get_tasks');
|
||||
expect(toolNames).toContain('initialize_project');
|
||||
});
|
||||
|
||||
it('should initialize project successfully', async () => {
|
||||
const result = await client.callTool({
|
||||
name: 'initialize_project',
|
||||
arguments: {
|
||||
projectRoot: projectRoot
|
||||
}
|
||||
});
|
||||
|
||||
expect(result.content).toBeDefined();
|
||||
expect(result.content[0].type).toBe('text');
|
||||
expect(result.content[0].text).toContain(
|
||||
'Project initialized successfully'
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle missing tasks file gracefully', async () => {
|
||||
const result = await client.callTool({
|
||||
name: 'get_tasks',
|
||||
arguments: {
|
||||
projectRoot: projectRoot,
|
||||
file: '.taskmaster/non-existent-tasks.json'
|
||||
}
|
||||
});
|
||||
|
||||
expect(result.isError).toBe(true);
|
||||
expect(result.content[0].text).toContain('Error');
|
||||
});
|
||||
|
||||
it('should get tasks with fixture data', async () => {
|
||||
// Create a temporary tasks file with proper structure
|
||||
const testTasksPath = join(projectRoot, '.taskmaster/test-tasks.json');
|
||||
const testTasks = {
|
||||
tasks: [
|
||||
{
|
||||
id: 'test-001',
|
||||
description: 'Test task 1',
|
||||
status: 'pending',
|
||||
priority: 'high',
|
||||
estimatedMinutes: 30,
|
||||
actualMinutes: 0,
|
||||
dependencies: [],
|
||||
tags: ['test'],
|
||||
subtasks: [
|
||||
{
|
||||
id: 'test-001-1',
|
||||
description: 'Test subtask 1.1',
|
||||
status: 'pending',
|
||||
priority: 'medium',
|
||||
estimatedMinutes: 15,
|
||||
actualMinutes: 0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'test-002',
|
||||
description: 'Test task 2',
|
||||
status: 'in_progress',
|
||||
priority: 'medium',
|
||||
estimatedMinutes: 60,
|
||||
actualMinutes: 15,
|
||||
dependencies: ['test-001'],
|
||||
tags: ['test', 'demo'],
|
||||
subtasks: []
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
// Write test tasks file
|
||||
fs.writeFileSync(testTasksPath, JSON.stringify(testTasks, null, 2));
|
||||
|
||||
try {
|
||||
const result = await client.callTool({
|
||||
name: 'get_tasks',
|
||||
arguments: {
|
||||
projectRoot: projectRoot,
|
||||
file: '.taskmaster/test-tasks.json',
|
||||
withSubtasks: true
|
||||
}
|
||||
});
|
||||
|
||||
expect(result.isError).toBeFalsy();
|
||||
expect(result.content[0].text).toContain('2 tasks found');
|
||||
expect(result.content[0].text).toContain('Test task 1');
|
||||
expect(result.content[0].text).toContain('Test task 2');
|
||||
expect(result.content[0].text).toContain('Test subtask 1.1');
|
||||
} finally {
|
||||
// Cleanup
|
||||
if (fs.existsSync(testTasksPath)) {
|
||||
fs.unlinkSync(testTasksPath);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('should filter tasks by status', async () => {
|
||||
// Create a temporary tasks file
|
||||
const testTasksPath = join(
|
||||
projectRoot,
|
||||
'.taskmaster/test-status-tasks.json'
|
||||
);
|
||||
const testTasks = {
|
||||
tasks: [
|
||||
{
|
||||
id: 'status-001',
|
||||
description: 'Pending task',
|
||||
status: 'pending',
|
||||
priority: 'high',
|
||||
estimatedMinutes: 30,
|
||||
actualMinutes: 0,
|
||||
dependencies: [],
|
||||
tags: ['test'],
|
||||
subtasks: []
|
||||
},
|
||||
{
|
||||
id: 'status-002',
|
||||
description: 'Done task',
|
||||
status: 'done',
|
||||
priority: 'medium',
|
||||
estimatedMinutes: 60,
|
||||
actualMinutes: 60,
|
||||
dependencies: [],
|
||||
tags: ['test'],
|
||||
subtasks: []
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
fs.writeFileSync(testTasksPath, JSON.stringify(testTasks, null, 2));
|
||||
|
||||
try {
|
||||
// Test filtering by 'done' status
|
||||
const result = await client.callTool({
|
||||
name: 'get_tasks',
|
||||
arguments: {
|
||||
projectRoot: projectRoot,
|
||||
file: '.taskmaster/test-status-tasks.json',
|
||||
status: 'done'
|
||||
}
|
||||
});
|
||||
|
||||
expect(result.isError).toBeFalsy();
|
||||
expect(result.content[0].text).toContain('1 task found');
|
||||
expect(result.content[0].text).toContain('Done task');
|
||||
expect(result.content[0].text).not.toContain('Pending task');
|
||||
} finally {
|
||||
// Cleanup
|
||||
if (fs.existsSync(testTasksPath)) {
|
||||
fs.unlinkSync(testTasksPath);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
146
tests/e2e/tests/mcp/mcp-jest-test.js
Normal file
146
tests/e2e/tests/mcp/mcp-jest-test.js
Normal file
@@ -0,0 +1,146 @@
|
||||
import { mcpTest } from 'mcp-jest';
|
||||
import { join, dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import fs from 'fs';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const projectRoot = join(__dirname, '../../../..');
|
||||
|
||||
// Create test tasks file for testing
|
||||
const testTasksPath = join(projectRoot, '.taskmaster/test-mcp-tasks.json');
|
||||
const testTasks = {
|
||||
tasks: [
|
||||
{
|
||||
id: 'mcp-test-001',
|
||||
description: 'MCP Test task 1',
|
||||
status: 'pending',
|
||||
priority: 'high',
|
||||
estimatedMinutes: 30,
|
||||
actualMinutes: 0,
|
||||
dependencies: [],
|
||||
tags: ['test'],
|
||||
subtasks: [
|
||||
{
|
||||
id: 'mcp-test-001-1',
|
||||
description: 'MCP Test subtask 1.1',
|
||||
status: 'pending',
|
||||
priority: 'medium',
|
||||
estimatedMinutes: 15,
|
||||
actualMinutes: 0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'mcp-test-002',
|
||||
description: 'MCP Test task 2',
|
||||
status: 'done',
|
||||
priority: 'medium',
|
||||
estimatedMinutes: 60,
|
||||
actualMinutes: 60,
|
||||
dependencies: ['mcp-test-001'],
|
||||
tags: ['test', 'demo'],
|
||||
subtasks: []
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
// Setup test data
|
||||
fs.mkdirSync(join(projectRoot, '.taskmaster'), { recursive: true });
|
||||
fs.writeFileSync(testTasksPath, JSON.stringify(testTasks, null, 2));
|
||||
|
||||
// Run MCP Jest tests
|
||||
async function runTests() {
|
||||
try {
|
||||
const results = await mcpTest(
|
||||
{
|
||||
command: 'node',
|
||||
args: [join(projectRoot, 'mcp-server/server.js')],
|
||||
env: process.env
|
||||
},
|
||||
{
|
||||
tools: {
|
||||
initialize_project: {
|
||||
args: { projectRoot: projectRoot },
|
||||
expect: (result) =>
|
||||
result.content[0].text.includes(
|
||||
'Project initialized successfully'
|
||||
)
|
||||
},
|
||||
get_tasks: [
|
||||
{
|
||||
name: 'get all tasks with subtasks',
|
||||
args: {
|
||||
projectRoot: projectRoot,
|
||||
file: '.taskmaster/test-mcp-tasks.json',
|
||||
withSubtasks: true
|
||||
},
|
||||
expect: (result) => {
|
||||
const text = result.content[0].text;
|
||||
return (
|
||||
!result.isError &&
|
||||
text.includes('2 tasks found') &&
|
||||
text.includes('MCP Test task 1') &&
|
||||
text.includes('MCP Test task 2') &&
|
||||
text.includes('MCP Test subtask 1.1')
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'filter by done status',
|
||||
args: {
|
||||
projectRoot: projectRoot,
|
||||
file: '.taskmaster/test-mcp-tasks.json',
|
||||
status: 'done'
|
||||
},
|
||||
expect: (result) => {
|
||||
const text = result.content[0].text;
|
||||
return (
|
||||
!result.isError &&
|
||||
text.includes('1 task found') &&
|
||||
text.includes('MCP Test task 2') &&
|
||||
!text.includes('MCP Test task 1')
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'handle non-existent file',
|
||||
args: {
|
||||
projectRoot: projectRoot,
|
||||
file: '.taskmaster/non-existent.json'
|
||||
},
|
||||
expect: (result) =>
|
||||
result.isError && result.content[0].text.includes('Error')
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
console.log('\nTest Results:');
|
||||
console.log('=============');
|
||||
console.log(`✅ Passed: ${results.passed}/${results.total}`);
|
||||
|
||||
if (results.failed > 0) {
|
||||
console.error(`❌ Failed: ${results.failed}`);
|
||||
console.error('\nDetailed Results:');
|
||||
console.log(JSON.stringify(results, null, 2));
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
if (fs.existsSync(testTasksPath)) {
|
||||
fs.unlinkSync(testTasksPath);
|
||||
}
|
||||
|
||||
// Exit with appropriate code
|
||||
process.exit(results.failed > 0 ? 1 : 0);
|
||||
} catch (error) {
|
||||
console.error('Test execution failed:', error);
|
||||
// Cleanup on error
|
||||
if (fs.existsSync(testTasksPath)) {
|
||||
fs.unlinkSync(testTasksPath);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
runTests();
|
||||
254
tests/e2e/tests/mcp/simple-mcp-test.js
Normal file
254
tests/e2e/tests/mcp/simple-mcp-test.js
Normal file
@@ -0,0 +1,254 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
||||
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
||||
import { join, dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import fs from 'fs';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const projectRoot = join(__dirname, '../../../..');
|
||||
|
||||
// Create test tasks file for testing
|
||||
const testTasksPath = join(projectRoot, '.taskmaster/test-tasks.json');
|
||||
const testTasks = {
|
||||
tasks: [
|
||||
{
|
||||
id: 'test-001',
|
||||
description: 'Test task 1',
|
||||
status: 'pending',
|
||||
priority: 'high',
|
||||
estimatedMinutes: 30,
|
||||
actualMinutes: 0,
|
||||
dependencies: [],
|
||||
tags: ['test'],
|
||||
subtasks: [
|
||||
{
|
||||
id: 'test-001-1',
|
||||
description: 'Test subtask 1.1',
|
||||
status: 'pending',
|
||||
priority: 'medium',
|
||||
estimatedMinutes: 15,
|
||||
actualMinutes: 0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'test-002',
|
||||
description: 'Test task 2',
|
||||
status: 'done',
|
||||
priority: 'medium',
|
||||
estimatedMinutes: 60,
|
||||
actualMinutes: 60,
|
||||
dependencies: ['test-001'],
|
||||
tags: ['test', 'demo'],
|
||||
subtasks: []
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
async function runTests() {
|
||||
console.log('Starting MCP server tests...\n');
|
||||
|
||||
// Setup test data
|
||||
fs.mkdirSync(join(projectRoot, '.taskmaster'), { recursive: true });
|
||||
fs.writeFileSync(testTasksPath, JSON.stringify(testTasks, null, 2));
|
||||
|
||||
// Create transport by spawning the server
|
||||
const transport = new StdioClientTransport({
|
||||
command: 'node',
|
||||
args: ['mcp-server/server.js'],
|
||||
env: process.env,
|
||||
cwd: projectRoot
|
||||
});
|
||||
|
||||
// Create client
|
||||
const client = new Client(
|
||||
{
|
||||
name: 'test-client',
|
||||
version: '1.0.0'
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
sampling: {}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
let testResults = {
|
||||
total: 0,
|
||||
passed: 0,
|
||||
failed: 0,
|
||||
tests: []
|
||||
};
|
||||
|
||||
async function runTest(name, testFn) {
|
||||
testResults.total++;
|
||||
try {
|
||||
await testFn();
|
||||
testResults.passed++;
|
||||
testResults.tests.push({ name, status: 'passed' });
|
||||
console.log(`✅ ${name}`);
|
||||
} catch (error) {
|
||||
testResults.failed++;
|
||||
testResults.tests.push({ name, status: 'failed', error: error.message });
|
||||
console.error(`❌ ${name}`);
|
||||
console.error(` Error: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// Connect to server
|
||||
await client.connect(transport);
|
||||
console.log('Connected to MCP server\n');
|
||||
|
||||
// Test 1: List available tools
|
||||
await runTest('List available tools', async () => {
|
||||
const tools = await client.listTools();
|
||||
if (!tools.tools || tools.tools.length === 0) {
|
||||
throw new Error('No tools found');
|
||||
}
|
||||
const toolNames = tools.tools.map((t) => t.name);
|
||||
if (!toolNames.includes('get_tasks')) {
|
||||
throw new Error('get_tasks tool not found');
|
||||
}
|
||||
console.log(` Found ${tools.tools.length} tools`);
|
||||
});
|
||||
|
||||
// Test 2: Initialize project
|
||||
await runTest('Initialize project', async () => {
|
||||
const result = await client.callTool({
|
||||
name: 'initialize_project',
|
||||
arguments: {
|
||||
projectRoot: projectRoot
|
||||
}
|
||||
});
|
||||
if (
|
||||
!result.content[0].text.includes('Project initialized successfully')
|
||||
) {
|
||||
throw new Error('Project initialization failed');
|
||||
}
|
||||
});
|
||||
|
||||
// Test 3: Get all tasks
|
||||
await runTest('Get all tasks with subtasks', async () => {
|
||||
const result = await client.callTool({
|
||||
name: 'get_tasks',
|
||||
arguments: {
|
||||
projectRoot: projectRoot,
|
||||
file: '.taskmaster/test-tasks.json',
|
||||
withSubtasks: true
|
||||
}
|
||||
});
|
||||
|
||||
if (result.isError) {
|
||||
throw new Error(`Tool returned error: ${result.content[0].text}`);
|
||||
}
|
||||
|
||||
const text = result.content[0].text;
|
||||
const data = JSON.parse(text);
|
||||
|
||||
if (!data.data || !data.data.tasks) {
|
||||
throw new Error('Invalid response format');
|
||||
}
|
||||
|
||||
if (data.data.tasks.length !== 2) {
|
||||
throw new Error(`Expected 2 tasks, got ${data.data.tasks.length}`);
|
||||
}
|
||||
|
||||
const taskDescriptions = data.data.tasks.map((t) => t.description);
|
||||
if (
|
||||
!taskDescriptions.includes('Test task 1') ||
|
||||
!taskDescriptions.includes('Test task 2')
|
||||
) {
|
||||
throw new Error('Expected tasks not found');
|
||||
}
|
||||
|
||||
// Check for subtask
|
||||
const task1 = data.data.tasks.find((t) => t.id === 'test-001');
|
||||
if (!task1.subtasks || task1.subtasks.length === 0) {
|
||||
throw new Error('Subtasks not found');
|
||||
}
|
||||
if (task1.subtasks[0].description !== 'Test subtask 1.1') {
|
||||
throw new Error('Expected subtask not found');
|
||||
}
|
||||
});
|
||||
|
||||
// Test 4: Filter by status
|
||||
await runTest('Filter tasks by done status', async () => {
|
||||
const result = await client.callTool({
|
||||
name: 'get_tasks',
|
||||
arguments: {
|
||||
projectRoot: projectRoot,
|
||||
file: '.taskmaster/test-tasks.json',
|
||||
status: 'done'
|
||||
}
|
||||
});
|
||||
|
||||
if (result.isError) {
|
||||
throw new Error(`Tool returned error: ${result.content[0].text}`);
|
||||
}
|
||||
|
||||
const text = result.content[0].text;
|
||||
const data = JSON.parse(text);
|
||||
|
||||
if (!data.data || !data.data.tasks) {
|
||||
throw new Error('Invalid response format');
|
||||
}
|
||||
|
||||
if (data.data.tasks.length !== 1) {
|
||||
throw new Error(
|
||||
`Expected 1 task with done status, got ${data.data.tasks.length}`
|
||||
);
|
||||
}
|
||||
|
||||
const task = data.data.tasks[0];
|
||||
if (task.description !== 'Test task 2') {
|
||||
throw new Error(`Expected 'Test task 2', got '${task.description}'`);
|
||||
}
|
||||
if (task.status !== 'done') {
|
||||
throw new Error(`Expected status 'done', got '${task.status}'`);
|
||||
}
|
||||
});
|
||||
|
||||
// Test 5: Handle non-existent file
|
||||
await runTest('Handle non-existent file gracefully', async () => {
|
||||
const result = await client.callTool({
|
||||
name: 'get_tasks',
|
||||
arguments: {
|
||||
projectRoot: projectRoot,
|
||||
file: '.taskmaster/non-existent.json'
|
||||
}
|
||||
});
|
||||
|
||||
if (!result.isError) {
|
||||
throw new Error('Expected error for non-existent file');
|
||||
}
|
||||
if (!result.content[0].text.includes('Error')) {
|
||||
throw new Error('Expected error message');
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('\nConnection error:', error.message);
|
||||
testResults.failed = testResults.total;
|
||||
} finally {
|
||||
// Clean up
|
||||
await client.close();
|
||||
if (fs.existsSync(testTasksPath)) {
|
||||
fs.unlinkSync(testTasksPath);
|
||||
}
|
||||
|
||||
// Print summary
|
||||
console.log('\n' + '='.repeat(50));
|
||||
console.log('Test Summary:');
|
||||
console.log(`Total: ${testResults.total}`);
|
||||
console.log(`Passed: ${testResults.passed}`);
|
||||
console.log(`Failed: ${testResults.failed}`);
|
||||
console.log('='.repeat(50));
|
||||
|
||||
// Exit with appropriate code
|
||||
process.exit(testResults.failed > 0 ? 1 : 0);
|
||||
}
|
||||
}
|
||||
|
||||
runTests().catch(console.error);
|
||||
@@ -1,5 +1,11 @@
|
||||
const { spawn } = require('child_process');
|
||||
const { readFileSync, existsSync, copyFileSync, writeFileSync, readdirSync } = require('fs');
|
||||
const {
|
||||
readFileSync,
|
||||
existsSync,
|
||||
copyFileSync,
|
||||
writeFileSync,
|
||||
readdirSync
|
||||
} = require('fs');
|
||||
const { join } = require('path');
|
||||
|
||||
class TestHelpers {
|
||||
@@ -120,9 +126,7 @@ class TestHelpers {
|
||||
writeFileSync(filePath, content, 'utf8');
|
||||
return true;
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to write file ${filePath}: ${error.message}`
|
||||
);
|
||||
this.logger.error(`Failed to write file ${filePath}: ${error.message}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -134,9 +138,7 @@ class TestHelpers {
|
||||
try {
|
||||
return readFileSync(filePath, 'utf8');
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to read file ${filePath}: ${error.message}`
|
||||
);
|
||||
this.logger.error(`Failed to read file ${filePath}: ${error.message}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -148,9 +150,7 @@ class TestHelpers {
|
||||
try {
|
||||
return readdirSync(dirPath);
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to list files in ${dirPath}: ${error.message}`
|
||||
);
|
||||
this.logger.error(`Failed to list files in ${dirPath}: ${error.message}`);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -243,4 +243,4 @@ class TestHelpers {
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { TestHelpers };
|
||||
module.exports = { TestHelpers };
|
||||
|
||||
Reference in New Issue
Block a user