chore: improve add-task command e2e test
This commit is contained in:
41
jest.e2e.config.js
Normal file
41
jest.e2e.config.js
Normal file
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Jest configuration for E2E tests
|
||||
* Separate from unit tests to allow different settings
|
||||
*/
|
||||
|
||||
export default {
|
||||
displayName: 'E2E Tests',
|
||||
testMatch: ['<rootDir>/tests/e2e/**/*.test.js'],
|
||||
testPathIgnorePatterns: [
|
||||
'/node_modules/',
|
||||
'/tests/e2e/utils/',
|
||||
'/tests/e2e/config/',
|
||||
'/tests/e2e/runners/',
|
||||
'/tests/e2e/e2e_helpers.sh',
|
||||
'/tests/e2e/test_llm_analysis.sh',
|
||||
'/tests/e2e/run_e2e.sh',
|
||||
'/tests/e2e/run_fallback_verification.sh'
|
||||
],
|
||||
testEnvironment: 'node',
|
||||
testTimeout: 180000, // 3 minutes default (AI operations can be slow)
|
||||
maxWorkers: 1, // Run E2E tests sequentially to avoid conflicts
|
||||
verbose: true,
|
||||
setupFilesAfterEnv: ['<rootDir>/tests/e2e/setup/jest-setup.js'],
|
||||
globalSetup: '<rootDir>/tests/e2e/setup/global-setup.js',
|
||||
globalTeardown: '<rootDir>/tests/e2e/setup/global-teardown.js',
|
||||
collectCoverageFrom: [
|
||||
'src/**/*.js',
|
||||
'!src/**/*.test.js',
|
||||
'!src/**/__tests__/**'
|
||||
],
|
||||
coverageDirectory: '<rootDir>/coverage-e2e',
|
||||
// Custom reporters for better E2E test output
|
||||
reporters: ['default'],
|
||||
// Environment variables for E2E tests
|
||||
testEnvironmentOptions: {
|
||||
env: {
|
||||
NODE_ENV: 'test',
|
||||
E2E_TEST: 'true'
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -25,6 +25,9 @@
|
||||
"test:e2e:core": "node tests/e2e/run-e2e-tests.js --groups core",
|
||||
"test:e2e:providers": "node tests/e2e/run-e2e-tests.js --groups providers",
|
||||
"test:e2e:advanced": "node tests/e2e/run-e2e-tests.js --groups advanced",
|
||||
"test:e2e:jest": "jest --config jest.e2e.config.js",
|
||||
"test:e2e:jest:watch": "jest --config jest.e2e.config.js --watch",
|
||||
"test:e2e:jest:command": "jest --config jest.e2e.config.js --testNamePattern",
|
||||
"prepare": "chmod +x bin/task-master.js mcp-server/server.js",
|
||||
"changeset": "changeset",
|
||||
"release": "changeset publish",
|
||||
|
||||
19
tests/e2e/run-jest-e2e.js
Normal file
19
tests/e2e/run-jest-e2e.js
Normal file
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const { spawn } = require('child_process');
|
||||
const path = require('path');
|
||||
|
||||
const args = [
|
||||
'--config', 'jest.e2e.config.js',
|
||||
...process.argv.slice(2)
|
||||
];
|
||||
|
||||
const jest = spawn('jest', args, {
|
||||
cwd: path.join(__dirname, '../..'),
|
||||
stdio: 'inherit',
|
||||
env: { ...process.env, NODE_ENV: 'test' }
|
||||
});
|
||||
|
||||
jest.on('exit', (code) => {
|
||||
process.exit(code);
|
||||
});
|
||||
42
tests/e2e/setup/global-setup.js
Normal file
42
tests/e2e/setup/global-setup.js
Normal file
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* Global setup for E2E tests
|
||||
* Runs once before all test suites
|
||||
*/
|
||||
|
||||
const { execSync } = require('child_process');
|
||||
const { existsSync } = require('fs');
|
||||
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', {
|
||||
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.');
|
||||
} 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.');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Global setup failed:', error.message);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
11
tests/e2e/setup/global-teardown.js
Normal file
11
tests/e2e/setup/global-teardown.js
Normal file
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Global teardown for E2E tests
|
||||
* Runs once after all test suites
|
||||
*/
|
||||
|
||||
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
|
||||
};
|
||||
80
tests/e2e/setup/jest-setup.js
Normal file
80
tests/e2e/setup/jest-setup.js
Normal file
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* Jest setup file for E2E tests
|
||||
* Runs before each test file
|
||||
*/
|
||||
|
||||
const { TestHelpers } = require('../utils/test-helpers.cjs');
|
||||
const { TestLogger } = require('../utils/logger.cjs');
|
||||
|
||||
// Increase timeout for all E2E tests (can be overridden per test)
|
||||
jest.setTimeout(180000);
|
||||
|
||||
// Add custom matchers for CLI testing
|
||||
expect.extend({
|
||||
toContainTaskId(received) {
|
||||
const taskIdRegex = /#?\d+/;
|
||||
const pass = taskIdRegex.test(received);
|
||||
|
||||
if (pass) {
|
||||
return {
|
||||
message: () => `expected ${received} not to contain a task ID`,
|
||||
pass: true
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
message: () => `expected ${received} to contain a task ID (e.g., #123)`,
|
||||
pass: false
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
toHaveExitCode(received, expected) {
|
||||
const pass = received.exitCode === expected;
|
||||
|
||||
if (pass) {
|
||||
return {
|
||||
message: () => `expected exit code not to be ${expected}`,
|
||||
pass: true
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
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}"`,
|
||||
pass: true
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
message: () => `expected output to contain "${expected}"\nstdout: ${received.stdout}\nstderr: ${received.stderr}`,
|
||||
pass: false
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Global test helpers
|
||||
global.TestHelpers = TestHelpers;
|
||||
global.TestLogger = TestLogger;
|
||||
|
||||
// Helper to create test context
|
||||
global.createTestContext = (testName) => {
|
||||
const logger = new TestLogger(testName);
|
||||
const helpers = new TestHelpers(logger);
|
||||
return { logger, helpers };
|
||||
};
|
||||
|
||||
// Clean up any hanging processes
|
||||
afterAll(async () => {
|
||||
// Give time for any async operations to complete
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
});
|
||||
@@ -3,53 +3,115 @@
|
||||
* Tests all aspects of task creation including AI and manual modes
|
||||
*/
|
||||
|
||||
export default async function testAddTask(logger, helpers, context) {
|
||||
const { testDir } = context;
|
||||
const results = {
|
||||
status: 'passed',
|
||||
errors: [],
|
||||
tests: []
|
||||
};
|
||||
const { mkdtempSync, existsSync, readFileSync, rmSync, writeFileSync, mkdirSync } = require('fs');
|
||||
const { join } = require('path');
|
||||
const { tmpdir } = require('os');
|
||||
const path = require('path');
|
||||
|
||||
async function runTest(name, testFn) {
|
||||
try {
|
||||
logger.info(`\nRunning: ${name}`);
|
||||
await testFn();
|
||||
results.tests.push({ name, status: 'passed' });
|
||||
logger.success(`✓ ${name}`);
|
||||
} catch (error) {
|
||||
results.tests.push({ name, status: 'failed', error: error.message });
|
||||
results.errors.push({ test: name, error: error.message });
|
||||
logger.error(`✗ ${name}: ${error.message}`);
|
||||
describe('add-task command', () => {
|
||||
let testDir;
|
||||
let helpers;
|
||||
|
||||
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');
|
||||
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: [] } }));
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
logger.info('Starting comprehensive add-task tests...');
|
||||
afterEach(() => {
|
||||
// Clean up test directory
|
||||
if (testDir && existsSync(testDir)) {
|
||||
rmSync(testDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
// Test 1: Basic AI task creation with --prompt
|
||||
await runTest('AI task creation with prompt', async () => {
|
||||
describe('AI-powered task creation', () => {
|
||||
it('should create task with AI prompt', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'add-task',
|
||||
['--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 });
|
||||
|
||||
// AI generated task should contain a title and description
|
||||
expect(showResult.stdout).toContain('Title:');
|
||||
expect(showResult.stdout).toContain('Description:');
|
||||
expect(showResult.stdout).toContain('Implementation Details:');
|
||||
}, 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 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 result = await helpers.taskMaster(
|
||||
'add-task',
|
||||
['--prompt', specialPrompt],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Command failed: ${result.stderr}`);
|
||||
}
|
||||
const taskId = helpers.extractTaskId(result.stdout);
|
||||
if (!taskId) {
|
||||
throw new Error('Failed to extract task ID from output');
|
||||
}
|
||||
// Verify task was created with AI-generated content
|
||||
const showResult = await helpers.taskMaster('show', [taskId], { cwd: testDir });
|
||||
if (!showResult.stdout.includes('authentication') && !showResult.stdout.includes('JWT')) {
|
||||
throw new Error('AI did not properly understand the prompt');
|
||||
}
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContainTaskId();
|
||||
});
|
||||
|
||||
// Test 2: Manual task creation with --title and --description
|
||||
await runTest('Manual task creation', async () => {
|
||||
it('should verify AI generates reasonable output', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'add-task',
|
||||
['--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 });
|
||||
// Verify AI generated task has proper structure
|
||||
expect(showResult.stdout).toContain('Title:');
|
||||
expect(showResult.stdout).toContain('Status:');
|
||||
expect(showResult.stdout).toContain('Priority:');
|
||||
expect(showResult.stdout).toContain('Description:');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Manual task creation', () => {
|
||||
it('should create task with title and description', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'add-task',
|
||||
[
|
||||
@@ -58,75 +120,55 @@ export default async function testAddTask(logger, helpers, context) {
|
||||
],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Command failed: ${result.stderr}`);
|
||||
}
|
||||
const taskId = helpers.extractTaskId(result.stdout);
|
||||
if (!taskId) {
|
||||
throw new Error('Failed to extract task ID');
|
||||
}
|
||||
// Verify exact title and description
|
||||
const showResult = await helpers.taskMaster('show', [taskId], { cwd: testDir });
|
||||
if (!showResult.stdout.includes('Setup database connection')) {
|
||||
throw new Error('Title not set correctly');
|
||||
}
|
||||
if (!showResult.stdout.includes('Configure PostgreSQL connection')) {
|
||||
throw new Error('Description not set correctly');
|
||||
}
|
||||
});
|
||||
|
||||
// Test 3: Task creation with tags
|
||||
await runTest('Task creation with tags', async () => {
|
||||
// First create a tag
|
||||
await helpers.taskMaster(
|
||||
'add-tag',
|
||||
['backend', '--description', 'Backend tasks'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
// Create task with tag
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContainTaskId();
|
||||
|
||||
const taskId = helpers.extractTaskId(result.stdout);
|
||||
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',
|
||||
['--prompt', 'Create REST API endpoints', '--tag', 'backend'],
|
||||
[
|
||||
'--title', 'Implement caching layer',
|
||||
'--description', 'Add Redis caching to improve performance',
|
||||
'--details', 'Use Redis for session storage and API response caching'
|
||||
],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Command failed: ${result.stderr}`);
|
||||
}
|
||||
const taskId = helpers.extractTaskId(result.stdout);
|
||||
|
||||
// Verify task is in tag
|
||||
const listResult = await helpers.taskMaster('list', ['--tag', 'backend'], { cwd: testDir });
|
||||
if (!listResult.stdout.includes(taskId)) {
|
||||
throw new Error('Task not found in specified tag');
|
||||
}
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContainTaskId();
|
||||
});
|
||||
});
|
||||
|
||||
// Test 4: Task creation with priority
|
||||
await runTest('Task creation with priority', async () => {
|
||||
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'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Command failed: ${result.stderr}`);
|
||||
}
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
const taskId = helpers.extractTaskId(result.stdout);
|
||||
|
||||
// Verify priority was set
|
||||
const showResult = await helpers.taskMaster('show', [taskId], { cwd: testDir });
|
||||
if (!showResult.stdout.includes('high') && !showResult.stdout.includes('High')) {
|
||||
throw new Error('Priority not set correctly');
|
||||
}
|
||||
expect(showResult.stdout.toLowerCase()).toContain('high');
|
||||
});
|
||||
|
||||
// Test 5: Task creation with dependencies at creation time
|
||||
await runTest('Task creation with dependencies', async () => {
|
||||
it('should create task with dependencies', async () => {
|
||||
// Create dependency task first
|
||||
const depResult = await helpers.taskMaster(
|
||||
'add-task',
|
||||
['--title', 'Setup environment'],
|
||||
['--title', 'Setup environment', '--description', 'Initial environment setup'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
const depTaskId = helpers.extractTaskId(depResult.stdout);
|
||||
@@ -134,208 +176,57 @@ export default async function testAddTask(logger, helpers, context) {
|
||||
// Create task with dependency
|
||||
const result = await helpers.taskMaster(
|
||||
'add-task',
|
||||
['--prompt', 'Deploy application', '--depends-on', depTaskId],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Command failed: ${result.stderr}`);
|
||||
}
|
||||
const taskId = helpers.extractTaskId(result.stdout);
|
||||
|
||||
// Verify dependency was set
|
||||
const showResult = await helpers.taskMaster('show', [taskId], { cwd: testDir });
|
||||
if (!showResult.stdout.includes(depTaskId)) {
|
||||
throw new Error('Dependency not set correctly');
|
||||
}
|
||||
});
|
||||
|
||||
// Test 6: Task creation with custom metadata
|
||||
await runTest('Task creation with metadata', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'add-task',
|
||||
[
|
||||
'--prompt', 'Implement caching layer',
|
||||
'--metadata', 'team=backend',
|
||||
'--metadata', 'sprint=2024-Q1'
|
||||
],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Command failed: ${result.stderr}`);
|
||||
}
|
||||
const taskId = helpers.extractTaskId(result.stdout);
|
||||
|
||||
// Verify metadata (check in tasks.json)
|
||||
const tasksPath = `${testDir}/.taskmaster/tasks/tasks.json`;
|
||||
const tasks = helpers.readJson(tasksPath);
|
||||
const task = tasks.tasks.find(t => t.id === taskId);
|
||||
if (!task || !task.metadata || task.metadata.team !== 'backend' || task.metadata.sprint !== '2024-Q1') {
|
||||
throw new Error('Metadata not set correctly');
|
||||
}
|
||||
});
|
||||
|
||||
// Test 7: Error handling - empty prompt
|
||||
await runTest('Error handling - empty prompt', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'add-task',
|
||||
['--prompt', ''],
|
||||
{ cwd: testDir, allowFailure: true }
|
||||
);
|
||||
if (result.exitCode === 0) {
|
||||
throw new Error('Should have failed with empty prompt');
|
||||
}
|
||||
});
|
||||
|
||||
// Test 8: Error handling - invalid priority
|
||||
await runTest('Error handling - invalid priority', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'add-task',
|
||||
['--prompt', 'Test task', '--priority', 'invalid'],
|
||||
{ cwd: testDir, allowFailure: true }
|
||||
);
|
||||
if (result.exitCode === 0) {
|
||||
throw new Error('Should have failed with invalid priority');
|
||||
}
|
||||
});
|
||||
|
||||
// Test 9: Error handling - non-existent dependency
|
||||
await runTest('Error handling - non-existent dependency', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'add-task',
|
||||
['--prompt', 'Test task', '--depends-on', '99999'],
|
||||
{ cwd: testDir, allowFailure: true }
|
||||
);
|
||||
if (result.exitCode === 0) {
|
||||
throw new Error('Should have failed with non-existent dependency');
|
||||
}
|
||||
});
|
||||
|
||||
// Test 10: Very long prompt handling
|
||||
await runTest('Very long prompt handling', async () => {
|
||||
const longPrompt = 'Create a comprehensive system that ' + 'handles many features '.repeat(50);
|
||||
const result = await helpers.taskMaster(
|
||||
'add-task',
|
||||
['--prompt', longPrompt],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Command failed: ${result.stderr}`);
|
||||
}
|
||||
const taskId = helpers.extractTaskId(result.stdout);
|
||||
if (!taskId) {
|
||||
throw new Error('Failed to create task with long prompt');
|
||||
}
|
||||
});
|
||||
|
||||
// Test 11: Special characters in prompt
|
||||
await runTest('Special characters in prompt', async () => {
|
||||
const specialPrompt = 'Implement feature: "User\'s data & settings" <with> special|chars!';
|
||||
const result = await helpers.taskMaster(
|
||||
'add-task',
|
||||
['--prompt', specialPrompt],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Command failed: ${result.stderr}`);
|
||||
}
|
||||
const taskId = helpers.extractTaskId(result.stdout);
|
||||
if (!taskId) {
|
||||
throw new Error('Failed to create task with special characters');
|
||||
}
|
||||
});
|
||||
|
||||
// Test 12: Multiple tasks in parallel
|
||||
await runTest('Multiple tasks in parallel', async () => {
|
||||
const promises = [];
|
||||
for (let i = 0; i < 3; i++) {
|
||||
promises.push(
|
||||
helpers.taskMaster(
|
||||
'add-task',
|
||||
['--prompt', `Parallel task ${i + 1}`],
|
||||
{ cwd: testDir }
|
||||
)
|
||||
);
|
||||
}
|
||||
const results = await Promise.all(promises);
|
||||
|
||||
for (let i = 0; i < results.length; i++) {
|
||||
if (results[i].exitCode !== 0) {
|
||||
throw new Error(`Parallel task ${i + 1} failed`);
|
||||
}
|
||||
const taskId = helpers.extractTaskId(results[i].stdout);
|
||||
if (!taskId) {
|
||||
throw new Error(`Failed to extract task ID for parallel task ${i + 1}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Test 13: AI fallback behavior (simulate by using invalid model)
|
||||
await runTest('AI fallback behavior', async () => {
|
||||
// Set an invalid model to trigger fallback
|
||||
await helpers.taskMaster(
|
||||
'models',
|
||||
['--set-main', 'invalid-model-xyz'],
|
||||
['--prompt', 'Deploy application', '--dependencies', depTaskId],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
const result = await helpers.taskMaster(
|
||||
'add-task',
|
||||
['--prompt', 'Test fallback behavior'],
|
||||
{ cwd: testDir, allowFailure: true }
|
||||
);
|
||||
|
||||
// Should either use fallback model or create task without AI
|
||||
// The exact behavior depends on implementation
|
||||
if (result.exitCode === 0) {
|
||||
const taskId = helpers.extractTaskId(result.stdout);
|
||||
if (!taskId) {
|
||||
throw new Error('Fallback did not create a task');
|
||||
}
|
||||
}
|
||||
|
||||
// Reset to valid model
|
||||
await helpers.taskMaster(
|
||||
'models',
|
||||
['--set-main', 'gpt-3.5-turbo'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
});
|
||||
|
||||
// Test 14: AI quality check - verify reasonable output
|
||||
await runTest('AI quality - reasonable title and description', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'add-task',
|
||||
['--prompt', 'Build a responsive navigation menu with dropdown support'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Command failed: ${result.stderr}`);
|
||||
}
|
||||
expect(result).toHaveExitCode(0);
|
||||
const taskId = helpers.extractTaskId(result.stdout);
|
||||
|
||||
const showResult = await helpers.taskMaster('show', [taskId], { cwd: testDir });
|
||||
const output = showResult.stdout.toLowerCase();
|
||||
|
||||
// Check for relevant keywords that indicate AI understood the prompt
|
||||
const relevantKeywords = ['navigation', 'menu', 'dropdown', 'responsive'];
|
||||
const foundKeywords = relevantKeywords.filter(keyword => output.includes(keyword));
|
||||
|
||||
if (foundKeywords.length < 2) {
|
||||
throw new Error('AI output does not seem to understand the prompt properly');
|
||||
}
|
||||
expect(showResult.stdout).toContain(depTaskId);
|
||||
});
|
||||
|
||||
// Test 15: Task creation with all options combined
|
||||
await runTest('Task creation with all options', async () => {
|
||||
// Create dependency
|
||||
it('should handle multiple dependencies', async () => {
|
||||
// Create multiple dependency tasks
|
||||
const dep1 = await helpers.taskMaster(
|
||||
'add-task',
|
||||
['--prompt', 'Setup environment'],
|
||||
{ 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}`],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
const taskId = helpers.extractTaskId(result.stdout);
|
||||
|
||||
const showResult = await helpers.taskMaster('show', [taskId], { cwd: testDir });
|
||||
expect(showResult.stdout).toContain(depId1);
|
||||
expect(showResult.stdout).toContain(depId2);
|
||||
});
|
||||
|
||||
it('should create task with all options combined', async () => {
|
||||
// Setup
|
||||
const depResult = await helpers.taskMaster(
|
||||
'add-task',
|
||||
['--title', 'Prerequisite task'],
|
||||
['--title', 'Prerequisite task', '--description', 'Task that must be completed first'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
const depTaskId = helpers.extractTaskId(depResult.stdout);
|
||||
|
||||
// Create tag
|
||||
await helpers.taskMaster(
|
||||
'add-tag',
|
||||
['feature-complete', '--description', 'Complete feature test'],
|
||||
@@ -348,64 +239,292 @@ export default async function testAddTask(logger, helpers, context) {
|
||||
[
|
||||
'--prompt', 'Comprehensive task with all features',
|
||||
'--priority', 'medium',
|
||||
'--tag', 'feature-complete',
|
||||
'--depends-on', depTaskId,
|
||||
'--metadata', 'complexity=high',
|
||||
'--metadata', 'estimated_hours=8'
|
||||
'--dependencies', depTaskId
|
||||
],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Command failed: ${result.stderr}`);
|
||||
}
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
const taskId = helpers.extractTaskId(result.stdout);
|
||||
|
||||
// Verify all options were applied
|
||||
// Verify all options
|
||||
const showResult = await helpers.taskMaster('show', [taskId], { cwd: testDir });
|
||||
const listResult = await helpers.taskMaster('list', ['--tag', 'feature-complete'], { cwd: testDir });
|
||||
const tasksData = helpers.readJson(`${testDir}/.taskmaster/tasks/tasks.json`);
|
||||
const task = tasksData.tasks.find(t => t.id === taskId);
|
||||
expect(showResult.stdout.toLowerCase()).toContain('medium');
|
||||
expect(showResult.stdout).toContain(depTaskId);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error handling', () => {
|
||||
it('should fail without prompt or title+description', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'add-task',
|
||||
[],
|
||||
{ cwd: testDir, allowFailure: true }
|
||||
);
|
||||
|
||||
if (!showResult.stdout.includes('medium') && !showResult.stdout.includes('Medium')) {
|
||||
throw new Error('Priority not set');
|
||||
}
|
||||
if (!listResult.stdout.includes(taskId)) {
|
||||
throw new Error('Task not in tag');
|
||||
}
|
||||
if (!showResult.stdout.includes(depTaskId)) {
|
||||
throw new Error('Dependency not set');
|
||||
}
|
||||
if (!task || !task.metadata || task.metadata.complexity !== 'high') {
|
||||
throw new Error('Metadata not set correctly');
|
||||
}
|
||||
expect(result.exitCode).not.toBe(0);
|
||||
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);
|
||||
});
|
||||
|
||||
// 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;
|
||||
|
||||
logger.info('\n=== Add-Task Test Summary ===');
|
||||
logger.info(`Total tests: ${totalTests}`);
|
||||
logger.info(`Passed: ${passedTests}`);
|
||||
logger.info(`Failed: ${failedTests}`);
|
||||
|
||||
if (failedTests > 0) {
|
||||
results.status = 'failed';
|
||||
logger.error(`\n${failedTests} tests failed`);
|
||||
} else {
|
||||
logger.success('\n✅ All add-task tests passed!');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
results.status = 'failed';
|
||||
results.errors.push({
|
||||
test: 'add-task test suite',
|
||||
error: error.message,
|
||||
stack: error.stack
|
||||
it('should handle invalid priority by defaulting to medium', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'add-task',
|
||||
['--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 });
|
||||
expect(showResult.stdout).toContain('Priority: │ medium');
|
||||
});
|
||||
logger.error(`Add-task test suite failed: ${error.message}`);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
it('should warn and continue with non-existent dependency', async () => {
|
||||
// Based on the implementation, invalid dependencies are filtered out with a warning
|
||||
const result = await helpers.taskMaster(
|
||||
'add-task',
|
||||
['--prompt', 'Test task', '--dependencies', '99999'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
// Should succeed but with warning
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('do not exist');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Concurrent operations', () => {
|
||||
it('should handle multiple tasks created in parallel', async () => {
|
||||
const promises = [];
|
||||
for (let i = 0; i < 3; i++) {
|
||||
promises.push(
|
||||
helpers.taskMaster(
|
||||
'add-task',
|
||||
['--prompt', `Parallel task ${i + 1}`],
|
||||
{ cwd: testDir }
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const results = await Promise.all(promises);
|
||||
|
||||
results.forEach((result) => {
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContainTaskId();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Research mode', () => {
|
||||
it('should create task using research mode', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'add-task',
|
||||
[
|
||||
'--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 });
|
||||
// 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'
|
||||
],
|
||||
{ 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],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
const taskId = helpers.extractTaskId(result.stdout);
|
||||
|
||||
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]],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
const taskId = helpers.extractTaskId(result.stdout);
|
||||
|
||||
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 });
|
||||
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
|
||||
// Create an existing task that AI might reference
|
||||
await helpers.taskMaster(
|
||||
'add-task',
|
||||
['--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')) {
|
||||
expect(result.stdout).toContain('Dependencies');
|
||||
}
|
||||
}, 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 });
|
||||
|
||||
// Add task to specific tag
|
||||
const result = await helpers.taskMaster(
|
||||
'add-task',
|
||||
[
|
||||
'--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(
|
||||
'show',
|
||||
[taskId, '--tag', 'feature-branch'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
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'));
|
||||
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 }
|
||||
);
|
||||
|
||||
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 }
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -3,33 +3,40 @@
|
||||
* Tests all aspects of complexity analysis including research mode and output formats
|
||||
*/
|
||||
|
||||
export default async function testAnalyzeComplexity(logger, helpers, context) {
|
||||
const { testDir } = context;
|
||||
const results = {
|
||||
status: 'passed',
|
||||
errors: [],
|
||||
tests: []
|
||||
};
|
||||
const { mkdtempSync, existsSync, readFileSync, rmSync, writeFileSync, mkdirSync } = require('fs');
|
||||
const { join } = require('path');
|
||||
const { tmpdir } = require('os');
|
||||
const { execSync } = require('child_process');
|
||||
|
||||
async function runTest(name, testFn) {
|
||||
try {
|
||||
logger.info(`\nRunning: ${name}`);
|
||||
await testFn();
|
||||
results.tests.push({ name, status: 'passed' });
|
||||
logger.success(`✓ ${name}`);
|
||||
} catch (error) {
|
||||
results.tests.push({ name, status: 'failed', error: error.message });
|
||||
results.errors.push({ test: name, error: error.message });
|
||||
logger.error(`✗ ${name}: ${error.message}`);
|
||||
describe('analyze-complexity command', () => {
|
||||
let testDir;
|
||||
let helpers;
|
||||
let logger;
|
||||
let taskIds;
|
||||
|
||||
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');
|
||||
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);
|
||||
|
||||
try {
|
||||
logger.info('Starting comprehensive analyze-complexity tests...');
|
||||
|
||||
// Setup: Create some tasks for analysis
|
||||
logger.info('Setting up test tasks...');
|
||||
const taskIds = [];
|
||||
// Setup test tasks for analysis
|
||||
taskIds = [];
|
||||
|
||||
// Create simple task
|
||||
const simple = await helpers.taskMaster(
|
||||
@@ -58,160 +65,112 @@ export default async function testAnalyzeComplexity(logger, helpers, context) {
|
||||
{ cwd: testDir }
|
||||
);
|
||||
taskIds.push(helpers.extractTaskId(withDeps.stdout));
|
||||
});
|
||||
|
||||
// Test 1: Basic complexity analysis
|
||||
await runTest('Basic complexity analysis', async () => {
|
||||
afterEach(() => {
|
||||
// Clean up test directory
|
||||
if (testDir && existsSync(testDir)) {
|
||||
rmSync(testDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
describe('Basic complexity analysis', () => {
|
||||
it('should analyze complexity without flags', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'analyze-complexity',
|
||||
[],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Command failed: ${result.stderr}`);
|
||||
}
|
||||
// Check for basic output
|
||||
if (!result.stdout.includes('Complexity') && !result.stdout.includes('complexity')) {
|
||||
throw new Error('Output does not contain complexity information');
|
||||
}
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout.toLowerCase()).toContain('complexity');
|
||||
});
|
||||
|
||||
// Test 2: Complexity analysis with research flag
|
||||
await runTest('Complexity analysis with --research', async () => {
|
||||
it('should analyze with research flag', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'analyze-complexity',
|
||||
['--research'],
|
||||
{ cwd: testDir, timeout: 120000 }
|
||||
);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Command failed: ${result.stderr}`);
|
||||
}
|
||||
// Research mode should provide more detailed analysis
|
||||
if (!result.stdout.includes('Complexity') && !result.stdout.includes('complexity')) {
|
||||
throw new Error('Research mode did not provide complexity analysis');
|
||||
}
|
||||
});
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout.toLowerCase()).toContain('complexity');
|
||||
}, 120000);
|
||||
});
|
||||
|
||||
// Test 3: Complexity analysis with custom output file
|
||||
await runTest('Complexity analysis with custom output', async () => {
|
||||
describe('Output options', () => {
|
||||
it('should save to custom output file', async () => {
|
||||
const outputPath = '.taskmaster/reports/custom-complexity.json';
|
||||
const result = await helpers.taskMaster(
|
||||
'analyze-complexity',
|
||||
['--output', outputPath],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Command failed: ${result.stderr}`);
|
||||
}
|
||||
// Verify file was created
|
||||
const fullPath = `${testDir}/${outputPath}`;
|
||||
if (!helpers.fileExists(fullPath)) {
|
||||
throw new Error('Custom output file was not created');
|
||||
}
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
const fullPath = join(testDir, outputPath);
|
||||
expect(existsSync(fullPath)).toBe(true);
|
||||
|
||||
// Verify it's valid JSON
|
||||
const report = helpers.readJson(fullPath);
|
||||
if (!report || typeof report !== 'object') {
|
||||
throw new Error('Output file is not valid JSON');
|
||||
}
|
||||
const report = JSON.parse(readFileSync(fullPath, 'utf8'));
|
||||
expect(report).toBeDefined();
|
||||
expect(typeof report).toBe('object');
|
||||
});
|
||||
|
||||
// Test 4: Complexity analysis for specific tasks
|
||||
await runTest('Complexity analysis for specific tasks', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'analyze-complexity',
|
||||
['--tasks', taskIds.join(',')],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Command failed: ${result.stderr}`);
|
||||
}
|
||||
// Should analyze only specified tasks
|
||||
for (const taskId of taskIds) {
|
||||
if (!result.stdout.includes(taskId)) {
|
||||
throw new Error(`Task ${taskId} not included in analysis`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Test 5: Complexity analysis with custom thresholds
|
||||
await runTest('Complexity analysis with custom thresholds', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'analyze-complexity',
|
||||
['--low-threshold', '3', '--medium-threshold', '7', '--high-threshold', '10'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Command failed: ${result.stderr}`);
|
||||
}
|
||||
// Output should reflect custom thresholds
|
||||
if (!result.stdout.includes('low') || !result.stdout.includes('medium') || !result.stdout.includes('high')) {
|
||||
throw new Error('Custom thresholds not reflected in output');
|
||||
}
|
||||
});
|
||||
|
||||
// Test 6: Complexity analysis with JSON output format
|
||||
await runTest('Complexity analysis with JSON format', async () => {
|
||||
it('should output in JSON format', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'analyze-complexity',
|
||||
['--format', 'json'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Command failed: ${result.stderr}`);
|
||||
}
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
// Output should be valid JSON
|
||||
try {
|
||||
const parsed = JSON.parse(result.stdout);
|
||||
if (!parsed || typeof parsed !== 'object') {
|
||||
throw new Error('Output is not valid JSON object');
|
||||
}
|
||||
} catch (e) {
|
||||
throw new Error('Output is not valid JSON format');
|
||||
}
|
||||
let parsed;
|
||||
expect(() => {
|
||||
parsed = JSON.parse(result.stdout);
|
||||
}).not.toThrow();
|
||||
|
||||
expect(parsed).toBeDefined();
|
||||
expect(typeof parsed).toBe('object');
|
||||
});
|
||||
|
||||
// Test 7: Complexity analysis with detailed breakdown
|
||||
await runTest('Complexity analysis with --detailed flag', async () => {
|
||||
it('should show detailed breakdown', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'analyze-complexity',
|
||||
['--detailed'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Command failed: ${result.stderr}`);
|
||||
}
|
||||
// Should include detailed breakdown
|
||||
const expectedDetails = ['subtasks', 'dependencies', 'description', 'metadata'];
|
||||
const foundDetails = expectedDetails.filter(detail =>
|
||||
result.stdout.toLowerCase().includes(detail)
|
||||
);
|
||||
if (foundDetails.length < 2) {
|
||||
throw new Error('Detailed breakdown not comprehensive enough');
|
||||
}
|
||||
});
|
||||
|
||||
// Test 8: Complexity analysis for empty project
|
||||
await runTest('Complexity analysis with no tasks', async () => {
|
||||
// Create a new temp directory
|
||||
const emptyDir = `${testDir}_empty`;
|
||||
await helpers.executeCommand('mkdir', ['-p', emptyDir]);
|
||||
await helpers.taskMaster('init', ['-y'], { cwd: emptyDir });
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
const output = result.stdout.toLowerCase();
|
||||
const expectedDetails = ['subtasks', 'dependencies', 'description', 'metadata'];
|
||||
const foundDetails = expectedDetails.filter(detail => output.includes(detail));
|
||||
|
||||
expect(foundDetails.length).toBeGreaterThanOrEqual(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Filtering options', () => {
|
||||
it('should analyze specific tasks', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'analyze-complexity',
|
||||
[],
|
||||
{ cwd: emptyDir }
|
||||
['--tasks', taskIds.join(',')],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Command failed: ${result.stderr}`);
|
||||
}
|
||||
// Should handle empty project gracefully
|
||||
if (!result.stdout.includes('No tasks') && !result.stdout.includes('0')) {
|
||||
throw new Error('Empty project not handled gracefully');
|
||||
}
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
// Should analyze only specified tasks
|
||||
taskIds.forEach(taskId => {
|
||||
expect(result.stdout).toContain(taskId);
|
||||
});
|
||||
});
|
||||
|
||||
// Test 9: Complexity analysis with tag filter
|
||||
await runTest('Complexity analysis filtered by tag', async () => {
|
||||
it('should filter by tag', async () => {
|
||||
// Create tag and tagged task
|
||||
await helpers.taskMaster('add-tag', ['complex-tag'], { cwd: testDir });
|
||||
const taggedResult = await helpers.taskMaster(
|
||||
@@ -226,17 +185,12 @@ export default async function testAnalyzeComplexity(logger, helpers, context) {
|
||||
['--tag', 'complex-tag'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Command failed: ${result.stderr}`);
|
||||
}
|
||||
// Should only analyze tagged tasks
|
||||
if (!result.stdout.includes(taggedId)) {
|
||||
throw new Error('Tagged task not included in filtered analysis');
|
||||
}
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain(taggedId);
|
||||
});
|
||||
|
||||
// Test 10: Complexity analysis with status filter
|
||||
await runTest('Complexity analysis filtered by status', async () => {
|
||||
it('should filter by status', async () => {
|
||||
// Set one task to completed
|
||||
await helpers.taskMaster('set-status', [taskIds[0], 'completed'], { cwd: testDir });
|
||||
|
||||
@@ -245,64 +199,74 @@ export default async function testAnalyzeComplexity(logger, helpers, context) {
|
||||
['--status', 'pending'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Command failed: ${result.stderr}`);
|
||||
}
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
// Should not include completed task
|
||||
if (result.stdout.includes(taskIds[0])) {
|
||||
throw new Error('Completed task included in pending-only analysis');
|
||||
}
|
||||
expect(result.stdout).not.toContain(taskIds[0]);
|
||||
});
|
||||
});
|
||||
|
||||
// Test 11: Generate complexity report command
|
||||
await runTest('Generate complexity report', async () => {
|
||||
// First run analyze-complexity to generate data
|
||||
await helpers.taskMaster(
|
||||
describe('Threshold configuration', () => {
|
||||
it('should use custom thresholds', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'analyze-complexity',
|
||||
['--output', '.taskmaster/reports/task-complexity-report.json'],
|
||||
['--low-threshold', '3', '--medium-threshold', '7', '--high-threshold', '10'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
const result = await helpers.taskMaster(
|
||||
'complexity-report',
|
||||
[],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Command failed: ${result.stderr}`);
|
||||
}
|
||||
// Should display report
|
||||
if (!result.stdout.includes('Complexity Report') && !result.stdout.includes('complexity')) {
|
||||
throw new Error('Complexity report not displayed');
|
||||
}
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
const output = result.stdout.toLowerCase();
|
||||
expect(output).toContain('low');
|
||||
expect(output).toContain('medium');
|
||||
expect(output).toContain('high');
|
||||
});
|
||||
|
||||
// Test 12: Error handling - invalid threshold values
|
||||
await runTest('Error handling - invalid thresholds', async () => {
|
||||
it('should reject invalid thresholds', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'analyze-complexity',
|
||||
['--low-threshold', '-1'],
|
||||
{ cwd: testDir, allowFailure: true }
|
||||
);
|
||||
if (result.exitCode === 0) {
|
||||
throw new Error('Should have failed with negative threshold');
|
||||
|
||||
expect(result.exitCode).not.toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge cases', () => {
|
||||
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 }
|
||||
);
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout.toLowerCase()).toMatch(/no tasks|0/);
|
||||
} finally {
|
||||
rmSync(emptyDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
// Test 13: Error handling - invalid output path
|
||||
await runTest('Error handling - invalid output path', async () => {
|
||||
it('should handle invalid output path', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'analyze-complexity',
|
||||
['--output', '/invalid/path/report.json'],
|
||||
{ cwd: testDir, allowFailure: true }
|
||||
);
|
||||
if (result.exitCode === 0) {
|
||||
throw new Error('Should have failed with invalid output path');
|
||||
}
|
||||
|
||||
expect(result.exitCode).not.toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
// Test 14: Performance test - large number of tasks
|
||||
await runTest('Performance - analyze many tasks', async () => {
|
||||
describe('Performance', () => {
|
||||
it('should analyze many tasks efficiently', async () => {
|
||||
// Create 20 more tasks
|
||||
const promises = [];
|
||||
for (let i = 0; i < 20; i++) {
|
||||
@@ -324,67 +288,48 @@ export default async function testAnalyzeComplexity(logger, helpers, context) {
|
||||
);
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Command failed: ${result.stderr}`);
|
||||
}
|
||||
// Should complete in reasonable time (< 10 seconds)
|
||||
if (duration > 10000) {
|
||||
throw new Error(`Analysis took too long: ${duration}ms`);
|
||||
}
|
||||
logger.info(`Analyzed ~25 tasks in ${duration}ms`);
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(duration).toBeLessThan(10000); // Should complete in less than 10 seconds
|
||||
});
|
||||
});
|
||||
|
||||
// Test 15: Verify complexity scoring algorithm
|
||||
await runTest('Verify complexity scoring accuracy', async () => {
|
||||
// The complex task with subtasks should have higher score than simple task
|
||||
describe('Complexity scoring', () => {
|
||||
it('should score complex tasks higher than simple ones', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'analyze-complexity',
|
||||
['--format', 'json'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Command failed: ${result.stderr}`);
|
||||
}
|
||||
|
||||
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]);
|
||||
|
||||
if (!simpleTask || !complexTask) {
|
||||
throw new Error('Could not find tasks in analysis');
|
||||
}
|
||||
expect(simpleTask).toBeDefined();
|
||||
expect(complexTask).toBeDefined();
|
||||
expect(complexTask.complexity).toBeGreaterThan(simpleTask.complexity);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Report generation', () => {
|
||||
it('should generate complexity report', async () => {
|
||||
// First run analyze-complexity to generate data
|
||||
await helpers.taskMaster(
|
||||
'analyze-complexity',
|
||||
['--output', '.taskmaster/reports/task-complexity-report.json'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
if (simpleTask.complexity >= complexTask.complexity) {
|
||||
throw new Error('Complex task should have higher complexity score than simple task');
|
||||
}
|
||||
const result = await helpers.taskMaster(
|
||||
'complexity-report',
|
||||
[],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout.toLowerCase()).toMatch(/complexity report|complexity/);
|
||||
});
|
||||
|
||||
// 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;
|
||||
|
||||
logger.info('\n=== Analyze-Complexity Test Summary ===');
|
||||
logger.info(`Total tests: ${totalTests}`);
|
||||
logger.info(`Passed: ${passedTests}`);
|
||||
logger.info(`Failed: ${failedTests}`);
|
||||
|
||||
if (failedTests > 0) {
|
||||
results.status = 'failed';
|
||||
logger.error(`\n${failedTests} tests failed`);
|
||||
} else {
|
||||
logger.success('\n✅ All analyze-complexity tests passed!');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
results.status = 'failed';
|
||||
results.errors.push({
|
||||
test: 'analyze-complexity test suite',
|
||||
error: error.message,
|
||||
stack: error.stack
|
||||
});
|
||||
logger.error(`Analyze-complexity test suite failed: ${error.message}`);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -3,32 +3,43 @@
|
||||
* Tests all aspects of task expansion including single, multiple, and recursive expansion
|
||||
*/
|
||||
|
||||
export default async function testExpandTask(logger, helpers, context) {
|
||||
const { testDir } = context;
|
||||
const results = {
|
||||
status: 'passed',
|
||||
errors: [],
|
||||
tests: []
|
||||
};
|
||||
const { mkdtempSync, existsSync, readFileSync, rmSync, writeFileSync, mkdirSync } = require('fs');
|
||||
const { join } = require('path');
|
||||
const { tmpdir } = require('os');
|
||||
|
||||
async function runTest(name, testFn) {
|
||||
try {
|
||||
logger.info(`\nRunning: ${name}`);
|
||||
await testFn();
|
||||
results.tests.push({ name, status: 'passed' });
|
||||
logger.success(`✓ ${name}`);
|
||||
} catch (error) {
|
||||
results.tests.push({ name, status: 'failed', error: error.message });
|
||||
results.errors.push({ test: name, error: error.message });
|
||||
logger.error(`✗ ${name}: ${error.message}`);
|
||||
describe('expand-task command', () => {
|
||||
let testDir;
|
||||
let helpers;
|
||||
let simpleTaskId;
|
||||
let complexTaskId;
|
||||
let manualTaskId;
|
||||
|
||||
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');
|
||||
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: [] } }));
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
logger.info('Starting comprehensive expand-task tests...');
|
||||
|
||||
// Setup: Create tasks for expansion testing
|
||||
logger.info('Setting up test tasks...');
|
||||
|
||||
// Create simple task for expansion
|
||||
const simpleResult = await helpers.taskMaster(
|
||||
@@ -36,7 +47,7 @@ export default async function testExpandTask(logger, helpers, context) {
|
||||
['--prompt', 'Create a user authentication system'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
const simpleTaskId = helpers.extractTaskId(simpleResult.stdout);
|
||||
simpleTaskId = helpers.extractTaskId(simpleResult.stdout);
|
||||
|
||||
// Create complex task for expansion
|
||||
const complexResult = await helpers.taskMaster(
|
||||
@@ -44,449 +55,302 @@ export default async function testExpandTask(logger, helpers, context) {
|
||||
['--prompt', 'Build a full-stack web application with React frontend and Node.js backend'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
const complexTaskId = helpers.extractTaskId(complexResult.stdout);
|
||||
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 task needs to be broken down into subtasks'],
|
||||
['--title', 'Manual task for expansion', '--description', 'This is a manually created task'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
const manualTaskId = helpers.extractTaskId(manualResult.stdout);
|
||||
manualTaskId = helpers.extractTaskId(manualResult.stdout);
|
||||
});
|
||||
|
||||
// Test 1: Single task expansion
|
||||
await runTest('Single task expansion', async () => {
|
||||
afterEach(() => {
|
||||
// Clean up test directory
|
||||
if (testDir && existsSync(testDir)) {
|
||||
rmSync(testDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
describe('Single task expansion', () => {
|
||||
it('should expand a single task', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'expand',
|
||||
[simpleTaskId],
|
||||
{ cwd: testDir, timeout: 120000 }
|
||||
['--id', simpleTaskId],
|
||||
{ cwd: testDir, timeout: 45000 }
|
||||
);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Command failed: ${result.stderr}`);
|
||||
}
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('Expanded');
|
||||
|
||||
// Verify subtasks were created
|
||||
const showResult = await helpers.taskMaster('show', [simpleTaskId], { cwd: testDir });
|
||||
if (!showResult.stdout.includes('Subtasks:') && !showResult.stdout.includes('.1')) {
|
||||
throw new Error('No subtasks created during expansion');
|
||||
}
|
||||
|
||||
// Check expansion output mentions subtasks
|
||||
if (!result.stdout.includes('subtask') && !result.stdout.includes('expanded')) {
|
||||
throw new Error('Expansion output does not mention subtasks');
|
||||
}
|
||||
});
|
||||
expect(showResult.stdout).toContain('Subtasks:');
|
||||
}, 60000);
|
||||
|
||||
// Test 2: Expansion of already expanded task (should skip)
|
||||
await runTest('Expansion of already expanded task', async () => {
|
||||
it('should expand with custom number of subtasks', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'expand',
|
||||
[simpleTaskId],
|
||||
{ cwd: testDir }
|
||||
['--id', complexTaskId, '--num', '3'],
|
||||
{ cwd: testDir, timeout: 45000 }
|
||||
);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Command failed: ${result.stderr}`);
|
||||
}
|
||||
|
||||
// Should indicate task is already expanded
|
||||
if (!result.stdout.includes('already') && !result.stdout.includes('skip')) {
|
||||
throw new Error('Did not indicate task was already expanded');
|
||||
}
|
||||
});
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
// Check that we got approximately 3 subtasks
|
||||
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);
|
||||
expect(subtaskMatches.length).toBeLessThanOrEqual(5);
|
||||
}, 60000);
|
||||
|
||||
// Test 3: Force re-expansion with --force
|
||||
await runTest('Force re-expansion', async () => {
|
||||
// Get initial subtask count
|
||||
const beforeShow = await helpers.taskMaster('show', [simpleTaskId], { cwd: testDir });
|
||||
const beforeSubtasks = (beforeShow.stdout.match(/\d+\.\d+/g) || []).length;
|
||||
|
||||
it('should expand with research mode', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'expand',
|
||||
[simpleTaskId, '--force'],
|
||||
{ cwd: testDir, timeout: 120000 }
|
||||
['--id', simpleTaskId, '--research'],
|
||||
{ cwd: testDir, timeout: 60000 }
|
||||
);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Command failed: ${result.stderr}`);
|
||||
}
|
||||
|
||||
// Verify it actually re-expanded
|
||||
if (!result.stdout.includes('expanded') && !result.stdout.includes('Re-expand')) {
|
||||
throw new Error('Force flag did not trigger re-expansion');
|
||||
}
|
||||
|
||||
// Check if subtasks changed (they might be different)
|
||||
const afterShow = await helpers.taskMaster('show', [simpleTaskId], { cwd: testDir });
|
||||
const afterSubtasks = (afterShow.stdout.match(/\d+\.\d+/g) || []).length;
|
||||
|
||||
if (afterSubtasks === 0) {
|
||||
throw new Error('Force re-expansion removed all subtasks');
|
||||
}
|
||||
});
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('research');
|
||||
}, 90000);
|
||||
|
||||
// Test 4: Expand multiple tasks
|
||||
await runTest('Expand multiple tasks', async () => {
|
||||
it('should expand with additional context', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'expand',
|
||||
[complexTaskId, manualTaskId],
|
||||
{ cwd: testDir, timeout: 180000 }
|
||||
['--id', manualTaskId, '--prompt', 'Focus on security best practices and testing'],
|
||||
{ cwd: testDir, timeout: 45000 }
|
||||
);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Command failed: ${result.stderr}`);
|
||||
}
|
||||
|
||||
// Verify both tasks were expanded
|
||||
const showComplex = await helpers.taskMaster('show', [complexTaskId], { cwd: testDir });
|
||||
const showManual = await helpers.taskMaster('show', [manualTaskId], { cwd: testDir });
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
if (!showComplex.stdout.includes('Subtasks:')) {
|
||||
throw new Error('Complex task was not expanded');
|
||||
}
|
||||
if (!showManual.stdout.includes('Subtasks:')) {
|
||||
throw new Error('Manual task was not expanded');
|
||||
}
|
||||
});
|
||||
// Verify context was used
|
||||
const showResult = await helpers.taskMaster('show', [manualTaskId], { cwd: testDir });
|
||||
const outputLower = showResult.stdout.toLowerCase();
|
||||
expect(outputLower).toMatch(/security|test/);
|
||||
}, 60000);
|
||||
});
|
||||
|
||||
// Test 5: Expand all tasks with --all
|
||||
await runTest('Expand all tasks', async () => {
|
||||
// Create a few more tasks
|
||||
await helpers.taskMaster('add-task', ['--prompt', 'Task A for expand all'], { cwd: testDir });
|
||||
await helpers.taskMaster('add-task', ['--prompt', 'Task B for expand all'], { cwd: testDir });
|
||||
|
||||
describe('Bulk expansion', () => {
|
||||
it('should expand all tasks', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'expand',
|
||||
['--all'],
|
||||
{ cwd: testDir, timeout: 240000 }
|
||||
{ cwd: testDir, timeout: 120000 }
|
||||
);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Command failed: ${result.stderr}`);
|
||||
}
|
||||
|
||||
// Should mention expanding multiple tasks
|
||||
if (!result.stdout.includes('Expand') || !result.stdout.includes('all')) {
|
||||
throw new Error('Expand all did not indicate it was processing all tasks');
|
||||
}
|
||||
});
|
||||
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);
|
||||
expect(tasksWithSubtasks.length).toBeGreaterThanOrEqual(2);
|
||||
}, 150000);
|
||||
|
||||
// Test 6: Error handling - invalid task ID
|
||||
await runTest('Error handling - invalid task ID', async () => {
|
||||
it('should expand all with force flag', async () => {
|
||||
// First expand one task
|
||||
await helpers.taskMaster(
|
||||
'expand',
|
||||
['--id', simpleTaskId],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
// Then expand all with force
|
||||
const result = await helpers.taskMaster(
|
||||
'expand',
|
||||
['99999'],
|
||||
['--all', '--force'],
|
||||
{ cwd: testDir, timeout: 120000 }
|
||||
);
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('force');
|
||||
}, 150000);
|
||||
});
|
||||
|
||||
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 }
|
||||
);
|
||||
|
||||
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 });
|
||||
|
||||
expect(showResult2.stdout).toContain('Subtasks:');
|
||||
expect(showResult3.stdout).toContain('Subtasks:');
|
||||
expect(showResult4.stdout).toContain('Subtasks:');
|
||||
}, 120000);
|
||||
|
||||
it('should expand specific task IDs', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'expand',
|
||||
['--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 });
|
||||
|
||||
expect(showResult1.stdout).toContain('Subtasks:');
|
||||
expect(showResult2.stdout).toContain('Subtasks:');
|
||||
}, 120000);
|
||||
});
|
||||
|
||||
describe('Error handling', () => {
|
||||
it('should fail for non-existent task ID', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'expand',
|
||||
['--id', '99999'],
|
||||
{ cwd: testDir, allowFailure: true }
|
||||
);
|
||||
if (result.exitCode === 0) {
|
||||
throw new Error('Should have failed with invalid task ID');
|
||||
}
|
||||
if (!result.stderr.includes('not found') && !result.stderr.includes('invalid')) {
|
||||
throw new Error('Error message does not indicate task not found');
|
||||
}
|
||||
|
||||
expect(result.exitCode).not.toBe(0);
|
||||
expect(result.stderr).toContain('not found');
|
||||
});
|
||||
|
||||
// Test 7: Expansion quality verification
|
||||
await runTest('Expansion quality - relevant subtasks', async () => {
|
||||
// Create a specific task
|
||||
const specificResult = await helpers.taskMaster(
|
||||
'add-task',
|
||||
['--prompt', 'Implement user login with email and password'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
const specificTaskId = helpers.extractTaskId(specificResult.stdout);
|
||||
|
||||
// Expand it
|
||||
await helpers.taskMaster('expand', [specificTaskId], { cwd: testDir, timeout: 120000 });
|
||||
|
||||
// Check subtasks are relevant
|
||||
const showResult = await helpers.taskMaster('show', [specificTaskId], { cwd: testDir });
|
||||
const subtaskText = showResult.stdout.toLowerCase();
|
||||
|
||||
// Should have subtasks related to login functionality
|
||||
const relevantKeywords = ['email', 'password', 'validation', 'auth', 'login', 'user', 'security'];
|
||||
const foundKeywords = relevantKeywords.filter(keyword => subtaskText.includes(keyword));
|
||||
|
||||
if (foundKeywords.length < 3) {
|
||||
throw new Error('Subtasks do not seem relevant to user login task');
|
||||
}
|
||||
});
|
||||
|
||||
// Test 8: Recursive expansion of subtasks
|
||||
await runTest('Recursive expansion with --recursive', async () => {
|
||||
// Create task for recursive expansion
|
||||
const recursiveResult = await helpers.taskMaster(
|
||||
'add-task',
|
||||
['--prompt', 'Build a complete project management system'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
const recursiveTaskId = helpers.extractTaskId(recursiveResult.stdout);
|
||||
|
||||
// First expand the main task
|
||||
await helpers.taskMaster('expand', [recursiveTaskId], { cwd: testDir, timeout: 120000 });
|
||||
|
||||
// Now expand recursively
|
||||
const result = await helpers.taskMaster(
|
||||
'expand',
|
||||
[recursiveTaskId, '--recursive'],
|
||||
{ cwd: testDir, timeout: 180000 }
|
||||
);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Command failed: ${result.stderr}`);
|
||||
}
|
||||
|
||||
// Check for nested subtasks (e.g., 1.1.1)
|
||||
const showResult = await helpers.taskMaster('show', [recursiveTaskId], { cwd: testDir });
|
||||
if (!showResult.stdout.match(/\d+\.\d+\.\d+/)) {
|
||||
throw new Error('Recursive expansion did not create nested subtasks');
|
||||
}
|
||||
});
|
||||
|
||||
// Test 9: Expand with depth limit
|
||||
await runTest('Expand with depth limit', async () => {
|
||||
// Create task for depth testing
|
||||
const depthResult = await helpers.taskMaster(
|
||||
'add-task',
|
||||
['--prompt', 'Create a mobile application'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
const depthTaskId = helpers.extractTaskId(depthResult.stdout);
|
||||
|
||||
const result = await helpers.taskMaster(
|
||||
'expand',
|
||||
[depthTaskId, '--depth', '2'],
|
||||
{ cwd: testDir, timeout: 180000 }
|
||||
);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Command failed: ${result.stderr}`);
|
||||
}
|
||||
|
||||
// Should have subtasks but not too deep
|
||||
const showResult = await helpers.taskMaster('show', [depthTaskId], { cwd: testDir });
|
||||
const hasLevel1 = showResult.stdout.match(/\d+\.1/);
|
||||
const hasLevel2 = showResult.stdout.match(/\d+\.1\.1/);
|
||||
const hasLevel3 = showResult.stdout.match(/\d+\.1\.1\.1/);
|
||||
|
||||
if (!hasLevel1) {
|
||||
throw new Error('No level 1 subtasks created');
|
||||
}
|
||||
if (hasLevel3) {
|
||||
throw new Error('Depth limit not respected - found level 3 subtasks');
|
||||
}
|
||||
});
|
||||
|
||||
// Test 10: Expand task with existing subtasks
|
||||
await runTest('Expand task with manual subtasks', async () => {
|
||||
// Create task and add manual subtask
|
||||
const mixedResult = await helpers.taskMaster(
|
||||
'add-task',
|
||||
['--title', 'Mixed subtasks task'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
const mixedTaskId = helpers.extractTaskId(mixedResult.stdout);
|
||||
|
||||
// Add manual subtask
|
||||
it('should skip already expanded tasks without force', async () => {
|
||||
// First expansion
|
||||
await helpers.taskMaster(
|
||||
'add-subtask',
|
||||
[mixedTaskId, 'Manual subtask 1'],
|
||||
'expand',
|
||||
['--id', simpleTaskId],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
// Now expand with AI
|
||||
// Second expansion without force
|
||||
const result = await helpers.taskMaster(
|
||||
'expand',
|
||||
[mixedTaskId],
|
||||
{ cwd: testDir, timeout: 120000 }
|
||||
);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Command failed: ${result.stderr}`);
|
||||
}
|
||||
|
||||
// Should preserve manual subtask and add AI ones
|
||||
const showResult = await helpers.taskMaster('show', [mixedTaskId], { cwd: testDir });
|
||||
if (!showResult.stdout.includes('Manual subtask 1')) {
|
||||
throw new Error('Manual subtask was removed during expansion');
|
||||
}
|
||||
|
||||
// Count total subtasks - should be more than 1
|
||||
const subtaskCount = (showResult.stdout.match(/\d+\.\d+/g) || []).length;
|
||||
if (subtaskCount <= 1) {
|
||||
throw new Error('AI did not add additional subtasks');
|
||||
}
|
||||
});
|
||||
|
||||
// Test 11: Expand with custom prompt
|
||||
await runTest('Expand with custom prompt', async () => {
|
||||
// Create task
|
||||
const customResult = await helpers.taskMaster(
|
||||
'add-task',
|
||||
['--title', 'Generic development task'],
|
||||
['--id', simpleTaskId],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
const customTaskId = helpers.extractTaskId(customResult.stdout);
|
||||
|
||||
// Expand with custom instructions
|
||||
const result = await helpers.taskMaster(
|
||||
'expand',
|
||||
[customTaskId, '--prompt', 'Break this down focusing on security aspects'],
|
||||
{ cwd: testDir, timeout: 120000 }
|
||||
);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Command failed: ${result.stderr}`);
|
||||
}
|
||||
|
||||
// Verify subtasks focus on security
|
||||
const showResult = await helpers.taskMaster('show', [customTaskId], { cwd: testDir });
|
||||
const subtaskText = showResult.stdout.toLowerCase();
|
||||
|
||||
if (!subtaskText.includes('security') && !subtaskText.includes('secure') &&
|
||||
!subtaskText.includes('auth') && !subtaskText.includes('protect')) {
|
||||
throw new Error('Custom prompt did not influence subtask generation');
|
||||
}
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout.toLowerCase()).toMatch(/already|skip/);
|
||||
});
|
||||
|
||||
// Test 12: Performance - expand large task
|
||||
await runTest('Performance - expand complex task', async () => {
|
||||
const perfResult = await helpers.taskMaster(
|
||||
'add-task',
|
||||
['--prompt', 'Build a complete enterprise resource planning (ERP) system with all modules'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
const perfTaskId = helpers.extractTaskId(perfResult.stdout);
|
||||
|
||||
const startTime = Date.now();
|
||||
it('should handle invalid number of subtasks', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'expand',
|
||||
[perfTaskId],
|
||||
{ cwd: testDir, timeout: 180000 }
|
||||
['--id', simpleTaskId, '--num', '-1'],
|
||||
{ cwd: testDir, allowFailure: true }
|
||||
);
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Command failed: ${result.stderr}`);
|
||||
}
|
||||
|
||||
logger.info(`Complex task expanded in ${duration}ms`);
|
||||
|
||||
// Should create many subtasks for complex task
|
||||
const showResult = await helpers.taskMaster('show', [perfTaskId], { cwd: testDir });
|
||||
const subtaskCount = (showResult.stdout.match(/\d+\.\d+/g) || []).length;
|
||||
|
||||
if (subtaskCount < 5) {
|
||||
throw new Error('Complex task should have generated more subtasks');
|
||||
}
|
||||
logger.info(`Generated ${subtaskCount} subtasks`);
|
||||
expect(result.exitCode).not.toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
// Test 13: Expand with tag context
|
||||
await runTest('Expand within tag context', async () => {
|
||||
// Create tag and task
|
||||
await helpers.taskMaster('add-tag', ['frontend-expansion'], { cwd: testDir });
|
||||
describe('Tag support', () => {
|
||||
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', 'Create UI components', '--tag', 'frontend-expansion'],
|
||||
['--prompt', 'Tagged task for expansion', '--tag', 'feature-tag'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
const taggedTaskId = helpers.extractTaskId(taggedResult.stdout);
|
||||
const taggedId = helpers.extractTaskId(taggedResult.stdout);
|
||||
|
||||
// Expand within tag context
|
||||
const result = await helpers.taskMaster(
|
||||
'expand',
|
||||
[taggedTaskId, '--tag', 'frontend-expansion'],
|
||||
{ cwd: testDir, timeout: 120000 }
|
||||
['--id', taggedId, '--tag', 'feature-tag'],
|
||||
{ cwd: testDir, timeout: 45000 }
|
||||
);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Command failed: ${result.stderr}`);
|
||||
}
|
||||
|
||||
// Verify subtasks inherit tag
|
||||
const listResult = await helpers.taskMaster(
|
||||
'list',
|
||||
['--tag', 'frontend-expansion'],
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
// Verify expansion in correct tag
|
||||
const showResult = await helpers.taskMaster(
|
||||
'show',
|
||||
[taggedId, '--tag', 'feature-tag'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
expect(showResult.stdout).toContain('Subtasks:');
|
||||
}, 60000);
|
||||
});
|
||||
|
||||
describe('Model configuration', () => {
|
||||
it('should use specified model for expansion', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'expand',
|
||||
['--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 }
|
||||
);
|
||||
|
||||
// Should show parent and subtasks in tag
|
||||
const taskMatches = listResult.stdout.match(/\d+(\.\d+)*/g) || [];
|
||||
if (taskMatches.length <= 1) {
|
||||
throw new Error('Subtasks did not inherit tag context');
|
||||
}
|
||||
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));
|
||||
|
||||
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}`);
|
||||
expect(subtask.title).toBeTruthy();
|
||||
expect(subtask.description).toBeTruthy();
|
||||
expect(subtask.status).toBe('pending');
|
||||
});
|
||||
});
|
||||
|
||||
// Test 14: Expand completed task
|
||||
await runTest('Expand completed task', async () => {
|
||||
// Create and complete a task
|
||||
const completedResult = await helpers.taskMaster(
|
||||
it('should maintain task dependencies after expansion', async () => {
|
||||
// Create task with dependency
|
||||
const depResult = await helpers.taskMaster(
|
||||
'add-task',
|
||||
['--title', 'Completed task'],
|
||||
['--prompt', 'Dependent task', '--dependencies', simpleTaskId],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
const completedTaskId = helpers.extractTaskId(completedResult.stdout);
|
||||
await helpers.taskMaster('set-status', [completedTaskId, 'completed'], { cwd: testDir });
|
||||
const depTaskId = helpers.extractTaskId(depResult.stdout);
|
||||
|
||||
// Try to expand
|
||||
const result = await helpers.taskMaster(
|
||||
// Expand the task
|
||||
await helpers.taskMaster(
|
||||
'expand',
|
||||
[completedTaskId],
|
||||
{ cwd: testDir, allowFailure: true }
|
||||
['--id', depTaskId],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
// Should either fail or warn about completed status
|
||||
if (result.exitCode === 0 && !result.stdout.includes('completed') && !result.stdout.includes('warning')) {
|
||||
throw new Error('No warning about expanding completed task');
|
||||
}
|
||||
// Check dependencies are preserved
|
||||
const showResult = await helpers.taskMaster('show', [depTaskId], { cwd: testDir });
|
||||
expect(showResult.stdout).toContain(`Dependencies: ${simpleTaskId}`);
|
||||
});
|
||||
|
||||
// Test 15: Batch expansion with mixed results
|
||||
await runTest('Batch expansion with mixed results', async () => {
|
||||
// Create tasks in different states
|
||||
const task1 = await helpers.taskMaster('add-task', ['--prompt', 'New task 1'], { cwd: testDir });
|
||||
const taskId1 = helpers.extractTaskId(task1.stdout);
|
||||
|
||||
const task2 = await helpers.taskMaster('add-task', ['--prompt', 'New task 2'], { cwd: testDir });
|
||||
const taskId2 = helpers.extractTaskId(task2.stdout);
|
||||
|
||||
// Expand task2 first
|
||||
await helpers.taskMaster('expand', [taskId2], { cwd: testDir });
|
||||
|
||||
// Now expand both - should skip task2
|
||||
const result = await helpers.taskMaster(
|
||||
'expand',
|
||||
[taskId1, taskId2],
|
||||
{ cwd: testDir, timeout: 180000 }
|
||||
);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Command failed: ${result.stderr}`);
|
||||
}
|
||||
|
||||
// Should indicate one was skipped
|
||||
if (!result.stdout.includes('skip') || !result.stdout.includes('already')) {
|
||||
throw new Error('Did not indicate that already-expanded task was skipped');
|
||||
}
|
||||
});
|
||||
|
||||
// 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;
|
||||
|
||||
logger.info('\n=== Expand-Task Test Summary ===');
|
||||
logger.info(`Total tests: ${totalTests}`);
|
||||
logger.info(`Passed: ${passedTests}`);
|
||||
logger.info(`Failed: ${failedTests}`);
|
||||
|
||||
if (failedTests > 0) {
|
||||
results.status = 'failed';
|
||||
logger.error(`\n${failedTests} tests failed`);
|
||||
} else {
|
||||
logger.success('\n✅ All expand-task tests passed!');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
results.status = 'failed';
|
||||
results.errors.push({
|
||||
test: 'expand-task test suite',
|
||||
error: error.message,
|
||||
stack: error.stack
|
||||
});
|
||||
logger.error(`Expand-task test suite failed: ${error.message}`);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
});
|
||||
});
|
||||
109
tests/e2e/utils/logger.cjs
Normal file
109
tests/e2e/utils/logger.cjs
Normal file
@@ -0,0 +1,109 @@
|
||||
// Simple console colors fallback if chalk is not available
|
||||
const colors = {
|
||||
green: (text) => `\x1b[32m${text}\x1b[0m`,
|
||||
red: (text) => `\x1b[31m${text}\x1b[0m`,
|
||||
yellow: (text) => `\x1b[33m${text}\x1b[0m`,
|
||||
blue: (text) => `\x1b[34m${text}\x1b[0m`,
|
||||
cyan: (text) => `\x1b[36m${text}\x1b[0m`,
|
||||
gray: (text) => `\x1b[90m${text}\x1b[0m`
|
||||
};
|
||||
|
||||
class TestLogger {
|
||||
constructor(testName = 'test') {
|
||||
this.testName = testName;
|
||||
this.startTime = Date.now();
|
||||
this.stepCount = 0;
|
||||
this.logBuffer = [];
|
||||
this.totalCost = 0;
|
||||
}
|
||||
|
||||
_formatMessage(level, message, options = {}) {
|
||||
const timestamp = new Date().toISOString();
|
||||
const elapsed = ((Date.now() - this.startTime) / 1000).toFixed(2);
|
||||
const formattedMessage = `[${timestamp}] [${elapsed}s] [${level}] ${message}`;
|
||||
|
||||
// Add to buffer for later saving if needed
|
||||
this.logBuffer.push(formattedMessage);
|
||||
|
||||
return formattedMessage;
|
||||
}
|
||||
|
||||
_log(level, message, color) {
|
||||
const formatted = this._formatMessage(level, message);
|
||||
|
||||
if (process.env.E2E_VERBOSE !== 'false') {
|
||||
console.log(color ? color(formatted) : formatted);
|
||||
}
|
||||
}
|
||||
|
||||
info(message) {
|
||||
this._log('INFO', message, colors.blue);
|
||||
}
|
||||
|
||||
success(message) {
|
||||
this._log('SUCCESS', message, colors.green);
|
||||
}
|
||||
|
||||
error(message) {
|
||||
this._log('ERROR', message, colors.red);
|
||||
}
|
||||
|
||||
warning(message) {
|
||||
this._log('WARNING', message, colors.yellow);
|
||||
}
|
||||
|
||||
step(message) {
|
||||
this.stepCount++;
|
||||
this._log('STEP', `Step ${this.stepCount}: ${message}`, colors.cyan);
|
||||
}
|
||||
|
||||
debug(message) {
|
||||
if (process.env.DEBUG) {
|
||||
this._log('DEBUG', message, colors.gray);
|
||||
}
|
||||
}
|
||||
|
||||
flush() {
|
||||
// In CommonJS version, we'll just clear the buffer
|
||||
// Real implementation would write to file if needed
|
||||
this.logBuffer = [];
|
||||
}
|
||||
|
||||
summary() {
|
||||
const duration = ((Date.now() - this.startTime) / 1000).toFixed(2);
|
||||
const summary = `Test completed in ${duration}s`;
|
||||
this.info(summary);
|
||||
return {
|
||||
duration: parseFloat(duration),
|
||||
steps: this.stepCount,
|
||||
totalCost: this.totalCost
|
||||
};
|
||||
}
|
||||
|
||||
extractAndAddCost(output) {
|
||||
// Extract cost information from LLM output
|
||||
const costPatterns = [
|
||||
/Total Cost: \$?([\d.]+)/i,
|
||||
/Cost: \$?([\d.]+)/i,
|
||||
/Estimated cost: \$?([\d.]+)/i
|
||||
];
|
||||
|
||||
for (const pattern of costPatterns) {
|
||||
const match = output.match(pattern);
|
||||
if (match) {
|
||||
const cost = parseFloat(match[1]);
|
||||
this.totalCost += cost;
|
||||
this.debug(
|
||||
`Added cost: $${cost} (Total: $${this.totalCost.toFixed(4)})`
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getTotalCost() {
|
||||
return this.totalCost;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { TestLogger };
|
||||
246
tests/e2e/utils/test-helpers.cjs
Normal file
246
tests/e2e/utils/test-helpers.cjs
Normal file
@@ -0,0 +1,246 @@
|
||||
const { spawn } = require('child_process');
|
||||
const { readFileSync, existsSync, copyFileSync, writeFileSync, readdirSync } = require('fs');
|
||||
const { join } = require('path');
|
||||
|
||||
class TestHelpers {
|
||||
constructor(logger) {
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a command and return output
|
||||
* @param {string} command - Command to execute
|
||||
* @param {string[]} args - Command arguments
|
||||
* @param {Object} options - Execution options
|
||||
* @returns {Promise<{stdout: string, stderr: string, exitCode: number}>}
|
||||
*/
|
||||
async executeCommand(command, args = [], options = {}) {
|
||||
return new Promise((resolve) => {
|
||||
const spawnOptions = {
|
||||
cwd: options.cwd || process.cwd(),
|
||||
env: { ...process.env, ...options.env },
|
||||
shell: true
|
||||
};
|
||||
|
||||
// When using shell: true, pass the full command as a single string
|
||||
const fullCommand =
|
||||
args.length > 0 ? `${command} ${args.join(' ')}` : command;
|
||||
const child = spawn(fullCommand, [], spawnOptions);
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
|
||||
child.stdout.on('data', (data) => {
|
||||
stdout += data.toString();
|
||||
});
|
||||
|
||||
child.stderr.on('data', (data) => {
|
||||
stderr += data.toString();
|
||||
});
|
||||
|
||||
child.on('close', (exitCode) => {
|
||||
const output = stdout + stderr;
|
||||
|
||||
// Extract and log costs
|
||||
this.logger.extractAndAddCost(output);
|
||||
|
||||
resolve({ stdout, stderr, exitCode });
|
||||
});
|
||||
|
||||
// Handle timeout
|
||||
if (options.timeout) {
|
||||
setTimeout(() => {
|
||||
child.kill('SIGTERM');
|
||||
}, options.timeout);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute task-master command
|
||||
* @param {string} subcommand - Task-master subcommand
|
||||
* @param {string[]} args - Command arguments
|
||||
* @param {Object} options - Execution options
|
||||
*/
|
||||
async taskMaster(subcommand, args = [], options = {}) {
|
||||
const fullArgs = [subcommand, ...args];
|
||||
this.logger.info(`Executing: task-master ${fullArgs.join(' ')}`);
|
||||
|
||||
const result = await this.executeCommand('task-master', fullArgs, options);
|
||||
|
||||
if (result.exitCode !== 0 && !options.allowFailure) {
|
||||
this.logger.error(`Command failed with exit code ${result.exitCode}`);
|
||||
this.logger.error(`stderr: ${result.stderr}`);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a file exists
|
||||
*/
|
||||
fileExists(filePath) {
|
||||
return existsSync(filePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read JSON file
|
||||
*/
|
||||
readJson(filePath) {
|
||||
try {
|
||||
const content = readFileSync(filePath, 'utf8');
|
||||
return JSON.parse(content);
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to read JSON file ${filePath}: ${error.message}`
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy file
|
||||
*/
|
||||
copyFile(source, destination) {
|
||||
try {
|
||||
copyFileSync(source, destination);
|
||||
return true;
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to copy file from ${source} to ${destination}: ${error.message}`
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write file
|
||||
*/
|
||||
writeFile(filePath, content) {
|
||||
try {
|
||||
writeFileSync(filePath, content, 'utf8');
|
||||
return true;
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to write file ${filePath}: ${error.message}`
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read file
|
||||
*/
|
||||
readFile(filePath) {
|
||||
try {
|
||||
return readFileSync(filePath, 'utf8');
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to read file ${filePath}: ${error.message}`
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* List files in directory
|
||||
*/
|
||||
listFiles(dirPath) {
|
||||
try {
|
||||
return readdirSync(dirPath);
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to list files in ${dirPath}: ${error.message}`
|
||||
);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for a specified duration
|
||||
*/
|
||||
async wait(milliseconds) {
|
||||
return new Promise((resolve) => setTimeout(resolve, milliseconds));
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify task exists in tasks.json
|
||||
*/
|
||||
verifyTaskExists(tasksFile, taskId, tagName = 'master') {
|
||||
const tasks = this.readJson(tasksFile);
|
||||
if (!tasks || !tasks[tagName]) return false;
|
||||
|
||||
return tasks[tagName].tasks.some((task) => task.id === taskId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get task count for a tag
|
||||
*/
|
||||
getTaskCount(tasksFile, tagName = 'master') {
|
||||
const tasks = this.readJson(tasksFile);
|
||||
if (!tasks || !tasks[tagName]) return 0;
|
||||
|
||||
return tasks[tagName].tasks.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract task ID from command output
|
||||
*/
|
||||
extractTaskId(output) {
|
||||
// First try to match the new numbered format (#123)
|
||||
const numberedMatch = output.match(/#(\d+(?:\.\d+)?)/);
|
||||
if (numberedMatch) {
|
||||
return numberedMatch[1];
|
||||
}
|
||||
|
||||
// Fallback to older patterns
|
||||
const patterns = [
|
||||
/✓ Added new task #(\d+(?:\.\d+)?)/,
|
||||
/✅ New task created successfully:.*?(\d+(?:\.\d+)?)/,
|
||||
/Task (\d+(?:\.\d+)?) Created Successfully/,
|
||||
/Task created with ID: (\d+(?:\.\d+)?)/,
|
||||
/Created task (\d+(?:\.\d+)?)/
|
||||
];
|
||||
|
||||
for (const pattern of patterns) {
|
||||
const match = output.match(pattern);
|
||||
if (match) {
|
||||
return match[1];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run multiple async operations in parallel
|
||||
*/
|
||||
async runParallel(operations) {
|
||||
return Promise.all(operations);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run operations with concurrency limit
|
||||
*/
|
||||
async runWithConcurrency(operations, limit = 3) {
|
||||
const results = [];
|
||||
const executing = [];
|
||||
|
||||
for (const operation of operations) {
|
||||
const promise = operation().then((result) => {
|
||||
executing.splice(executing.indexOf(promise), 1);
|
||||
return result;
|
||||
});
|
||||
|
||||
results.push(promise);
|
||||
executing.push(promise);
|
||||
|
||||
if (executing.length >= limit) {
|
||||
await Promise.race(executing);
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.all(results);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { TestHelpers };
|
||||
Reference in New Issue
Block a user