This commit is contained in:
Ralph Khreish
2025-07-11 13:29:52 +03:00
parent 74232d0e0d
commit 14cc09d241
27 changed files with 5699 additions and 958 deletions

View File

@@ -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);
});
});

View File

@@ -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;
}
};
};

View File

@@ -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
};
};

View File

@@ -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));
});

View 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/);
});
});
});

View File

@@ -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
});
});
});
});
});

View File

@@ -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/
);
});
});
});
});

View File

@@ -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}`);
});
});
});
});

View 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');
});
});
});

View File

@@ -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);
});
});
});

View 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();
});
});
});

View File

@@ -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;
}
}

View File

@@ -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);
});
});
});

View 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');
});
});
});

View 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
});
});
});

View File

@@ -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');
});
});
});
});

View File

@@ -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);
});
});
});

View File

@@ -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);
});
});
});

View 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);
}
}
});
});

View 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();

View 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);

View File

@@ -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 };