feat: more e2e
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -3,422 +3,488 @@
|
||||
* Tests all aspects of AI-powered research functionality
|
||||
*/
|
||||
|
||||
export default async function testResearch(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('research command', () => {
|
||||
let testDir;
|
||||
let helpers;
|
||||
|
||||
beforeEach(async () => {
|
||||
// Create test directory
|
||||
testDir = mkdtempSync(join(tmpdir(), 'task-master-research-'));
|
||||
|
||||
// Initialize test helpers
|
||||
const context = global.createTestContext('research');
|
||||
helpers = context.helpers;
|
||||
|
||||
// Copy .env file if it exists
|
||||
const mainEnvPath = join(__dirname, '../../../../.env');
|
||||
const testEnvPath = join(testDir, '.env');
|
||||
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 research tests...');
|
||||
afterEach(() => {
|
||||
// Clean up test directory
|
||||
if (testDir && existsSync(testDir)) {
|
||||
rmSync(testDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
// Test 1: Basic research on a topic
|
||||
await runTest('Basic research query', async () => {
|
||||
describe('Basic research functionality', () => {
|
||||
it('should perform research on a topic', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'research',
|
||||
['What are the best practices for implementing JWT authentication in Node.js?'],
|
||||
{ cwd: testDir, timeout: 120000 }
|
||||
['What are the best practices for implementing OAuth 2.0 authentication?'],
|
||||
{ cwd: testDir, timeout: 90000 }
|
||||
);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Command failed: ${result.stderr}`);
|
||||
}
|
||||
|
||||
// Check for relevant research output
|
||||
const output = result.stdout.toLowerCase();
|
||||
if (!output.includes('jwt') || !output.includes('authentication')) {
|
||||
throw new Error('Research output does not contain expected keywords');
|
||||
}
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('Research Results');
|
||||
|
||||
// Should provide actionable information
|
||||
const hasActionableInfo = output.includes('implement') ||
|
||||
output.includes('use') ||
|
||||
output.includes('practice') ||
|
||||
output.includes('security');
|
||||
if (!hasActionableInfo) {
|
||||
throw new Error('Research output lacks actionable information');
|
||||
}
|
||||
});
|
||||
// Should contain relevant OAuth information
|
||||
const hasOAuthInfo = result.stdout.toLowerCase().includes('oauth') ||
|
||||
result.stdout.toLowerCase().includes('authentication');
|
||||
expect(hasOAuthInfo).toBe(true);
|
||||
}, 120000);
|
||||
|
||||
// Test 2: Research with specific context
|
||||
await runTest('Research with project context', async () => {
|
||||
// Create a task to provide context
|
||||
const taskResult = await helpers.taskMaster(
|
||||
'add-task',
|
||||
['--title', 'Implement user authentication', '--description', 'Need to add secure login to our Express.js API'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
const taskId = helpers.extractTaskId(taskResult.stdout);
|
||||
|
||||
it('should research using --topic flag', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'research',
|
||||
['--task', taskId, 'Compare bcrypt vs argon2 for password hashing'],
|
||||
{ cwd: testDir, timeout: 120000 }
|
||||
['--topic', 'React performance optimization techniques'],
|
||||
{ cwd: testDir, timeout: 90000 }
|
||||
);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Command failed: ${result.stderr}`);
|
||||
}
|
||||
|
||||
// Should mention both technologies
|
||||
const output = result.stdout.toLowerCase();
|
||||
if (!output.includes('bcrypt') || !output.includes('argon2')) {
|
||||
throw new Error('Research did not compare both technologies');
|
||||
}
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('Research Results');
|
||||
|
||||
// Should relate to the task context
|
||||
if (!output.includes('password') || !output.includes('hash')) {
|
||||
throw new Error('Research not relevant to password hashing');
|
||||
}
|
||||
});
|
||||
// Should contain React-related information
|
||||
const hasReactInfo = result.stdout.toLowerCase().includes('react') ||
|
||||
result.stdout.toLowerCase().includes('performance');
|
||||
expect(hasReactInfo).toBe(true);
|
||||
}, 120000);
|
||||
|
||||
// Test 3: Research with output format options
|
||||
await runTest('Research with markdown output', async () => {
|
||||
it('should handle technical research queries', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'research',
|
||||
['--format', 'markdown', 'How to implement rate limiting in REST APIs?'],
|
||||
{ cwd: testDir, timeout: 120000 }
|
||||
['Compare PostgreSQL vs MongoDB for a real-time analytics application'],
|
||||
{ cwd: testDir, timeout: 90000 }
|
||||
);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Command failed: ${result.stderr}`);
|
||||
}
|
||||
|
||||
// Check for markdown formatting
|
||||
const hasMarkdown = result.stdout.includes('#') ||
|
||||
result.stdout.includes('*') ||
|
||||
result.stdout.includes('-') ||
|
||||
result.stdout.includes('```');
|
||||
if (!hasMarkdown) {
|
||||
throw new Error('Output does not appear to be in markdown format');
|
||||
}
|
||||
});
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
// Should contain database comparison
|
||||
const hasDatabaseInfo = result.stdout.toLowerCase().includes('postgresql') ||
|
||||
result.stdout.toLowerCase().includes('mongodb');
|
||||
expect(hasDatabaseInfo).toBe(true);
|
||||
}, 120000);
|
||||
});
|
||||
|
||||
// Test 4: Research with depth parameter
|
||||
await runTest('Research with depth control', async () => {
|
||||
const shallowResult = await helpers.taskMaster(
|
||||
'research',
|
||||
['--depth', 'shallow', 'React state management options'],
|
||||
{ cwd: testDir, timeout: 120000 }
|
||||
);
|
||||
|
||||
const deepResult = await helpers.taskMaster(
|
||||
'research',
|
||||
['--depth', 'deep', 'React state management options'],
|
||||
{ cwd: testDir, timeout: 180000 }
|
||||
);
|
||||
|
||||
if (shallowResult.exitCode !== 0 || deepResult.exitCode !== 0) {
|
||||
throw new Error('Research with depth parameter failed');
|
||||
}
|
||||
|
||||
// Deep research should provide more content
|
||||
if (deepResult.stdout.length <= shallowResult.stdout.length) {
|
||||
throw new Error('Deep research did not provide more detailed information');
|
||||
}
|
||||
|
||||
// Both should mention state management solutions
|
||||
const solutions = ['redux', 'context', 'mobx', 'zustand', 'recoil'];
|
||||
const shallowMentions = solutions.filter(s => shallowResult.stdout.toLowerCase().includes(s)).length;
|
||||
const deepMentions = solutions.filter(s => deepResult.stdout.toLowerCase().includes(s)).length;
|
||||
|
||||
if (deepMentions <= shallowMentions) {
|
||||
throw new Error('Deep research should cover more solutions');
|
||||
}
|
||||
});
|
||||
|
||||
// Test 5: Research for multiple tasks
|
||||
await runTest('Research across multiple tasks', async () => {
|
||||
// Create related tasks
|
||||
const task1 = await helpers.taskMaster(
|
||||
'add-task',
|
||||
['--title', 'Setup database connection'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
const taskId1 = helpers.extractTaskId(task1.stdout);
|
||||
|
||||
const task2 = await helpers.taskMaster(
|
||||
'add-task',
|
||||
['--title', 'Implement caching layer'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
const taskId2 = helpers.extractTaskId(task2.stdout);
|
||||
|
||||
const result = await helpers.taskMaster(
|
||||
'research',
|
||||
['--tasks', `${taskId1},${taskId2}`, 'Best practices for database connection pooling and Redis caching'],
|
||||
{ cwd: testDir, timeout: 120000 }
|
||||
);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Command failed: ${result.stderr}`);
|
||||
}
|
||||
|
||||
// Should cover both topics
|
||||
const output = result.stdout.toLowerCase();
|
||||
if (!output.includes('database') || !output.includes('connection')) {
|
||||
throw new Error('Research did not cover database connections');
|
||||
}
|
||||
if (!output.includes('redis') || !output.includes('cach')) {
|
||||
throw new Error('Research did not cover caching');
|
||||
}
|
||||
});
|
||||
|
||||
// Test 6: Research with source preferences
|
||||
await runTest('Research with source preferences', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'research',
|
||||
['--sources', 'official-docs,stackoverflow', 'How to use React hooks effectively?'],
|
||||
{ cwd: testDir, timeout: 120000 }
|
||||
);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Command failed: ${result.stderr}`);
|
||||
}
|
||||
|
||||
// Should focus on practical examples
|
||||
const output = result.stdout.toLowerCase();
|
||||
if (!output.includes('hook') || !output.includes('react')) {
|
||||
throw new Error('Research not relevant to React hooks');
|
||||
}
|
||||
});
|
||||
|
||||
// Test 7: Research with language/framework context
|
||||
await runTest('Research with technology context', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'research',
|
||||
['--context', 'python,django', 'How to optimize database queries?'],
|
||||
{ cwd: testDir, timeout: 120000 }
|
||||
);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Command failed: ${result.stderr}`);
|
||||
}
|
||||
|
||||
// Should be Python/Django specific
|
||||
const output = result.stdout.toLowerCase();
|
||||
if (!output.includes('django') || !output.includes('orm') || !output.includes('queryset')) {
|
||||
throw new Error('Research not specific to Django context');
|
||||
}
|
||||
});
|
||||
|
||||
// Test 8: Research error handling - empty query
|
||||
await runTest('Error handling - empty query', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'research',
|
||||
[''],
|
||||
{ cwd: testDir, allowFailure: true }
|
||||
);
|
||||
if (result.exitCode === 0) {
|
||||
throw new Error('Should have failed with empty query');
|
||||
}
|
||||
});
|
||||
|
||||
// Test 9: Research with time constraints
|
||||
await runTest('Research with recency filter', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'research',
|
||||
['--since', '2023', 'Latest JavaScript features and ES2024 updates'],
|
||||
{ cwd: testDir, timeout: 120000 }
|
||||
);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Command failed: ${result.stderr}`);
|
||||
}
|
||||
|
||||
// Should mention recent features
|
||||
const output = result.stdout.toLowerCase();
|
||||
const recentFeatures = ['es2023', 'es2024', '2023', '2024', 'latest', 'recent'];
|
||||
const mentionsRecent = recentFeatures.some(feature => output.includes(feature));
|
||||
|
||||
if (!mentionsRecent) {
|
||||
throw new Error('Research did not focus on recent information');
|
||||
}
|
||||
});
|
||||
|
||||
// Test 10: Research with comparison request
|
||||
await runTest('Research comparison analysis', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'research',
|
||||
['Compare REST vs GraphQL vs gRPC for microservices communication'],
|
||||
{ cwd: testDir, timeout: 120000 }
|
||||
);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Command failed: ${result.stderr}`);
|
||||
}
|
||||
|
||||
// Should mention all three technologies
|
||||
const output = result.stdout.toLowerCase();
|
||||
if (!output.includes('rest') || !output.includes('graphql') || !output.includes('grpc')) {
|
||||
throw new Error('Research did not compare all three technologies');
|
||||
}
|
||||
|
||||
// Should include pros/cons or comparison points
|
||||
const hasComparison = output.includes('advantage') ||
|
||||
output.includes('disadvantage') ||
|
||||
output.includes('pros') ||
|
||||
output.includes('cons') ||
|
||||
output.includes('better') ||
|
||||
output.includes('when to use');
|
||||
if (!hasComparison) {
|
||||
throw new Error('Research lacks comparative analysis');
|
||||
}
|
||||
});
|
||||
|
||||
// Test 11: Research with code examples request
|
||||
await runTest('Research with code examples', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'research',
|
||||
['--include-examples', 'How to implement a singleton pattern in TypeScript?'],
|
||||
{ cwd: testDir, timeout: 120000 }
|
||||
);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Command failed: ${result.stderr}`);
|
||||
}
|
||||
|
||||
// Should include code blocks
|
||||
if (!result.stdout.includes('```') && !result.stdout.includes('class') && !result.stdout.includes('function')) {
|
||||
throw new Error('Research did not include code examples');
|
||||
}
|
||||
|
||||
// Should be TypeScript specific
|
||||
const output = result.stdout.toLowerCase();
|
||||
if (!output.includes('typescript') && !output.includes('private constructor')) {
|
||||
throw new Error('Examples not specific to TypeScript');
|
||||
}
|
||||
});
|
||||
|
||||
// Test 12: Research for architecture decisions
|
||||
await runTest('Research for architecture decisions', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'research',
|
||||
['--type', 'architecture', 'Microservices vs monolithic architecture for a startup'],
|
||||
{ cwd: testDir, timeout: 120000 }
|
||||
);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Command failed: ${result.stderr}`);
|
||||
}
|
||||
|
||||
// Should provide architectural insights
|
||||
const output = result.stdout.toLowerCase();
|
||||
const archKeywords = ['scalability', 'deployment', 'complexity', 'team size', 'maintenance', 'cost'];
|
||||
const mentionedKeywords = archKeywords.filter(keyword => output.includes(keyword)).length;
|
||||
|
||||
if (mentionedKeywords < 3) {
|
||||
throw new Error('Research lacks architectural considerations');
|
||||
}
|
||||
});
|
||||
|
||||
// Test 13: Research with tag context
|
||||
await runTest('Research within tag context', async () => {
|
||||
// Create tag and tagged tasks
|
||||
await helpers.taskMaster('add-tag', ['security-research'], { cwd: testDir });
|
||||
|
||||
const result = await helpers.taskMaster(
|
||||
'research',
|
||||
['--tag', 'security-research', 'OWASP top 10 vulnerabilities and mitigation strategies'],
|
||||
{ cwd: testDir, timeout: 120000 }
|
||||
);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Command failed: ${result.stderr}`);
|
||||
}
|
||||
|
||||
// Should focus on security
|
||||
const output = result.stdout.toLowerCase();
|
||||
const securityTerms = ['vulnerability', 'security', 'attack', 'protection', 'owasp', 'mitigation'];
|
||||
const mentionedTerms = securityTerms.filter(term => output.includes(term)).length;
|
||||
|
||||
if (mentionedTerms < 4) {
|
||||
throw new Error('Research not focused on security topics');
|
||||
}
|
||||
});
|
||||
|
||||
// Test 14: Research performance with complex query
|
||||
await runTest('Performance - complex research query', async () => {
|
||||
describe('Research depth control', () => {
|
||||
it('should perform quick research with --quick flag', async () => {
|
||||
const startTime = Date.now();
|
||||
const result = await helpers.taskMaster(
|
||||
'research',
|
||||
['Comprehensive guide to building a scalable real-time chat application with WebSockets, including architecture, database design, message queuing, and deployment strategies'],
|
||||
{ cwd: testDir, timeout: 180000 }
|
||||
['--topic', 'REST API design', '--quick'],
|
||||
{ cwd: testDir, timeout: 60000 }
|
||||
);
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Command failed: ${result.stderr}`);
|
||||
}
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('Research Results');
|
||||
|
||||
logger.info(`Complex research completed in ${duration}ms`);
|
||||
|
||||
// Should cover all requested topics
|
||||
const output = result.stdout.toLowerCase();
|
||||
const topics = ['websocket', 'architecture', 'database', 'queue', 'deployment', 'scalab'];
|
||||
const coveredTopics = topics.filter(topic => output.includes(topic)).length;
|
||||
|
||||
if (coveredTopics < 4) {
|
||||
throw new Error('Complex research did not cover all requested topics');
|
||||
}
|
||||
});
|
||||
// Quick research should be faster
|
||||
expect(duration).toBeLessThan(60000);
|
||||
}, 90000);
|
||||
|
||||
// Test 15: Research with export option (preparing for research-save)
|
||||
await runTest('Research with export preparation', async () => {
|
||||
it('should perform detailed research with --detailed flag', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'research',
|
||||
['--prepare-export', 'Best practices for API versioning'],
|
||||
['--topic', 'Microservices architecture patterns', '--detailed'],
|
||||
{ cwd: testDir, timeout: 120000 }
|
||||
);
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('Research Results');
|
||||
|
||||
// Detailed research should have more content
|
||||
expect(result.stdout.length).toBeGreaterThan(500);
|
||||
|
||||
// Should contain comprehensive information
|
||||
const hasPatterns = result.stdout.toLowerCase().includes('pattern') ||
|
||||
result.stdout.toLowerCase().includes('architecture');
|
||||
expect(hasPatterns).toBe(true);
|
||||
}, 150000);
|
||||
});
|
||||
|
||||
describe('Research with citations', () => {
|
||||
it('should include sources with --sources flag', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'research',
|
||||
['--topic', 'GraphQL best practices', '--sources'],
|
||||
{ cwd: testDir, timeout: 90000 }
|
||||
);
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('Research Results');
|
||||
|
||||
// Should include source references
|
||||
const hasSources = result.stdout.includes('Source:') ||
|
||||
result.stdout.includes('Reference:') ||
|
||||
result.stdout.includes('http');
|
||||
expect(hasSources).toBe(true);
|
||||
}, 120000);
|
||||
});
|
||||
|
||||
describe('Research output options', () => {
|
||||
it('should save research to file with --save flag', async () => {
|
||||
const outputPath = join(testDir, 'research-output.md');
|
||||
|
||||
const result = await helpers.taskMaster(
|
||||
'research',
|
||||
['--topic', 'Docker container security', '--save', outputPath],
|
||||
{ cwd: testDir, timeout: 90000 }
|
||||
);
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('Research saved to');
|
||||
|
||||
// Verify file was created
|
||||
expect(existsSync(outputPath)).toBe(true);
|
||||
|
||||
// Verify file contains research content
|
||||
const content = readFileSync(outputPath, 'utf8');
|
||||
expect(content).toContain('Docker');
|
||||
expect(content.length).toBeGreaterThan(100);
|
||||
}, 120000);
|
||||
|
||||
it('should output in JSON format', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'research',
|
||||
['--topic', 'WebSocket implementation', '--output', 'json'],
|
||||
{ cwd: testDir, timeout: 90000 }
|
||||
);
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
// Output should be valid JSON
|
||||
const jsonOutput = JSON.parse(result.stdout);
|
||||
expect(jsonOutput.topic).toBeDefined();
|
||||
expect(jsonOutput.research).toBeDefined();
|
||||
expect(jsonOutput.timestamp).toBeDefined();
|
||||
}, 120000);
|
||||
|
||||
it('should output in markdown format by default', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'research',
|
||||
['--topic', 'CI/CD pipeline best practices'],
|
||||
{ cwd: testDir, timeout: 90000 }
|
||||
);
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
// Should contain markdown formatting
|
||||
const hasMarkdown = result.stdout.includes('#') ||
|
||||
result.stdout.includes('*') ||
|
||||
result.stdout.includes('-');
|
||||
expect(hasMarkdown).toBe(true);
|
||||
}, 120000);
|
||||
});
|
||||
|
||||
describe('Research categories', () => {
|
||||
it('should research coding patterns', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'research',
|
||||
['--topic', 'Singleton pattern in JavaScript', '--category', 'patterns'],
|
||||
{ cwd: testDir, timeout: 90000 }
|
||||
);
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout.toLowerCase()).toContain('singleton');
|
||||
expect(result.stdout.toLowerCase()).toContain('pattern');
|
||||
}, 120000);
|
||||
|
||||
it('should research security topics', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'research',
|
||||
['--topic', 'OWASP Top 10 vulnerabilities', '--category', 'security'],
|
||||
{ cwd: testDir, timeout: 90000 }
|
||||
);
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout.toLowerCase()).toContain('security');
|
||||
expect(result.stdout.toUpperCase()).toContain('OWASP');
|
||||
}, 120000);
|
||||
|
||||
it('should research performance topics', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'research',
|
||||
['--topic', 'Database query optimization', '--category', 'performance'],
|
||||
{ cwd: testDir, timeout: 90000 }
|
||||
);
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout.toLowerCase()).toContain('optimization');
|
||||
expect(result.stdout.toLowerCase()).toContain('performance');
|
||||
}, 120000);
|
||||
});
|
||||
|
||||
describe('Research integration with tasks', () => {
|
||||
it('should research for specific task context', async () => {
|
||||
// Create a task first
|
||||
const addResult = await helpers.taskMaster(
|
||||
'add-task',
|
||||
['--prompt', 'Implement real-time chat feature'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
const taskId = helpers.extractTaskId(addResult.stdout);
|
||||
|
||||
// Research for the task
|
||||
const result = await helpers.taskMaster(
|
||||
'research',
|
||||
['--task', taskId, '--topic', 'WebSocket vs Server-Sent Events'],
|
||||
{ cwd: testDir, timeout: 90000 }
|
||||
);
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('Research Results');
|
||||
expect(result.stdout.toLowerCase()).toContain('websocket');
|
||||
}, 120000);
|
||||
|
||||
it('should append research to task notes', async () => {
|
||||
// Create a task
|
||||
const addResult = await helpers.taskMaster(
|
||||
'add-task',
|
||||
['--prompt', 'Setup monitoring system'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
const taskId = helpers.extractTaskId(addResult.stdout);
|
||||
|
||||
// Research and append to task
|
||||
const result = await helpers.taskMaster(
|
||||
'research',
|
||||
['--task', taskId, '--topic', 'Prometheus vs ELK stack', '--append-to-task'],
|
||||
{ cwd: testDir, timeout: 90000 }
|
||||
);
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('Research appended to task');
|
||||
|
||||
// Verify task has research notes
|
||||
const showResult = await helpers.taskMaster('show', [taskId], { cwd: testDir });
|
||||
expect(showResult.stdout.toLowerCase()).toContain('prometheus');
|
||||
}, 120000);
|
||||
});
|
||||
|
||||
describe('Research history', () => {
|
||||
it('should save research history', async () => {
|
||||
// Perform multiple researches
|
||||
await helpers.taskMaster(
|
||||
'research',
|
||||
['--topic', 'GraphQL subscriptions'],
|
||||
{ cwd: testDir, timeout: 60000 }
|
||||
);
|
||||
|
||||
await helpers.taskMaster(
|
||||
'research',
|
||||
['--topic', 'Redis pub/sub'],
|
||||
{ cwd: testDir, timeout: 60000 }
|
||||
);
|
||||
|
||||
// Check research history
|
||||
const historyPath = join(testDir, '.taskmaster/research-history.json');
|
||||
if (existsSync(historyPath)) {
|
||||
const history = JSON.parse(readFileSync(historyPath, 'utf8'));
|
||||
expect(history.length).toBeGreaterThanOrEqual(2);
|
||||
}
|
||||
}, 150000);
|
||||
|
||||
it('should list recent research with --history flag', async () => {
|
||||
// Perform a research first
|
||||
await helpers.taskMaster(
|
||||
'research',
|
||||
['--topic', 'Kubernetes deployment strategies'],
|
||||
{ cwd: testDir, timeout: 60000 }
|
||||
);
|
||||
|
||||
// List history
|
||||
const result = await helpers.taskMaster(
|
||||
'research',
|
||||
['--history'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('Research History');
|
||||
}, 90000);
|
||||
});
|
||||
|
||||
describe('Error handling', () => {
|
||||
it('should fail without topic', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'research',
|
||||
[],
|
||||
{ cwd: testDir, allowFailure: true }
|
||||
);
|
||||
|
||||
expect(result.exitCode).not.toBe(0);
|
||||
expect(result.stderr).toContain('topic');
|
||||
});
|
||||
|
||||
it('should handle invalid output format', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'research',
|
||||
['--topic', 'Test topic', '--output', 'invalid-format'],
|
||||
{ cwd: testDir, allowFailure: true }
|
||||
);
|
||||
|
||||
expect(result.exitCode).not.toBe(0);
|
||||
expect(result.stderr).toContain('Invalid output format');
|
||||
});
|
||||
|
||||
it('should handle network errors gracefully', async () => {
|
||||
// This test might pass if network is available
|
||||
// It's mainly to ensure the command handles errors gracefully
|
||||
const result = await helpers.taskMaster(
|
||||
'research',
|
||||
['--topic', 'Test with potential network issues'],
|
||||
{ cwd: testDir, timeout: 30000, allowFailure: true }
|
||||
);
|
||||
|
||||
// Should either succeed or fail gracefully
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Command failed: ${result.stderr}`);
|
||||
expect(result.stderr).toBeTruthy();
|
||||
} else {
|
||||
expect(result.stdout).toContain('Research Results');
|
||||
}
|
||||
}, 45000);
|
||||
});
|
||||
|
||||
describe('Research focus areas', () => {
|
||||
it('should research implementation details', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'research',
|
||||
['--topic', 'JWT implementation in Node.js', '--focus', 'implementation'],
|
||||
{ cwd: testDir, timeout: 90000 }
|
||||
);
|
||||
|
||||
// Should indicate export readiness
|
||||
if (!result.stdout.includes('API') || !result.stdout.includes('version')) {
|
||||
throw new Error('Research content not relevant to query');
|
||||
}
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout.toLowerCase()).toContain('implementation');
|
||||
expect(result.stdout.toLowerCase()).toContain('code');
|
||||
}, 120000);
|
||||
|
||||
it('should research best practices', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'research',
|
||||
['--topic', 'REST API versioning', '--focus', 'best-practices'],
|
||||
{ cwd: testDir, timeout: 90000 }
|
||||
);
|
||||
|
||||
// Check if research is structured for saving
|
||||
const hasStructure = result.stdout.includes('#') ||
|
||||
result.stdout.includes('##') ||
|
||||
result.stdout.includes('1.') ||
|
||||
result.stdout.includes('*');
|
||||
if (!hasStructure) {
|
||||
throw new Error('Research not well-structured for export');
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout.toLowerCase()).toContain('best practice');
|
||||
}, 120000);
|
||||
|
||||
it('should research comparisons', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'research',
|
||||
['--topic', 'Vue vs React vs Angular', '--focus', 'comparison'],
|
||||
{ cwd: testDir, timeout: 90000 }
|
||||
);
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
const output = result.stdout.toLowerCase();
|
||||
expect(output).toContain('vue');
|
||||
expect(output).toContain('react');
|
||||
expect(output).toContain('angular');
|
||||
}, 120000);
|
||||
});
|
||||
|
||||
describe('Research with constraints', () => {
|
||||
it('should limit research length with --max-length', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'research',
|
||||
['--topic', 'Machine learning basics', '--max-length', '500'],
|
||||
{ cwd: testDir, timeout: 60000 }
|
||||
);
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
// Research output should be concise
|
||||
expect(result.stdout.length).toBeLessThan(2000); // Accounting for formatting
|
||||
}, 90000);
|
||||
|
||||
it('should research with specific year constraint', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'research',
|
||||
['--topic', 'Latest JavaScript features', '--year', '2024'],
|
||||
{ cwd: testDir, timeout: 90000 }
|
||||
);
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
// Should focus on recent content
|
||||
const hasRecentInfo = result.stdout.includes('2024') ||
|
||||
result.stdout.toLowerCase().includes('latest') ||
|
||||
result.stdout.toLowerCase().includes('recent');
|
||||
expect(hasRecentInfo).toBe(true);
|
||||
}, 120000);
|
||||
});
|
||||
|
||||
describe('Research caching', () => {
|
||||
it('should cache and reuse research results', async () => {
|
||||
const topic = 'Redis caching strategies';
|
||||
|
||||
// First research
|
||||
const startTime1 = Date.now();
|
||||
const result1 = await helpers.taskMaster(
|
||||
'research',
|
||||
['--topic', topic],
|
||||
{ cwd: testDir, timeout: 90000 }
|
||||
);
|
||||
const duration1 = Date.now() - startTime1;
|
||||
expect(result1).toHaveExitCode(0);
|
||||
|
||||
// Second research (should be cached)
|
||||
const startTime2 = Date.now();
|
||||
const result2 = await helpers.taskMaster(
|
||||
'research',
|
||||
['--topic', topic],
|
||||
{ cwd: testDir, timeout: 30000 }
|
||||
);
|
||||
const duration2 = Date.now() - startTime2;
|
||||
expect(result2).toHaveExitCode(0);
|
||||
|
||||
// Cached result should be much faster
|
||||
if (result2.stdout.includes('(cached)')) {
|
||||
expect(duration2).toBeLessThan(duration1 / 2);
|
||||
}
|
||||
});
|
||||
}, 150000);
|
||||
|
||||
// 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=== Research 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 research tests passed!');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
results.status = 'failed';
|
||||
results.errors.push({
|
||||
test: 'research test suite',
|
||||
error: error.message,
|
||||
stack: error.stack
|
||||
});
|
||||
logger.error(`Research test suite failed: ${error.message}`);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
it('should bypass cache with --no-cache flag', async () => {
|
||||
const topic = 'Docker best practices';
|
||||
|
||||
// First research
|
||||
await helpers.taskMaster(
|
||||
'research',
|
||||
['--topic', topic],
|
||||
{ cwd: testDir, timeout: 60000 }
|
||||
);
|
||||
|
||||
// Second research without cache
|
||||
const result = await helpers.taskMaster(
|
||||
'research',
|
||||
['--topic', topic, '--no-cache'],
|
||||
{ cwd: testDir, timeout: 90000 }
|
||||
);
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).not.toContain('(cached)');
|
||||
}, 180000);
|
||||
});
|
||||
});
|
||||
@@ -1,475 +1,565 @@
|
||||
/**
|
||||
* Comprehensive E2E tests for update-subtask command
|
||||
* Tests all aspects of subtask updates including AI and manual modes
|
||||
* Tests all aspects of subtask updates including AI-powered updates
|
||||
*/
|
||||
|
||||
export default async function testUpdateSubtask(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('update-subtask command', () => {
|
||||
let testDir;
|
||||
let helpers;
|
||||
let parentTaskId;
|
||||
let subtaskId;
|
||||
|
||||
try {
|
||||
logger.info('Starting comprehensive update-subtask tests...');
|
||||
|
||||
// Setup: Create parent task with subtasks
|
||||
logger.info('Setting up parent task with subtasks...');
|
||||
beforeEach(async () => {
|
||||
// Create test directory
|
||||
testDir = mkdtempSync(join(tmpdir(), 'task-master-update-subtask-'));
|
||||
|
||||
// Create parent task
|
||||
// Initialize test helpers
|
||||
const context = global.createTestContext('update-subtask');
|
||||
helpers = context.helpers;
|
||||
|
||||
// Copy .env file if it exists
|
||||
const mainEnvPath = join(__dirname, '../../../../.env');
|
||||
const testEnvPath = join(testDir, '.env');
|
||||
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);
|
||||
|
||||
// Create a parent task with subtask
|
||||
const parentResult = await helpers.taskMaster(
|
||||
'add-task',
|
||||
['--prompt', 'Build a user authentication system'],
|
||||
['--title', 'Parent task', '--description', 'Task with subtasks'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
const parentTaskId = helpers.extractTaskId(parentResult.stdout);
|
||||
parentTaskId = helpers.extractTaskId(parentResult.stdout);
|
||||
|
||||
// Expand to get AI-generated subtasks
|
||||
await helpers.taskMaster('expand', [parentTaskId], { cwd: testDir, timeout: 120000 });
|
||||
|
||||
// Add some manual subtasks
|
||||
await helpers.taskMaster(
|
||||
// Create a subtask
|
||||
const subtaskResult = await helpers.taskMaster(
|
||||
'add-subtask',
|
||||
[parentTaskId, 'Setup database schema'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
await helpers.taskMaster(
|
||||
'add-subtask',
|
||||
[parentTaskId, 'Create login endpoint'],
|
||||
[parentTaskId, 'Initial subtask'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
// Extract subtask ID (should be like "1.1")
|
||||
const match = subtaskResult.stdout.match(/subtask #?(\d+\.\d+)/i);
|
||||
subtaskId = match ? match[1] : '1.1';
|
||||
});
|
||||
|
||||
// Test 1: Basic AI-powered subtask update
|
||||
await runTest('AI-powered subtask update', async () => {
|
||||
const subtaskId = `${parentTaskId}.1`;
|
||||
afterEach(() => {
|
||||
// Clean up test directory
|
||||
if (testDir && existsSync(testDir)) {
|
||||
rmSync(testDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
describe('Basic subtask updates', () => {
|
||||
it('should update subtask title', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'update-subtask',
|
||||
['--id', subtaskId, '--prompt', 'Make this subtask focus on JWT token implementation'],
|
||||
{ cwd: testDir, timeout: 120000 }
|
||||
[subtaskId, 'Updated subtask title'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Command failed: ${result.stderr}`);
|
||||
}
|
||||
|
||||
// Verify subtask was updated
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('Updated subtask');
|
||||
|
||||
// Verify update
|
||||
const showResult = await helpers.taskMaster('show', [parentTaskId], { cwd: testDir });
|
||||
if (!showResult.stdout.includes('JWT') || !showResult.stdout.includes('token')) {
|
||||
throw new Error('Subtask not updated with JWT focus');
|
||||
}
|
||||
expect(showResult.stdout).toContain('Updated subtask title');
|
||||
});
|
||||
|
||||
// Test 2: Manual subtask update (without AI)
|
||||
await runTest('Manual subtask update', async () => {
|
||||
const subtaskId = `${parentTaskId}.2`;
|
||||
it('should update subtask with additional notes', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'update-subtask',
|
||||
[subtaskId, '--notes', 'Implementation details: Use async/await'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
// Verify notes were added
|
||||
const showResult = await helpers.taskMaster('show', [parentTaskId], { cwd: testDir });
|
||||
expect(showResult.stdout).toContain('async/await');
|
||||
});
|
||||
|
||||
it('should update subtask status', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'update-subtask',
|
||||
[subtaskId, '--status', 'completed'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
// Verify status update
|
||||
const showResult = await helpers.taskMaster('show', [parentTaskId], { cwd: testDir });
|
||||
expect(showResult.stdout.toLowerCase()).toContain('completed');
|
||||
});
|
||||
});
|
||||
|
||||
describe('AI-powered subtask updates', () => {
|
||||
it('should update subtask using AI prompt', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'update-subtask',
|
||||
[subtaskId, '--prompt', 'Add implementation steps and best practices'],
|
||||
{ cwd: testDir, timeout: 45000 }
|
||||
);
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('Updated subtask');
|
||||
|
||||
// Verify AI enhanced the subtask
|
||||
const tasksPath = join(testDir, '.taskmaster/tasks/tasks.json');
|
||||
const tasks = JSON.parse(readFileSync(tasksPath, 'utf8'));
|
||||
const parentTask = tasks.master.tasks.find(t => t.id === parseInt(parentTaskId));
|
||||
const subtask = parentTask.subtasks.find(s => s.id === subtaskId);
|
||||
|
||||
// Should have more detailed content
|
||||
expect(subtask.title.length).toBeGreaterThan(20);
|
||||
}, 60000);
|
||||
|
||||
it('should enhance subtask with technical details', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'update-subtask',
|
||||
[subtaskId, '--prompt', 'Add technical requirements and edge cases to consider'],
|
||||
{ cwd: testDir, timeout: 45000 }
|
||||
);
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
// Check that subtask was enhanced
|
||||
const showResult = await helpers.taskMaster('show', [parentTaskId], { cwd: testDir });
|
||||
const hasEnhancement = showResult.stdout.toLowerCase().includes('requirement') ||
|
||||
showResult.stdout.toLowerCase().includes('edge case') ||
|
||||
showResult.stdout.toLowerCase().includes('consider');
|
||||
expect(hasEnhancement).toBe(true);
|
||||
}, 60000);
|
||||
|
||||
it('should update subtask with research mode', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'update-subtask',
|
||||
[
|
||||
'--id', subtaskId,
|
||||
'--title', 'Implement OAuth 2.0 integration',
|
||||
'--description', 'Add support for Google and GitHub OAuth providers'
|
||||
subtaskId,
|
||||
'--prompt', 'Add industry best practices for error handling',
|
||||
'--research'
|
||||
],
|
||||
{ cwd: testDir, timeout: 90000 }
|
||||
);
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
// Research mode should add comprehensive content
|
||||
const showResult = await helpers.taskMaster('show', [parentTaskId], { cwd: testDir });
|
||||
const hasResearchContent = showResult.stdout.toLowerCase().includes('error') ||
|
||||
showResult.stdout.toLowerCase().includes('handling') ||
|
||||
showResult.stdout.toLowerCase().includes('practice');
|
||||
expect(hasResearchContent).toBe(true);
|
||||
}, 120000);
|
||||
});
|
||||
|
||||
describe('Multiple subtask updates', () => {
|
||||
it('should update multiple subtasks sequentially', async () => {
|
||||
// Create another subtask
|
||||
const subtask2Result = await helpers.taskMaster(
|
||||
'add-subtask',
|
||||
[parentTaskId, 'Second subtask'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
const match = subtask2Result.stdout.match(/subtask #?(\d+\.\d+)/i);
|
||||
const subtaskId2 = match ? match[1] : '1.2';
|
||||
|
||||
// Update first subtask
|
||||
await helpers.taskMaster(
|
||||
'update-subtask',
|
||||
[subtaskId, 'First subtask updated'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
// Update second subtask
|
||||
const result = await helpers.taskMaster(
|
||||
'update-subtask',
|
||||
[subtaskId2, 'Second subtask updated'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
// Verify both updates
|
||||
const showResult = await helpers.taskMaster('show', [parentTaskId], { cwd: testDir });
|
||||
expect(showResult.stdout).toContain('First subtask updated');
|
||||
expect(showResult.stdout).toContain('Second subtask updated');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Subtask metadata updates', () => {
|
||||
it('should add priority to subtask', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'update-subtask',
|
||||
[subtaskId, '--priority', 'high'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
// Verify priority was set
|
||||
const showResult = await helpers.taskMaster('show', [parentTaskId], { cwd: testDir });
|
||||
expect(showResult.stdout.toLowerCase()).toContain('high');
|
||||
});
|
||||
|
||||
it('should add estimated time to subtask', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'update-subtask',
|
||||
[subtaskId, '--estimated-time', '2h'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
// Verify estimated time was set
|
||||
const showResult = await helpers.taskMaster('show', [parentTaskId], { cwd: testDir });
|
||||
expect(showResult.stdout).toContain('2h');
|
||||
});
|
||||
|
||||
it('should add assignee to subtask', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'update-subtask',
|
||||
[subtaskId, '--assignee', 'john.doe@example.com'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
// Verify assignee was set
|
||||
const showResult = await helpers.taskMaster('show', [parentTaskId], { cwd: testDir });
|
||||
expect(showResult.stdout).toContain('john.doe');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Combined updates', () => {
|
||||
it('should update title and notes together', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'update-subtask',
|
||||
[
|
||||
subtaskId,
|
||||
'New comprehensive title',
|
||||
'--notes', 'Additional implementation details'
|
||||
],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Command failed: ${result.stderr}`);
|
||||
}
|
||||
|
||||
// Verify exact updates
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
// Verify both updates
|
||||
const showResult = await helpers.taskMaster('show', [parentTaskId], { cwd: testDir });
|
||||
if (!showResult.stdout.includes('OAuth 2.0')) {
|
||||
throw new Error('Subtask title not updated');
|
||||
}
|
||||
if (!showResult.stdout.includes('Google') || !showResult.stdout.includes('GitHub')) {
|
||||
throw new Error('Subtask description not updated');
|
||||
}
|
||||
expect(showResult.stdout).toContain('New comprehensive title');
|
||||
expect(showResult.stdout).toContain('Additional implementation details');
|
||||
});
|
||||
|
||||
// Test 3: Update subtask status
|
||||
await runTest('Update subtask status', async () => {
|
||||
const subtaskId = `${parentTaskId}.3`;
|
||||
it('should combine manual update with AI prompt', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'update-subtask',
|
||||
['--id', subtaskId, '--status', 'in_progress'],
|
||||
{ cwd: testDir }
|
||||
[
|
||||
subtaskId,
|
||||
'--status', 'in_progress',
|
||||
'--prompt', 'Add acceptance criteria'
|
||||
],
|
||||
{ cwd: testDir, timeout: 45000 }
|
||||
);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Command failed: ${result.stderr}`);
|
||||
}
|
||||
|
||||
// Verify status change
|
||||
const tasksJson = helpers.readJson(`${testDir}/.taskmaster/tasks/tasks.json`);
|
||||
const parentTask = tasksJson.tasks.find(t => t.id === parentTaskId);
|
||||
const subtask = parentTask.subtasks.find(s => s.id === subtaskId);
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
if (subtask.status !== 'in_progress') {
|
||||
throw new Error('Subtask status not updated');
|
||||
}
|
||||
});
|
||||
|
||||
// Test 4: Update subtask priority
|
||||
await runTest('Update subtask priority', async () => {
|
||||
const subtaskId = `${parentTaskId}.4`;
|
||||
const result = await helpers.taskMaster(
|
||||
'update-subtask',
|
||||
['--id', subtaskId, '--priority', 'high'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Command failed: ${result.stderr}`);
|
||||
}
|
||||
|
||||
// Verify priority change
|
||||
const tasksJson = helpers.readJson(`${testDir}/.taskmaster/tasks/tasks.json`);
|
||||
const parentTask = tasksJson.tasks.find(t => t.id === parentTaskId);
|
||||
const subtask = parentTask.subtasks.find(s => s.id === subtaskId);
|
||||
|
||||
if (subtask.priority !== 'high') {
|
||||
throw new Error('Subtask priority not updated');
|
||||
}
|
||||
});
|
||||
|
||||
// Test 5: Batch subtask updates
|
||||
await runTest('Batch subtask updates', async () => {
|
||||
// Update multiple subtasks at once
|
||||
const subtaskIds = [`${parentTaskId}.1`, `${parentTaskId}.2`];
|
||||
const result = await helpers.taskMaster(
|
||||
'update-subtask',
|
||||
['--ids', subtaskIds.join(','), '--status', 'completed'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Command failed: ${result.stderr}`);
|
||||
}
|
||||
|
||||
// Verify all were updated
|
||||
const tasksJson = helpers.readJson(`${testDir}/.taskmaster/tasks/tasks.json`);
|
||||
const parentTask = tasksJson.tasks.find(t => t.id === parentTaskId);
|
||||
|
||||
subtaskIds.forEach(id => {
|
||||
const subtask = parentTask.subtasks.find(s => s.id === id);
|
||||
if (subtask.status !== 'completed') {
|
||||
throw new Error(`Subtask ${id} not updated in batch`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Test 6: Update subtask with dependencies
|
||||
await runTest('Update subtask dependencies', async () => {
|
||||
const subtask1 = `${parentTaskId}.3`;
|
||||
const subtask2 = `${parentTaskId}.4`;
|
||||
|
||||
const result = await helpers.taskMaster(
|
||||
'update-subtask',
|
||||
['--id', subtask2, '--depends-on', subtask1],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Command failed: ${result.stderr}`);
|
||||
}
|
||||
|
||||
// Verify dependency was added
|
||||
const tasksJson = helpers.readJson(`${testDir}/.taskmaster/tasks/tasks.json`);
|
||||
const parentTask = tasksJson.tasks.find(t => t.id === parentTaskId);
|
||||
const subtask = parentTask.subtasks.find(s => s.id === subtask2);
|
||||
|
||||
if (!subtask.dependencies || !subtask.dependencies.includes(subtask1)) {
|
||||
throw new Error('Subtask dependency not added');
|
||||
}
|
||||
});
|
||||
|
||||
// Test 7: AI enhancement of existing subtask
|
||||
await runTest('AI enhancement of manual subtask', async () => {
|
||||
// Get last manual subtask
|
||||
// Verify both manual and AI updates
|
||||
const showResult = await helpers.taskMaster('show', [parentTaskId], { cwd: testDir });
|
||||
const subtaskMatches = showResult.stdout.match(/(\d+\.\d+)/g) || [];
|
||||
const lastSubtaskId = subtaskMatches[subtaskMatches.length - 1];
|
||||
|
||||
const result = await helpers.taskMaster(
|
||||
'update-subtask',
|
||||
['--id', lastSubtaskId, '--enhance', '--prompt', 'Add security considerations'],
|
||||
{ cwd: testDir, timeout: 120000 }
|
||||
);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Command failed: ${result.stderr}`);
|
||||
}
|
||||
|
||||
// Should include security aspects
|
||||
const updatedShow = await helpers.taskMaster('show', [parentTaskId], { cwd: testDir });
|
||||
const hasSecurityMention = updatedShow.stdout.toLowerCase().includes('security') ||
|
||||
updatedShow.stdout.toLowerCase().includes('secure') ||
|
||||
updatedShow.stdout.toLowerCase().includes('protection');
|
||||
|
||||
if (!hasSecurityMention) {
|
||||
throw new Error('AI enhancement did not add security considerations');
|
||||
}
|
||||
});
|
||||
expect(showResult.stdout.toLowerCase()).toContain('in_progress');
|
||||
const hasAcceptanceCriteria = showResult.stdout.toLowerCase().includes('acceptance') ||
|
||||
showResult.stdout.toLowerCase().includes('criteria');
|
||||
expect(hasAcceptanceCriteria).toBe(true);
|
||||
}, 60000);
|
||||
});
|
||||
|
||||
// Test 8: Error handling - invalid subtask ID
|
||||
await runTest('Error handling - invalid subtask ID', async () => {
|
||||
describe('Append mode', () => {
|
||||
it('should append to subtask notes', async () => {
|
||||
// First set some notes
|
||||
await helpers.taskMaster(
|
||||
'update-subtask',
|
||||
[subtaskId, '--notes', 'Initial notes.'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
// Then append
|
||||
const result = await helpers.taskMaster(
|
||||
'update-subtask',
|
||||
['--id', '999.999', '--title', 'Invalid update'],
|
||||
[subtaskId, '--append-notes', '\nAdditional considerations.'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
// Verify notes were appended
|
||||
const showResult = await helpers.taskMaster('show', [parentTaskId], { cwd: testDir });
|
||||
expect(showResult.stdout).toContain('Initial notes');
|
||||
expect(showResult.stdout).toContain('Additional considerations');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Nested subtasks', () => {
|
||||
it('should update nested subtask', async () => {
|
||||
// Create a nested subtask
|
||||
const nestedResult = await helpers.taskMaster(
|
||||
'add-subtask',
|
||||
[subtaskId, 'Nested subtask'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
const match = nestedResult.stdout.match(/subtask #?(\d+\.\d+\.\d+)/i);
|
||||
const nestedId = match ? match[1] : '1.1.1';
|
||||
|
||||
// Update nested subtask
|
||||
const result = await helpers.taskMaster(
|
||||
'update-subtask',
|
||||
[nestedId, 'Updated nested subtask'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
// Verify update
|
||||
const showResult = await helpers.taskMaster('show', [parentTaskId], { cwd: testDir });
|
||||
expect(showResult.stdout).toContain('Updated nested subtask');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Tag-specific subtask updates', () => {
|
||||
it('should update subtask in specific tag', async () => {
|
||||
// Create a tag and add task to it
|
||||
await helpers.taskMaster('add-tag', ['feature-y'], { cwd: testDir });
|
||||
|
||||
// Create task in tag
|
||||
const tagTaskResult = await helpers.taskMaster(
|
||||
'add-task',
|
||||
['--prompt', 'Task in feature-y', '--tag', 'feature-y'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
const tagTaskId = helpers.extractTaskId(tagTaskResult.stdout);
|
||||
|
||||
// Add subtask to tagged task
|
||||
const tagSubtaskResult = await helpers.taskMaster(
|
||||
'add-subtask',
|
||||
[tagTaskId, 'Subtask in feature tag'],
|
||||
{ cwd: testDir, options: ['--tag', 'feature-y'] }
|
||||
);
|
||||
const match = tagSubtaskResult.stdout.match(/subtask #?(\d+\.\d+)/i);
|
||||
const tagSubtaskId = match ? match[1] : '1.1';
|
||||
|
||||
// Update subtask in specific tag
|
||||
const result = await helpers.taskMaster(
|
||||
'update-subtask',
|
||||
[tagSubtaskId, 'Updated in feature tag', '--tag', 'feature-y'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
// Verify update in correct tag
|
||||
const showResult = await helpers.taskMaster(
|
||||
'show',
|
||||
[tagTaskId, '--tag', 'feature-y'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
expect(showResult.stdout).toContain('Updated in feature tag');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Output formats', () => {
|
||||
it('should output in JSON format', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'update-subtask',
|
||||
[subtaskId, 'JSON test update', '--output', 'json'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
// Output should be valid JSON
|
||||
const jsonOutput = JSON.parse(result.stdout);
|
||||
expect(jsonOutput.success).toBe(true);
|
||||
expect(jsonOutput.subtask).toBeDefined();
|
||||
expect(jsonOutput.subtask.title).toBe('JSON test update');
|
||||
expect(jsonOutput.parentTaskId).toBe(parseInt(parentTaskId));
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error handling', () => {
|
||||
it('should fail with non-existent subtask ID', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'update-subtask',
|
||||
['99.99', 'This should fail'],
|
||||
{ cwd: testDir, allowFailure: true }
|
||||
);
|
||||
if (result.exitCode === 0) {
|
||||
throw new Error('Should have failed with invalid subtask ID');
|
||||
}
|
||||
if (!result.stderr.includes('not found') && !result.stderr.includes('invalid')) {
|
||||
throw new Error('Error message not clear about invalid ID');
|
||||
}
|
||||
|
||||
expect(result.exitCode).not.toBe(0);
|
||||
expect(result.stderr).toContain('not found');
|
||||
});
|
||||
|
||||
// Test 9: Update subtask metadata
|
||||
await runTest('Update subtask metadata', async () => {
|
||||
const subtaskId = `${parentTaskId}.1`;
|
||||
it('should fail with invalid subtask ID format', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'update-subtask',
|
||||
[
|
||||
'--id', subtaskId,
|
||||
'--metadata', 'assigned_to=john@example.com',
|
||||
'--metadata', 'estimated_hours=4'
|
||||
],
|
||||
['invalid-id', 'This should fail'],
|
||||
{ cwd: testDir, allowFailure: true }
|
||||
);
|
||||
|
||||
expect(result.exitCode).not.toBe(0);
|
||||
expect(result.stderr).toContain('Invalid subtask ID');
|
||||
});
|
||||
|
||||
it('should fail with invalid priority', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'update-subtask',
|
||||
[subtaskId, '--priority', 'invalid-priority'],
|
||||
{ cwd: testDir, allowFailure: true }
|
||||
);
|
||||
|
||||
expect(result.exitCode).not.toBe(0);
|
||||
expect(result.stderr).toContain('Invalid priority');
|
||||
});
|
||||
|
||||
it('should fail with invalid status', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'update-subtask',
|
||||
[subtaskId, '--status', 'invalid-status'],
|
||||
{ cwd: testDir, allowFailure: true }
|
||||
);
|
||||
|
||||
expect(result.exitCode).not.toBe(0);
|
||||
expect(result.stderr).toContain('Invalid status');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Performance and edge cases', () => {
|
||||
it('should handle very long subtask titles', async () => {
|
||||
const longTitle = 'This is a very detailed subtask title. '.repeat(10);
|
||||
|
||||
const result = await helpers.taskMaster(
|
||||
'update-subtask',
|
||||
[subtaskId, longTitle],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Command failed: ${result.stderr}`);
|
||||
}
|
||||
|
||||
// Verify metadata
|
||||
const tasksJson = helpers.readJson(`${testDir}/.taskmaster/tasks/tasks.json`);
|
||||
const parentTask = tasksJson.tasks.find(t => t.id === parentTaskId);
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
// Verify long title was saved
|
||||
const tasksPath = join(testDir, '.taskmaster/tasks/tasks.json');
|
||||
const tasks = JSON.parse(readFileSync(tasksPath, 'utf8'));
|
||||
const parentTask = tasks.master.tasks.find(t => t.id === parseInt(parentTaskId));
|
||||
const subtask = parentTask.subtasks.find(s => s.id === subtaskId);
|
||||
|
||||
if (!subtask.metadata || subtask.metadata.assigned_to !== 'john@example.com') {
|
||||
throw new Error('Subtask metadata not updated');
|
||||
}
|
||||
expect(subtask.title).toBe(longTitle);
|
||||
});
|
||||
|
||||
// Test 10: Update with validation
|
||||
await runTest('Update with validation rules', async () => {
|
||||
// Try to update completed subtask (should warn or fail based on rules)
|
||||
const subtaskId = `${parentTaskId}.1`; // This was marked completed earlier
|
||||
it('should update subtask without affecting parent task', async () => {
|
||||
const originalParentTitle = 'Parent task';
|
||||
|
||||
// Update subtask
|
||||
const result = await helpers.taskMaster(
|
||||
'update-subtask',
|
||||
['--id', subtaskId, '--title', 'Trying to update completed task', '--force'],
|
||||
[subtaskId, 'Completely different subtask'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
// Should either succeed with --force or provide clear message
|
||||
if (result.exitCode !== 0 && !result.stderr.includes('completed')) {
|
||||
throw new Error('No clear message about updating completed subtask');
|
||||
}
|
||||
});
|
||||
|
||||
// Test 11: Complex update with multiple fields
|
||||
await runTest('Complex multi-field update', async () => {
|
||||
// Create fresh subtask
|
||||
await helpers.taskMaster(
|
||||
'add-subtask',
|
||||
[parentTaskId, 'Fresh subtask for complex update'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
// Verify parent task remains unchanged
|
||||
const showResult = await helpers.taskMaster('show', [parentTaskId], { cwd: testDir });
|
||||
const subtaskMatches = showResult.stdout.match(/(\d+\.\d+)/g) || [];
|
||||
const newSubtaskId = subtaskMatches[subtaskMatches.length - 1];
|
||||
|
||||
const result = await helpers.taskMaster(
|
||||
'update-subtask',
|
||||
[
|
||||
'--id', newSubtaskId,
|
||||
'--prompt', 'Enhance with testing requirements',
|
||||
'--priority', 'medium',
|
||||
'--status', 'in_progress',
|
||||
'--metadata', 'test_coverage=required'
|
||||
],
|
||||
{ cwd: testDir, timeout: 120000 }
|
||||
);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Command failed: ${result.stderr}`);
|
||||
}
|
||||
|
||||
// Verify all updates applied
|
||||
const tasksJson = helpers.readJson(`${testDir}/.taskmaster/tasks/tasks.json`);
|
||||
const parentTask = tasksJson.tasks.find(t => t.id === parentTaskId);
|
||||
const subtask = parentTask.subtasks.find(s => s.id === newSubtaskId);
|
||||
|
||||
if (subtask.priority !== 'medium' || subtask.status !== 'in_progress') {
|
||||
throw new Error('Not all fields updated');
|
||||
}
|
||||
if (!subtask.metadata || subtask.metadata.test_coverage !== 'required') {
|
||||
throw new Error('Metadata not updated in complex update');
|
||||
}
|
||||
expect(showResult.stdout).toContain(originalParentTitle);
|
||||
});
|
||||
|
||||
// Test 12: Update subtask in context of parent task
|
||||
await runTest('Context-aware subtask update', async () => {
|
||||
// Create new parent task with specific context
|
||||
const contextParent = await helpers.taskMaster(
|
||||
'add-task',
|
||||
['--prompt', 'Build REST API with Node.js'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
const contextParentId = helpers.extractTaskId(contextParent.stdout);
|
||||
it('should handle subtask updates with special characters', async () => {
|
||||
const specialTitle = 'Subtask with special chars: @#$% & "quotes" \'apostrophes\'';
|
||||
|
||||
// Add subtask
|
||||
await helpers.taskMaster(
|
||||
'add-subtask',
|
||||
[contextParentId, 'Create endpoints'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
const subtaskId = `${contextParentId}.1`;
|
||||
const result = await helpers.taskMaster(
|
||||
'update-subtask',
|
||||
['--id', subtaskId, '--prompt', 'Focus on CRUD operations', '--use-parent-context'],
|
||||
{ cwd: testDir, timeout: 120000 }
|
||||
);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Command failed: ${result.stderr}`);
|
||||
}
|
||||
|
||||
// Should maintain REST API context
|
||||
const showResult = await helpers.taskMaster('show', [contextParentId], { cwd: testDir });
|
||||
const hasApiContext = showResult.stdout.toLowerCase().includes('api') ||
|
||||
showResult.stdout.toLowerCase().includes('endpoint') ||
|
||||
showResult.stdout.toLowerCase().includes('rest');
|
||||
|
||||
if (!hasApiContext) {
|
||||
throw new Error('Parent context not preserved in subtask update');
|
||||
}
|
||||
});
|
||||
|
||||
// Test 13: Reorder subtasks during update
|
||||
await runTest('Reorder subtasks', async () => {
|
||||
const subtaskId = `${parentTaskId}.3`;
|
||||
const result = await helpers.taskMaster(
|
||||
'update-subtask',
|
||||
['--id', subtaskId, '--position', '1'],
|
||||
[subtaskId, specialTitle],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Command failed: ${result.stderr}`);
|
||||
}
|
||||
|
||||
// Verify reordering
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
// Verify special characters were preserved
|
||||
const showResult = await helpers.taskMaster('show', [parentTaskId], { cwd: testDir });
|
||||
// The subtask that was at position 3 should now appear first
|
||||
// This is implementation dependent, so we just check it succeeded
|
||||
expect(showResult.stdout).toContain('@#$%');
|
||||
});
|
||||
});
|
||||
|
||||
// Test 14: Update with tag assignment
|
||||
await runTest('Update subtask with tags', async () => {
|
||||
// Create tag first
|
||||
await helpers.taskMaster('add-tag', ['backend-subtasks'], { cwd: testDir });
|
||||
|
||||
const subtaskId = `${parentTaskId}.1`;
|
||||
describe('Dry run mode', () => {
|
||||
it('should preview updates without applying them', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'update-subtask',
|
||||
['--id', subtaskId, '--tag', 'backend-subtasks'],
|
||||
[subtaskId, 'Dry run test', '--dry-run'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Command failed: ${result.stderr}`);
|
||||
}
|
||||
|
||||
// Verify tag was assigned
|
||||
const tasksJson = helpers.readJson(`${testDir}/.taskmaster/tasks/tasks.json`);
|
||||
const parentTask = tasksJson.tasks.find(t => t.id === parentTaskId);
|
||||
const subtask = parentTask.subtasks.find(s => s.id === subtaskId);
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('DRY RUN');
|
||||
expect(result.stdout).toContain('Would update');
|
||||
|
||||
if (!subtask.tags || !subtask.tags.includes('backend-subtasks')) {
|
||||
throw new Error('Tag not assigned to subtask');
|
||||
}
|
||||
// Verify subtask was NOT actually updated
|
||||
const showResult = await helpers.taskMaster('show', [parentTaskId], { cwd: testDir });
|
||||
expect(showResult.stdout).not.toContain('Dry run test');
|
||||
expect(showResult.stdout).toContain('Initial subtask');
|
||||
});
|
||||
});
|
||||
|
||||
// Test 15: Performance - update many subtasks
|
||||
await runTest('Performance - bulk subtask updates', async () => {
|
||||
// Create parent with many subtasks
|
||||
const perfParent = await helpers.taskMaster(
|
||||
'add-task',
|
||||
['--prompt', 'Large project with many components'],
|
||||
describe('Integration with other commands', () => {
|
||||
it('should reflect updates in parent task expansion', async () => {
|
||||
// Update subtask with AI
|
||||
await helpers.taskMaster(
|
||||
'update-subtask',
|
||||
[subtaskId, '--prompt', 'Add detailed implementation steps'],
|
||||
{ cwd: testDir, timeout: 45000 }
|
||||
);
|
||||
|
||||
// Expand parent task
|
||||
const expandResult = await helpers.taskMaster(
|
||||
'expand',
|
||||
['--id', parentTaskId],
|
||||
{ cwd: testDir, timeout: 45000 }
|
||||
);
|
||||
|
||||
expect(expandResult).toHaveExitCode(0);
|
||||
expect(expandResult.stdout).toContain('Expanded task');
|
||||
|
||||
// Should include updated subtask information
|
||||
const showResult = await helpers.taskMaster('show', [parentTaskId], { cwd: testDir });
|
||||
const hasImplementationSteps = showResult.stdout.toLowerCase().includes('implementation') ||
|
||||
showResult.stdout.toLowerCase().includes('step');
|
||||
expect(hasImplementationSteps).toBe(true);
|
||||
}, 90000);
|
||||
|
||||
it('should update subtask after parent task status change', async () => {
|
||||
// Change parent task status
|
||||
await helpers.taskMaster(
|
||||
'set-status',
|
||||
[parentTaskId, 'in_progress'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
const perfParentId = helpers.extractTaskId(perfParent.stdout);
|
||||
|
||||
// Add 20 subtasks
|
||||
const promises = [];
|
||||
for (let i = 1; i <= 20; i++) {
|
||||
promises.push(
|
||||
helpers.taskMaster(
|
||||
'add-subtask',
|
||||
[perfParentId, `Component ${i}`],
|
||||
{ cwd: testDir }
|
||||
)
|
||||
);
|
||||
}
|
||||
await Promise.all(promises);
|
||||
|
||||
// Update all to in_progress
|
||||
const subtaskIds = [];
|
||||
for (let i = 1; i <= 20; i++) {
|
||||
subtaskIds.push(`${perfParentId}.${i}`);
|
||||
}
|
||||
|
||||
const startTime = Date.now();
|
||||
// Update subtask
|
||||
const result = await helpers.taskMaster(
|
||||
'update-subtask',
|
||||
['--ids', subtaskIds.join(','), '--status', 'in_progress'],
|
||||
[subtaskId, '--status', 'in_progress'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Command failed: ${result.stderr}`);
|
||||
}
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
logger.info(`Updated 20 subtasks in ${duration}ms`);
|
||||
if (duration > 5000) {
|
||||
throw new Error(`Bulk update too slow: ${duration}ms`);
|
||||
}
|
||||
// Verify both statuses
|
||||
const showResult = await helpers.taskMaster('show', [parentTaskId], { cwd: testDir });
|
||||
expect(showResult.stdout.toLowerCase()).toContain('in_progress');
|
||||
});
|
||||
|
||||
// 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=== Update-Subtask 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 update-subtask tests passed!');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
results.status = 'failed';
|
||||
results.errors.push({
|
||||
test: 'update-subtask test suite',
|
||||
error: error.message,
|
||||
stack: error.stack
|
||||
});
|
||||
logger.error(`Update-subtask test suite failed: ${error.message}`);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,484 +1,554 @@
|
||||
/**
|
||||
* Comprehensive E2E tests for update-task command
|
||||
* Tests all aspects of task updates including AI-powered and manual updates
|
||||
* Comprehensive E2E tests for update-task command (single task update)
|
||||
* Tests all aspects of single task updates including AI-powered updates
|
||||
*/
|
||||
|
||||
export default async function testUpdateTask(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('update-task command', () => {
|
||||
let testDir;
|
||||
let helpers;
|
||||
let taskId;
|
||||
|
||||
beforeEach(async () => {
|
||||
// Create test directory
|
||||
testDir = mkdtempSync(join(tmpdir(), 'task-master-update-task-'));
|
||||
|
||||
// Initialize test helpers
|
||||
const context = global.createTestContext('update-task');
|
||||
helpers = context.helpers;
|
||||
|
||||
// Copy .env file if it exists
|
||||
const mainEnvPath = join(__dirname, '../../../../.env');
|
||||
const testEnvPath = join(testDir, '.env');
|
||||
if (existsSync(mainEnvPath)) {
|
||||
const envContent = readFileSync(mainEnvPath, 'utf8');
|
||||
writeFileSync(testEnvPath, envContent);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
logger.info('Starting comprehensive update-task tests...');
|
||||
|
||||
// Setup: Create various tasks for testing
|
||||
logger.info('Setting up test tasks...');
|
||||
|
||||
// Create simple task
|
||||
const simpleResult = await helpers.taskMaster(
|
||||
// Initialize task-master project
|
||||
const initResult = await helpers.taskMaster('init', ['-y'], { cwd: testDir });
|
||||
expect(initResult).toHaveExitCode(0);
|
||||
|
||||
// Create a test task for updates
|
||||
const addResult = await helpers.taskMaster(
|
||||
'add-task',
|
||||
['--title', 'Simple task', '--description', 'Initial description'],
|
||||
['--title', 'Initial task', '--description', 'Task to be updated'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
const simpleTaskId = helpers.extractTaskId(simpleResult.stdout);
|
||||
|
||||
// Create AI task
|
||||
const aiResult = await helpers.taskMaster(
|
||||
'add-task',
|
||||
['--prompt', 'Create a logging system for the application'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
const aiTaskId = helpers.extractTaskId(aiResult.stdout);
|
||||
|
||||
// Create task with metadata
|
||||
const metaResult = await helpers.taskMaster(
|
||||
'add-task',
|
||||
['--title', 'Task with metadata', '--metadata', 'version=1.0'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
const metaTaskId = helpers.extractTaskId(metaResult.stdout);
|
||||
taskId = helpers.extractTaskId(addResult.stdout);
|
||||
});
|
||||
|
||||
// Test 1: Basic manual update - description only
|
||||
await runTest('Basic description update', async () => {
|
||||
afterEach(() => {
|
||||
// Clean up test directory
|
||||
if (testDir && existsSync(testDir)) {
|
||||
rmSync(testDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
describe('Basic task updates', () => {
|
||||
it('should update task description', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'update-task',
|
||||
[simpleTaskId, '--description', 'Updated description with more details'],
|
||||
[taskId, '--description', 'Updated task description with more details'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Command failed: ${result.stderr}`);
|
||||
}
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('Updated task');
|
||||
|
||||
// Verify update
|
||||
const showResult = await helpers.taskMaster('show', [simpleTaskId], { cwd: testDir });
|
||||
if (!showResult.stdout.includes('Updated description with more details')) {
|
||||
throw new Error('Description not updated');
|
||||
}
|
||||
const showResult = await helpers.taskMaster('show', [taskId], { cwd: testDir });
|
||||
expect(showResult.stdout).toContain('Updated task description');
|
||||
});
|
||||
|
||||
// Test 2: AI-powered task update
|
||||
await runTest('AI-powered task update', async () => {
|
||||
it('should update task title', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'update-task',
|
||||
[aiTaskId, '--prompt', 'Add requirements for structured logging with log levels and rotation'],
|
||||
{ cwd: testDir, timeout: 120000 }
|
||||
[taskId, '--title', 'Completely new title'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Command failed: ${result.stderr}`);
|
||||
}
|
||||
|
||||
// Verify AI enhancements
|
||||
const showResult = await helpers.taskMaster('show', [aiTaskId], { cwd: testDir });
|
||||
const output = showResult.stdout.toLowerCase();
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
// Should mention logging concepts
|
||||
const hasLoggingConcepts = output.includes('log level') ||
|
||||
output.includes('rotation') ||
|
||||
output.includes('structured') ||
|
||||
output.includes('logging');
|
||||
if (!hasLoggingConcepts) {
|
||||
throw new Error('AI did not enhance task with logging requirements');
|
||||
}
|
||||
// Verify update
|
||||
const showResult = await helpers.taskMaster('show', [taskId], { cwd: testDir });
|
||||
expect(showResult.stdout).toContain('Completely new title');
|
||||
});
|
||||
|
||||
// Test 3: Update multiple fields simultaneously
|
||||
await runTest('Multi-field update', async () => {
|
||||
it('should update task priority', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'update-task',
|
||||
[taskId, '--priority', 'high'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
// Verify update
|
||||
const showResult = await helpers.taskMaster('show', [taskId], { cwd: testDir });
|
||||
expect(showResult.stdout.toLowerCase()).toContain('high');
|
||||
});
|
||||
|
||||
it('should update task details', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'update-task',
|
||||
[taskId, '--details', 'Implementation notes: Use async/await pattern'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
// Verify update
|
||||
const showResult = await helpers.taskMaster('show', [taskId], { cwd: testDir });
|
||||
expect(showResult.stdout).toContain('async/await');
|
||||
});
|
||||
});
|
||||
|
||||
describe('AI-powered updates', () => {
|
||||
it('should update task using AI prompt', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'update-task',
|
||||
[taskId, '--prompt', 'Add security considerations and best practices'],
|
||||
{ cwd: testDir, timeout: 45000 }
|
||||
);
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('Updated task');
|
||||
|
||||
// Verify AI added security content
|
||||
const showResult = await helpers.taskMaster('show', [taskId], { cwd: testDir });
|
||||
const hasSecurityInfo = showResult.stdout.toLowerCase().includes('security') ||
|
||||
showResult.stdout.toLowerCase().includes('practice');
|
||||
expect(hasSecurityInfo).toBe(true);
|
||||
}, 60000);
|
||||
|
||||
it('should enhance task with AI suggestions', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'update-task',
|
||||
[taskId, '--prompt', 'Break this down into subtasks and add implementation details'],
|
||||
{ cwd: testDir, timeout: 45000 }
|
||||
);
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
// Check that task was enhanced
|
||||
const tasksPath = join(testDir, '.taskmaster/tasks/tasks.json');
|
||||
const tasks = JSON.parse(readFileSync(tasksPath, 'utf8'));
|
||||
const updatedTask = tasks.master.tasks.find(t => t.id === parseInt(taskId));
|
||||
|
||||
// Should have more detailed content
|
||||
expect(updatedTask.details.length).toBeGreaterThan(50);
|
||||
}, 60000);
|
||||
|
||||
it('should update task with research mode', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'update-task',
|
||||
[
|
||||
simpleTaskId,
|
||||
'--title', 'Renamed task',
|
||||
'--description', 'New comprehensive description',
|
||||
taskId,
|
||||
'--prompt', 'Add current industry best practices for authentication',
|
||||
'--research'
|
||||
],
|
||||
{ cwd: testDir, timeout: 90000 }
|
||||
);
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
// Research mode should add comprehensive content
|
||||
const showResult = await helpers.taskMaster('show', [taskId], { cwd: testDir });
|
||||
expect(showResult.stdout.length).toBeGreaterThan(500);
|
||||
}, 120000);
|
||||
});
|
||||
|
||||
describe('Multiple field updates', () => {
|
||||
it('should update multiple fields at once', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'update-task',
|
||||
[
|
||||
taskId,
|
||||
'--title', 'New comprehensive title',
|
||||
'--description', 'New detailed description',
|
||||
'--priority', 'high',
|
||||
'--status', 'in_progress'
|
||||
'--details', 'Additional implementation notes'
|
||||
],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Command failed: ${result.stderr}`);
|
||||
}
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
// Verify all updates
|
||||
const showResult = await helpers.taskMaster('show', [simpleTaskId], { cwd: testDir });
|
||||
if (!showResult.stdout.includes('Renamed task')) {
|
||||
throw new Error('Title not updated');
|
||||
}
|
||||
if (!showResult.stdout.includes('New comprehensive description')) {
|
||||
throw new Error('Description not updated');
|
||||
}
|
||||
if (!showResult.stdout.includes('high') && !showResult.stdout.includes('High')) {
|
||||
throw new Error('Priority not updated');
|
||||
}
|
||||
if (!showResult.stdout.includes('in_progress') && !showResult.stdout.includes('In Progress')) {
|
||||
throw new Error('Status not updated');
|
||||
}
|
||||
const showResult = await helpers.taskMaster('show', [taskId], { cwd: testDir });
|
||||
expect(showResult.stdout).toContain('New comprehensive title');
|
||||
expect(showResult.stdout).toContain('New detailed description');
|
||||
expect(showResult.stdout.toLowerCase()).toContain('high');
|
||||
expect(showResult.stdout).toContain('Additional implementation notes');
|
||||
});
|
||||
|
||||
// Test 4: Update task metadata
|
||||
await runTest('Update task metadata', async () => {
|
||||
it('should combine manual updates with AI prompt', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'update-task',
|
||||
[
|
||||
metaTaskId,
|
||||
'--metadata', 'version=2.0',
|
||||
'--metadata', 'author=test-user',
|
||||
'--metadata', 'reviewed=true'
|
||||
taskId,
|
||||
'--priority', 'high',
|
||||
'--prompt', 'Add technical requirements and dependencies'
|
||||
],
|
||||
{ cwd: testDir, timeout: 45000 }
|
||||
);
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
// Verify both manual and AI updates
|
||||
const showResult = await helpers.taskMaster('show', [taskId], { cwd: testDir });
|
||||
expect(showResult.stdout.toLowerCase()).toContain('high');
|
||||
const hasTechnicalInfo = showResult.stdout.toLowerCase().includes('requirement') ||
|
||||
showResult.stdout.toLowerCase().includes('dependenc');
|
||||
expect(hasTechnicalInfo).toBe(true);
|
||||
}, 60000);
|
||||
});
|
||||
|
||||
describe('Task metadata updates', () => {
|
||||
it('should add tags to task', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'update-task',
|
||||
[taskId, '--add-tags', 'backend,api,urgent'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Command failed: ${result.stderr}`);
|
||||
}
|
||||
|
||||
// Verify metadata
|
||||
const tasksJson = helpers.readJson(`${testDir}/.taskmaster/tasks/tasks.json`);
|
||||
const task = tasksJson.tasks.find(t => t.id === metaTaskId);
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
if (!task.metadata || task.metadata.version !== '2.0' ||
|
||||
task.metadata.author !== 'test-user' || task.metadata.reviewed !== 'true') {
|
||||
throw new Error('Metadata not properly updated');
|
||||
}
|
||||
// Verify tags were added
|
||||
const showResult = await helpers.taskMaster('show', [taskId], { cwd: testDir });
|
||||
expect(showResult.stdout).toContain('backend');
|
||||
expect(showResult.stdout).toContain('api');
|
||||
expect(showResult.stdout).toContain('urgent');
|
||||
});
|
||||
|
||||
// Test 5: Error handling - non-existent task
|
||||
await runTest('Error handling - non-existent task', async () => {
|
||||
it('should remove tags from task', async () => {
|
||||
// First add tags
|
||||
await helpers.taskMaster(
|
||||
'update-task',
|
||||
[taskId, '--add-tags', 'frontend,ui,design'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
// Then remove some
|
||||
const result = await helpers.taskMaster(
|
||||
'update-task',
|
||||
[taskId, '--remove-tags', 'ui,design'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
// Verify tags were removed
|
||||
const showResult = await helpers.taskMaster('show', [taskId], { cwd: testDir });
|
||||
expect(showResult.stdout).toContain('frontend');
|
||||
expect(showResult.stdout).not.toContain('ui');
|
||||
expect(showResult.stdout).not.toContain('design');
|
||||
});
|
||||
|
||||
it('should update due date', async () => {
|
||||
const futureDate = new Date();
|
||||
futureDate.setDate(futureDate.getDate() + 7);
|
||||
const dateStr = futureDate.toISOString().split('T')[0];
|
||||
|
||||
const result = await helpers.taskMaster(
|
||||
'update-task',
|
||||
[taskId, '--due-date', dateStr],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
// Verify due date was set
|
||||
const showResult = await helpers.taskMaster('show', [taskId], { cwd: testDir });
|
||||
expect(showResult.stdout).toContain(dateStr);
|
||||
});
|
||||
|
||||
it('should update estimated time', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'update-task',
|
||||
[taskId, '--estimated-time', '4h'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
// Verify estimated time was set
|
||||
const showResult = await helpers.taskMaster('show', [taskId], { cwd: testDir });
|
||||
expect(showResult.stdout).toContain('4h');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Status updates', () => {
|
||||
it('should update task status', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'update-task',
|
||||
[taskId, '--status', 'in_progress'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
// Verify status change
|
||||
const showResult = await helpers.taskMaster('show', [taskId], { cwd: testDir });
|
||||
expect(showResult.stdout.toLowerCase()).toContain('in_progress');
|
||||
});
|
||||
|
||||
it('should mark task as completed', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'update-task',
|
||||
[taskId, '--status', 'completed'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
// Verify completion
|
||||
const showResult = await helpers.taskMaster('show', [taskId], { cwd: testDir });
|
||||
expect(showResult.stdout.toLowerCase()).toContain('completed');
|
||||
});
|
||||
|
||||
it('should mark task as blocked with reason', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'update-task',
|
||||
[taskId, '--status', 'blocked', '--blocked-reason', 'Waiting for API access'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
// Verify blocked status and reason
|
||||
const showResult = await helpers.taskMaster('show', [taskId], { cwd: testDir });
|
||||
expect(showResult.stdout.toLowerCase()).toContain('blocked');
|
||||
expect(showResult.stdout).toContain('Waiting for API access');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Append mode', () => {
|
||||
it('should append to description', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'update-task',
|
||||
[taskId, '--append-description', '\nAdditional requirements added.'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
// Verify description was appended
|
||||
const showResult = await helpers.taskMaster('show', [taskId], { cwd: testDir });
|
||||
expect(showResult.stdout).toContain('Task to be updated');
|
||||
expect(showResult.stdout).toContain('Additional requirements added');
|
||||
});
|
||||
|
||||
it('should append to details', async () => {
|
||||
// First set some details
|
||||
await helpers.taskMaster(
|
||||
'update-task',
|
||||
[taskId, '--details', 'Initial implementation notes.'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
// Then append
|
||||
const result = await helpers.taskMaster(
|
||||
'update-task',
|
||||
[taskId, '--append-details', '\nPerformance considerations added.'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
// Verify details were appended
|
||||
const showResult = await helpers.taskMaster('show', [taskId], { cwd: testDir });
|
||||
expect(showResult.stdout).toContain('Initial implementation notes');
|
||||
expect(showResult.stdout).toContain('Performance considerations added');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Tag-specific updates', () => {
|
||||
it('should update task in specific tag', async () => {
|
||||
// Create a tag and move task to it
|
||||
await helpers.taskMaster('add-tag', ['feature-x'], { cwd: testDir });
|
||||
await helpers.taskMaster(
|
||||
'add-task',
|
||||
['--prompt', 'Task in feature-x', '--tag', 'feature-x'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
// Get task ID from feature-x tag
|
||||
const listResult = await helpers.taskMaster('list', ['--tag', 'feature-x'], { cwd: testDir });
|
||||
const featureTaskId = helpers.extractTaskId(listResult.stdout);
|
||||
|
||||
// Update task in specific tag
|
||||
const result = await helpers.taskMaster(
|
||||
'update-task',
|
||||
[featureTaskId, '--description', 'Updated in feature tag', '--tag', 'feature-x'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
// Verify update in correct tag
|
||||
const showResult = await helpers.taskMaster(
|
||||
'show',
|
||||
[featureTaskId, '--tag', 'feature-x'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
expect(showResult.stdout).toContain('Updated in feature tag');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Output formats', () => {
|
||||
it('should output in JSON format', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'update-task',
|
||||
[taskId, '--description', 'JSON test update', '--output', 'json'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
// Output should be valid JSON
|
||||
const jsonOutput = JSON.parse(result.stdout);
|
||||
expect(jsonOutput.success).toBe(true);
|
||||
expect(jsonOutput.task).toBeDefined();
|
||||
expect(jsonOutput.task.description).toBe('JSON test update');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error handling', () => {
|
||||
it('should fail with non-existent task ID', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'update-task',
|
||||
['99999', '--description', 'This should fail'],
|
||||
{ cwd: testDir, allowFailure: true }
|
||||
);
|
||||
if (result.exitCode === 0) {
|
||||
throw new Error('Should have failed with non-existent task');
|
||||
}
|
||||
if (!result.stderr.includes('not found') && !result.stderr.includes('exist')) {
|
||||
throw new Error('Error message not clear about missing task');
|
||||
}
|
||||
|
||||
expect(result.exitCode).not.toBe(0);
|
||||
expect(result.stderr).toContain('not found');
|
||||
});
|
||||
|
||||
// Test 6: Update with validation of AI output
|
||||
await runTest('AI update with validation', async () => {
|
||||
// Create task with specific context
|
||||
const validationResult = await helpers.taskMaster(
|
||||
'add-task',
|
||||
['--prompt', 'Setup CI/CD pipeline'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
const validationTaskId = helpers.extractTaskId(validationResult.stdout);
|
||||
|
||||
// Update with specific requirements
|
||||
it('should fail with invalid priority', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'update-task',
|
||||
[validationTaskId, '--prompt', 'Add automated testing and deployment stages', '--validate-ai'],
|
||||
{ cwd: testDir, timeout: 120000 }
|
||||
);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Command failed: ${result.stderr}`);
|
||||
}
|
||||
|
||||
// Check AI added relevant content
|
||||
const showResult = await helpers.taskMaster('show', [validationTaskId], { cwd: testDir });
|
||||
const output = showResult.stdout.toLowerCase();
|
||||
|
||||
if (!output.includes('test') || !output.includes('deploy')) {
|
||||
throw new Error('AI validation failed - missing required concepts');
|
||||
}
|
||||
});
|
||||
|
||||
// Test 7: Update task with tag changes
|
||||
await runTest('Update task tags', async () => {
|
||||
// Create tags
|
||||
await helpers.taskMaster('add-tag', ['frontend'], { cwd: testDir });
|
||||
await helpers.taskMaster('add-tag', ['urgent'], { cwd: testDir });
|
||||
|
||||
const result = await helpers.taskMaster(
|
||||
'update-task',
|
||||
[simpleTaskId, '--add-tag', 'frontend', '--add-tag', 'urgent'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Command failed: ${result.stderr}`);
|
||||
}
|
||||
|
||||
// Verify tags in appropriate contexts
|
||||
const frontendList = await helpers.taskMaster('list', ['--tag', 'frontend'], { cwd: testDir });
|
||||
const urgentList = await helpers.taskMaster('list', ['--tag', 'urgent'], { cwd: testDir });
|
||||
|
||||
if (!frontendList.stdout.includes(simpleTaskId)) {
|
||||
throw new Error('Task not found in frontend tag');
|
||||
}
|
||||
if (!urgentList.stdout.includes(simpleTaskId)) {
|
||||
throw new Error('Task not found in urgent tag');
|
||||
}
|
||||
});
|
||||
|
||||
// Test 8: Remove tags from task
|
||||
await runTest('Remove task tags', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'update-task',
|
||||
[simpleTaskId, '--remove-tag', 'urgent'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Command failed: ${result.stderr}`);
|
||||
}
|
||||
|
||||
// Verify tag removed
|
||||
const urgentList = await helpers.taskMaster('list', ['--tag', 'urgent'], { cwd: testDir });
|
||||
if (urgentList.stdout.includes(simpleTaskId)) {
|
||||
throw new Error('Task still in removed tag');
|
||||
}
|
||||
});
|
||||
|
||||
// Test 9: Update with dependencies
|
||||
await runTest('Update task dependencies', async () => {
|
||||
// Create dependency task
|
||||
const depResult = await helpers.taskMaster(
|
||||
'add-task',
|
||||
['--title', 'Dependency task'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
const depTaskId = helpers.extractTaskId(depResult.stdout);
|
||||
|
||||
const result = await helpers.taskMaster(
|
||||
'update-task',
|
||||
[aiTaskId, '--add-dependency', depTaskId],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Command failed: ${result.stderr}`);
|
||||
}
|
||||
|
||||
// Verify dependency added
|
||||
const showResult = await helpers.taskMaster('show', [aiTaskId], { cwd: testDir });
|
||||
if (!showResult.stdout.includes(depTaskId)) {
|
||||
throw new Error('Dependency not added to task');
|
||||
}
|
||||
});
|
||||
|
||||
// Test 10: Complex AI enhancement
|
||||
await runTest('Complex AI task enhancement', async () => {
|
||||
// Create task needing enhancement
|
||||
const enhanceResult = await helpers.taskMaster(
|
||||
'add-task',
|
||||
['--title', 'Basic API endpoint', '--description', 'Create user endpoint'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
const enhanceTaskId = helpers.extractTaskId(enhanceResult.stdout);
|
||||
|
||||
const result = await helpers.taskMaster(
|
||||
'update-task',
|
||||
[
|
||||
enhanceTaskId,
|
||||
'--prompt', 'Enhance with REST best practices, error handling, validation, and OpenAPI documentation',
|
||||
'--keep-original'
|
||||
],
|
||||
{ cwd: testDir, timeout: 120000 }
|
||||
);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Command failed: ${result.stderr}`);
|
||||
}
|
||||
|
||||
// Should preserve original and add enhancements
|
||||
const showResult = await helpers.taskMaster('show', [enhanceTaskId], { cwd: testDir });
|
||||
if (!showResult.stdout.includes('user endpoint')) {
|
||||
throw new Error('Original content lost during enhancement');
|
||||
}
|
||||
|
||||
// Check for enhancements
|
||||
const output = showResult.stdout.toLowerCase();
|
||||
const enhancements = ['validation', 'error', 'rest', 'openapi', 'documentation'];
|
||||
const foundEnhancements = enhancements.filter(e => output.includes(e)).length;
|
||||
|
||||
if (foundEnhancements < 3) {
|
||||
throw new Error('AI did not add sufficient enhancements');
|
||||
}
|
||||
});
|
||||
|
||||
// Test 11: Bulk property update
|
||||
await runTest('Update common properties across tasks', async () => {
|
||||
// Update all tasks to have a common property
|
||||
const taskIds = [simpleTaskId, aiTaskId, metaTaskId];
|
||||
|
||||
// This tests if update-task can handle multiple IDs (implementation dependent)
|
||||
// If not supported, test single updates in sequence
|
||||
for (const taskId of taskIds) {
|
||||
const result = await helpers.taskMaster(
|
||||
'update-task',
|
||||
[taskId, '--metadata', 'project=test-suite'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Failed to update task ${taskId}: ${result.stderr}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Verify all have the metadata
|
||||
const tasksJson = helpers.readJson(`${testDir}/.taskmaster/tasks/tasks.json`);
|
||||
taskIds.forEach(taskId => {
|
||||
const task = tasksJson.tasks.find(t => t.id === taskId);
|
||||
if (!task.metadata || task.metadata.project !== 'test-suite') {
|
||||
throw new Error(`Task ${taskId} missing project metadata`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Test 12: Update completed task
|
||||
await runTest('Update completed task handling', async () => {
|
||||
// Complete a task first
|
||||
await helpers.taskMaster('set-status', [simpleTaskId, 'completed'], { cwd: testDir });
|
||||
|
||||
// Try to update it
|
||||
const result = await helpers.taskMaster(
|
||||
'update-task',
|
||||
[simpleTaskId, '--description', 'Trying to update completed task'],
|
||||
[taskId, '--priority', 'invalid-priority'],
|
||||
{ cwd: testDir, allowFailure: true }
|
||||
);
|
||||
|
||||
// Should either fail with clear message or succeed with warning
|
||||
if (result.exitCode !== 0) {
|
||||
if (!result.stderr.includes('completed')) {
|
||||
throw new Error('No clear message about updating completed task');
|
||||
}
|
||||
} else if (!result.stdout.includes('warning') && !result.stdout.includes('completed')) {
|
||||
throw new Error('No warning about updating completed task');
|
||||
}
|
||||
expect(result.exitCode).not.toBe(0);
|
||||
expect(result.stderr).toContain('Invalid priority');
|
||||
});
|
||||
|
||||
// Test 13: Update with context preservation
|
||||
await runTest('Context-aware AI update', async () => {
|
||||
// Create task with rich context
|
||||
const contextResult = await helpers.taskMaster(
|
||||
it('should fail with invalid status', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'update-task',
|
||||
[taskId, '--status', 'invalid-status'],
|
||||
{ cwd: testDir, allowFailure: true }
|
||||
);
|
||||
|
||||
expect(result.exitCode).not.toBe(0);
|
||||
expect(result.stderr).toContain('Invalid status');
|
||||
});
|
||||
|
||||
it('should fail without any update parameters', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'update-task',
|
||||
[taskId],
|
||||
{ cwd: testDir, allowFailure: true }
|
||||
);
|
||||
|
||||
expect(result.exitCode).not.toBe(0);
|
||||
expect(result.stderr).toContain('No updates specified');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Performance and edge cases', () => {
|
||||
it('should handle very long descriptions', async () => {
|
||||
const longDescription = 'This is a very detailed description. '.repeat(50);
|
||||
|
||||
const result = await helpers.taskMaster(
|
||||
'update-task',
|
||||
[taskId, '--description', longDescription],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
// Verify long description was saved
|
||||
const tasksPath = join(testDir, '.taskmaster/tasks/tasks.json');
|
||||
const tasks = JSON.parse(readFileSync(tasksPath, 'utf8'));
|
||||
const updatedTask = tasks.master.tasks.find(t => t.id === parseInt(taskId));
|
||||
expect(updatedTask.description).toBe(longDescription);
|
||||
});
|
||||
|
||||
it('should preserve task relationships during updates', async () => {
|
||||
// Add a dependency
|
||||
const depResult = await helpers.taskMaster(
|
||||
'add-task',
|
||||
['--prompt', 'Implement user profile page with React'],
|
||||
['--title', 'Dependency task', '--description', 'Must be done first'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
const contextTaskId = helpers.extractTaskId(contextResult.stdout);
|
||||
const depId = helpers.extractTaskId(depResult.stdout);
|
||||
|
||||
// Expand to add subtasks
|
||||
await helpers.taskMaster('expand', [contextTaskId], { cwd: testDir, timeout: 120000 });
|
||||
|
||||
// Update with context preservation
|
||||
const result = await helpers.taskMaster(
|
||||
'update-task',
|
||||
[contextTaskId, '--prompt', 'Add accessibility features', '--preserve-context'],
|
||||
{ cwd: testDir, timeout: 120000 }
|
||||
);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Command failed: ${result.stderr}`);
|
||||
}
|
||||
|
||||
// Should maintain React context and add accessibility
|
||||
const showResult = await helpers.taskMaster('show', [contextTaskId], { cwd: testDir });
|
||||
const output = showResult.stdout.toLowerCase();
|
||||
|
||||
if (!output.includes('react')) {
|
||||
throw new Error('Lost React context during update');
|
||||
}
|
||||
if (!output.includes('accessibility') && !output.includes('a11y') && !output.includes('aria')) {
|
||||
throw new Error('Accessibility features not added');
|
||||
}
|
||||
});
|
||||
|
||||
// Test 14: Update with estimation
|
||||
await runTest('Update task with time estimation', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'update-task',
|
||||
[
|
||||
aiTaskId,
|
||||
'--estimate', '8h',
|
||||
'--metadata', 'story_points=5'
|
||||
],
|
||||
await helpers.taskMaster(
|
||||
'add-dependency',
|
||||
['--id', taskId, '--depends-on', depId],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Command failed: ${result.stderr}`);
|
||||
}
|
||||
|
||||
// Verify estimation added
|
||||
const tasksJson = helpers.readJson(`${testDir}/.taskmaster/tasks/tasks.json`);
|
||||
const task = tasksJson.tasks.find(t => t.id === aiTaskId);
|
||||
|
||||
if (!task.estimate || !task.estimate.includes('8h')) {
|
||||
throw new Error('Time estimate not added');
|
||||
}
|
||||
if (!task.metadata || task.metadata.story_points !== '5') {
|
||||
throw new Error('Story points not added');
|
||||
}
|
||||
});
|
||||
|
||||
// Test 15: Performance - large description update
|
||||
await runTest('Performance - large content update', async () => {
|
||||
// Create large description
|
||||
const largeDescription = 'This is a detailed task description. '.repeat(100) +
|
||||
'\n\n## Requirements\n' +
|
||||
'- Requirement item\n'.repeat(50);
|
||||
|
||||
const startTime = Date.now();
|
||||
// Update the task
|
||||
const result = await helpers.taskMaster(
|
||||
'update-task',
|
||||
[metaTaskId, '--description', largeDescription],
|
||||
[taskId, '--description', 'Updated with dependencies intact'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Command failed: ${result.stderr}`);
|
||||
}
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
logger.info(`Large update completed in ${duration}ms`);
|
||||
if (duration > 5000) {
|
||||
throw new Error(`Update too slow: ${duration}ms`);
|
||||
}
|
||||
|
||||
// Verify content was saved
|
||||
const showResult = await helpers.taskMaster('show', [metaTaskId], { cwd: testDir });
|
||||
if (!showResult.stdout.includes('detailed task description')) {
|
||||
throw new Error('Large description not saved properly');
|
||||
}
|
||||
// Verify dependency is preserved
|
||||
const showResult = await helpers.taskMaster('show', [taskId], { cwd: testDir });
|
||||
expect(showResult.stdout).toContain(depId);
|
||||
});
|
||||
});
|
||||
|
||||
// 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=== Update-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 update-task tests passed!');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
results.status = 'failed';
|
||||
results.errors.push({
|
||||
test: 'update-task test suite',
|
||||
error: error.message,
|
||||
stack: error.stack
|
||||
describe('Dry run mode', () => {
|
||||
it('should preview updates without applying them', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'update-task',
|
||||
[taskId, '--description', 'Dry run test', '--dry-run'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('DRY RUN');
|
||||
expect(result.stdout).toContain('Would update');
|
||||
|
||||
// Verify task was NOT actually updated
|
||||
const showResult = await helpers.taskMaster('show', [taskId], { cwd: testDir });
|
||||
expect(showResult.stdout).not.toContain('Dry run test');
|
||||
});
|
||||
logger.error(`Update-task test suite failed: ${error.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
return results;
|
||||
}
|
||||
describe('Integration with other commands', () => {
|
||||
it('should work with expand after update', async () => {
|
||||
// Update task with AI
|
||||
await helpers.taskMaster(
|
||||
'update-task',
|
||||
[taskId, '--prompt', 'Add implementation steps'],
|
||||
{ cwd: testDir, timeout: 45000 }
|
||||
);
|
||||
|
||||
// Then expand it
|
||||
const expandResult = await helpers.taskMaster(
|
||||
'expand',
|
||||
['--id', taskId],
|
||||
{ cwd: testDir, timeout: 45000 }
|
||||
);
|
||||
|
||||
expect(expandResult).toHaveExitCode(0);
|
||||
expect(expandResult.stdout).toContain('Expanded task');
|
||||
}, 90000);
|
||||
});
|
||||
});
|
||||
@@ -1,483 +1,449 @@
|
||||
/**
|
||||
* Comprehensive E2E tests for update-tasks command
|
||||
* Tests bulk task update functionality with various filters and AI capabilities
|
||||
* Comprehensive E2E tests for update-tasks command (bulk update)
|
||||
* Tests all aspects of bulk task updates including AI-powered updates
|
||||
*/
|
||||
|
||||
export default async function testUpdateTasks(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('update-tasks command', () => {
|
||||
let testDir;
|
||||
let helpers;
|
||||
|
||||
try {
|
||||
logger.info('Starting comprehensive update-tasks tests...');
|
||||
|
||||
// Setup: Create a variety of tasks for bulk operations
|
||||
logger.info('Setting up test tasks for bulk operations...');
|
||||
beforeEach(async () => {
|
||||
// Create test directory
|
||||
testDir = mkdtempSync(join(tmpdir(), 'task-master-update-tasks-'));
|
||||
|
||||
// Create tasks with different statuses
|
||||
const taskIds = [];
|
||||
// Initialize test helpers
|
||||
const context = global.createTestContext('update-tasks');
|
||||
helpers = context.helpers;
|
||||
|
||||
// Pending tasks
|
||||
for (let i = 1; i <= 3; i++) {
|
||||
const result = await helpers.taskMaster(
|
||||
'add-task',
|
||||
['--title', `Pending task ${i}`, '--priority', i === 1 ? 'high' : 'medium'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
taskIds.push(helpers.extractTaskId(result.stdout));
|
||||
// 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);
|
||||
}
|
||||
|
||||
// In-progress tasks
|
||||
for (let i = 1; i <= 2; i++) {
|
||||
const result = await helpers.taskMaster(
|
||||
'add-task',
|
||||
['--title', `In-progress task ${i}`],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
const taskId = helpers.extractTaskId(result.stdout);
|
||||
taskIds.push(taskId);
|
||||
await helpers.taskMaster('set-status', [taskId, 'in_progress'], { cwd: testDir });
|
||||
}
|
||||
// Initialize task-master project
|
||||
const initResult = await helpers.taskMaster('init', ['-y'], { cwd: testDir });
|
||||
expect(initResult).toHaveExitCode(0);
|
||||
|
||||
// Tasks with tags
|
||||
await helpers.taskMaster('add-tag', ['backend'], { cwd: testDir });
|
||||
await helpers.taskMaster('add-tag', ['frontend'], { cwd: testDir });
|
||||
|
||||
for (let i = 1; i <= 2; i++) {
|
||||
const result = await helpers.taskMaster(
|
||||
'add-task',
|
||||
['--title', `Backend task ${i}`, '--tag', 'backend'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
taskIds.push(helpers.extractTaskId(result.stdout));
|
||||
// Create some test tasks for bulk updates
|
||||
const tasksPath = join(testDir, '.taskmaster/tasks/tasks.json');
|
||||
const tasksData = {
|
||||
master: {
|
||||
tasks: [
|
||||
{
|
||||
id: 1,
|
||||
title: "Setup authentication",
|
||||
description: "Implement user authentication",
|
||||
priority: "medium",
|
||||
status: "pending",
|
||||
details: "Basic auth implementation"
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "Create database schema",
|
||||
description: "Design database structure",
|
||||
priority: "high",
|
||||
status: "pending",
|
||||
details: "PostgreSQL schema"
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: "Build API endpoints",
|
||||
description: "RESTful API development",
|
||||
priority: "medium",
|
||||
status: "in_progress",
|
||||
details: "Express.js endpoints"
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
mkdirSync(join(testDir, '.taskmaster/tasks'), { recursive: true });
|
||||
writeFileSync(tasksPath, JSON.stringify(tasksData, null, 2));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Clean up test directory
|
||||
if (testDir && existsSync(testDir)) {
|
||||
rmSync(testDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
// Test 1: Bulk update by status
|
||||
await runTest('Bulk update tasks by status', async () => {
|
||||
describe('Bulk task updates with prompts', () => {
|
||||
it('should update all tasks with general prompt', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'update-tasks',
|
||||
['--status', 'pending', '--set-priority', 'high'],
|
||||
{ cwd: testDir }
|
||||
['--prompt', 'Add security considerations to all tasks'],
|
||||
{ cwd: testDir, timeout: 45000 }
|
||||
);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Command failed: ${result.stderr}`);
|
||||
}
|
||||
|
||||
// Should report number of tasks updated
|
||||
if (!result.stdout.includes('updated') || !result.stdout.match(/\d+/)) {
|
||||
throw new Error('No update count reported');
|
||||
}
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('Updated');
|
||||
expect(result.stdout).toContain('task');
|
||||
|
||||
// Verify all pending tasks now have high priority
|
||||
const listResult = await helpers.taskMaster('list', ['--status', 'pending'], { cwd: testDir });
|
||||
const pendingTasks = listResult.stdout.match(/\d+\s*\|/g) || [];
|
||||
// Verify tasks were updated
|
||||
const tasksPath = join(testDir, '.taskmaster/tasks/tasks.json');
|
||||
const tasks = JSON.parse(readFileSync(tasksPath, 'utf8'));
|
||||
|
||||
// Check a sample task
|
||||
if (pendingTasks.length > 0) {
|
||||
const showResult = await helpers.taskMaster('show', [taskIds[0]], { cwd: testDir });
|
||||
if (!showResult.stdout.includes('high') && !showResult.stdout.includes('High')) {
|
||||
throw new Error('Priority not updated for pending tasks');
|
||||
}
|
||||
}
|
||||
});
|
||||
// Check that tasks have been modified (details should mention security)
|
||||
const hasSecurityUpdates = tasks.master.tasks.some(t =>
|
||||
t.details && t.details.toLowerCase().includes('security')
|
||||
);
|
||||
expect(hasSecurityUpdates).toBe(true);
|
||||
}, 60000);
|
||||
|
||||
// Test 2: Bulk update by tag
|
||||
await runTest('Bulk update tasks by tag', async () => {
|
||||
it('should update specific tasks by IDs', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'update-tasks',
|
||||
['--tag', 'backend', '--add-metadata', 'team=backend-team'],
|
||||
{ cwd: testDir }
|
||||
['--ids', '1,3', '--prompt', 'Add performance optimization notes'],
|
||||
{ cwd: testDir, timeout: 45000 }
|
||||
);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Command failed: ${result.stderr}`);
|
||||
}
|
||||
|
||||
// Verify backend tasks have metadata
|
||||
const listResult = await helpers.taskMaster('list', ['--tag', 'backend'], { cwd: testDir });
|
||||
const backendTaskIds = (listResult.stdout.match(/\d+(?=\s*\|)/g) || []);
|
||||
|
||||
if (backendTaskIds.length > 0) {
|
||||
const tasksJson = helpers.readJson(`${testDir}/.taskmaster/tasks/tasks.json`);
|
||||
const backendTask = tasksJson.tasks.find(t => backendTaskIds.includes(t.id));
|
||||
|
||||
if (!backendTask || !backendTask.metadata || backendTask.metadata.team !== 'backend-team') {
|
||||
throw new Error('Metadata not added to backend tasks');
|
||||
}
|
||||
}
|
||||
});
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('Updated 2 task');
|
||||
}, 60000);
|
||||
|
||||
// Test 3: Bulk update with AI enhancement
|
||||
await runTest('Bulk AI enhancement', async () => {
|
||||
it('should update tasks by status filter', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'update-tasks',
|
||||
['--tag', 'backend', '--enhance', '--prompt', 'Add security considerations'],
|
||||
{ cwd: testDir, timeout: 180000 }
|
||||
['--status', 'pending', '--prompt', 'Add estimated time requirements'],
|
||||
{ cwd: testDir, timeout: 45000 }
|
||||
);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Command failed: ${result.stderr}`);
|
||||
}
|
||||
|
||||
// Check that tasks were enhanced
|
||||
const listResult = await helpers.taskMaster('list', ['--tag', 'backend'], { cwd: testDir });
|
||||
const backendTaskIds = (listResult.stdout.match(/\d+(?=\s*\|)/g) || []);
|
||||
expect(result).toHaveExitCode(0);
|
||||
// Should update tasks 1 and 2 (pending status)
|
||||
expect(result.stdout).toContain('Updated 2 task');
|
||||
|
||||
if (backendTaskIds.length > 0) {
|
||||
const showResult = await helpers.taskMaster('show', [backendTaskIds[0]], { cwd: testDir });
|
||||
const hasSecurityMention = showResult.stdout.toLowerCase().includes('security') ||
|
||||
showResult.stdout.toLowerCase().includes('secure') ||
|
||||
showResult.stdout.toLowerCase().includes('auth');
|
||||
|
||||
if (!hasSecurityMention) {
|
||||
throw new Error('AI enhancement did not add security considerations');
|
||||
}
|
||||
}
|
||||
});
|
||||
const tasksPath = join(testDir, '.taskmaster/tasks/tasks.json');
|
||||
const tasks = JSON.parse(readFileSync(tasksPath, 'utf8'));
|
||||
|
||||
// Verify only pending tasks were updated
|
||||
const pendingTasks = tasks.master.tasks.filter(t => t.status === 'pending');
|
||||
const hasTimeEstimates = pendingTasks.some(t =>
|
||||
t.details && (t.details.includes('time') || t.details.includes('hour') || t.details.includes('day'))
|
||||
);
|
||||
expect(hasTimeEstimates).toBe(true);
|
||||
}, 60000);
|
||||
|
||||
// Test 4: Bulk status change
|
||||
await runTest('Bulk status change', async () => {
|
||||
it('should update tasks by priority filter', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'update-tasks',
|
||||
['--priority', 'high', '--set-status', 'in_progress'],
|
||||
{ cwd: testDir }
|
||||
['--priority', 'medium', '--prompt', 'Add testing requirements'],
|
||||
{ cwd: testDir, timeout: 45000 }
|
||||
);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Command failed: ${result.stderr}`);
|
||||
}
|
||||
|
||||
// Verify high priority tasks are now in progress
|
||||
const listResult = await helpers.taskMaster('list', ['--priority', 'high'], { cwd: testDir });
|
||||
const highPriorityIds = (listResult.stdout.match(/\d+(?=\s*\|)/g) || []);
|
||||
|
||||
if (highPriorityIds.length > 0) {
|
||||
const showResult = await helpers.taskMaster('show', [highPriorityIds[0]], { cwd: testDir });
|
||||
if (!showResult.stdout.includes('in_progress') && !showResult.stdout.includes('In Progress')) {
|
||||
throw new Error('Status not updated for high priority tasks');
|
||||
}
|
||||
}
|
||||
});
|
||||
expect(result).toHaveExitCode(0);
|
||||
// Should update tasks 1 and 3 (medium priority)
|
||||
expect(result.stdout).toContain('Updated 2 task');
|
||||
}, 60000);
|
||||
});
|
||||
|
||||
// Test 5: Bulk update with multiple filters
|
||||
await runTest('Bulk update with combined filters', async () => {
|
||||
// Add frontend tag to some tasks
|
||||
await helpers.taskMaster('update-task', [taskIds[0], '--add-tag', 'frontend'], { cwd: testDir });
|
||||
await helpers.taskMaster('update-task', [taskIds[1], '--add-tag', 'frontend'], { cwd: testDir });
|
||||
|
||||
const result = await helpers.taskMaster(
|
||||
'update-tasks',
|
||||
['--tag', 'frontend', '--status', 'in_progress', '--add-metadata', 'urgent=true'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Command failed: ${result.stderr}`);
|
||||
}
|
||||
|
||||
// Should only update tasks matching both filters
|
||||
const updateCount = result.stdout.match(/(\d+) tasks? updated/);
|
||||
if (!updateCount) {
|
||||
throw new Error('Update count not reported');
|
||||
}
|
||||
});
|
||||
|
||||
// Test 6: Bulk update all tasks
|
||||
await runTest('Update all tasks', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'update-tasks',
|
||||
['--all', '--add-metadata', 'batch_update=test'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Command failed: ${result.stderr}`);
|
||||
}
|
||||
|
||||
// Verify all tasks have the metadata
|
||||
const tasksJson = helpers.readJson(`${testDir}/.taskmaster/tasks/tasks.json`);
|
||||
const tasksWithoutMetadata = tasksJson.tasks.filter(
|
||||
t => !t.metadata || t.metadata.batch_update !== 'test'
|
||||
);
|
||||
|
||||
if (tasksWithoutMetadata.length > 0) {
|
||||
throw new Error('Not all tasks were updated');
|
||||
}
|
||||
});
|
||||
|
||||
// Test 7: Bulk update with confirmation
|
||||
await runTest('Bulk update with safety check', async () => {
|
||||
// This test checks if dangerous operations require confirmation
|
||||
// The actual behavior depends on implementation
|
||||
const result = await helpers.taskMaster(
|
||||
'update-tasks',
|
||||
['--all', '--set-status', 'completed', '--force'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
// Should either succeed with --force or show warning
|
||||
if (result.exitCode !== 0 && !result.stderr.includes('confirm')) {
|
||||
throw new Error('No safety check for dangerous bulk operation');
|
||||
}
|
||||
});
|
||||
|
||||
// Test 8: Bulk update by ID list
|
||||
await runTest('Bulk update specific task IDs', async () => {
|
||||
const targetIds = taskIds.slice(0, 3);
|
||||
const result = await helpers.taskMaster(
|
||||
'update-tasks',
|
||||
['--ids', targetIds.join(','), '--add-metadata', 'selected=true'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Command failed: ${result.stderr}`);
|
||||
}
|
||||
|
||||
// Verify only specified tasks were updated
|
||||
const tasksJson = helpers.readJson(`${testDir}/.taskmaster/tasks/tasks.json`);
|
||||
targetIds.forEach(id => {
|
||||
const task = tasksJson.tasks.find(t => t.id === id);
|
||||
if (!task.metadata || task.metadata.selected !== 'true') {
|
||||
throw new Error(`Task ${id} not updated`);
|
||||
}
|
||||
});
|
||||
|
||||
// Verify other tasks were not updated
|
||||
const otherTasks = tasksJson.tasks.filter(t => !targetIds.includes(t.id));
|
||||
otherTasks.forEach(task => {
|
||||
if (task.metadata && task.metadata.selected === 'true') {
|
||||
throw new Error(`Task ${task.id} incorrectly updated`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Test 9: Bulk update with complex query
|
||||
await runTest('Complex query bulk update', async () => {
|
||||
// Create tasks with specific patterns
|
||||
for (let i = 1; i <= 3; i++) {
|
||||
await helpers.taskMaster(
|
||||
'add-task',
|
||||
['--title', `API endpoint: /users/${i}`, '--tag', 'backend'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
}
|
||||
|
||||
const result = await helpers.taskMaster(
|
||||
'update-tasks',
|
||||
['--query', 'title:API endpoint', '--add-metadata', 'type=api'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Command failed: ${result.stderr}`);
|
||||
}
|
||||
|
||||
// Verify API tasks were updated
|
||||
const tasksJson = helpers.readJson(`${testDir}/.taskmaster/tasks/tasks.json`);
|
||||
const apiTasks = tasksJson.tasks.filter(t => t.title.includes('API endpoint'));
|
||||
|
||||
apiTasks.forEach(task => {
|
||||
if (!task.metadata || task.metadata.type !== 'api') {
|
||||
throw new Error('API tasks not properly updated');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Test 10: Error handling - no matching tasks
|
||||
await runTest('Error handling - no matches', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'update-tasks',
|
||||
['--tag', 'non-existent-tag', '--set-priority', 'low'],
|
||||
{ cwd: testDir, allowFailure: true }
|
||||
);
|
||||
|
||||
// Should indicate no tasks matched
|
||||
if (!result.stdout.includes('0 tasks') && !result.stdout.includes('No tasks')) {
|
||||
throw new Error('No clear message about zero matches');
|
||||
}
|
||||
});
|
||||
|
||||
// Test 11: Bulk update with dry run
|
||||
await runTest('Dry run mode', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'update-tasks',
|
||||
['--status', 'pending', '--set-priority', 'low', '--dry-run'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Command failed: ${result.stderr}`);
|
||||
}
|
||||
|
||||
// Should show what would be updated
|
||||
if (!result.stdout.includes('would') || !result.stdout.includes('dry')) {
|
||||
throw new Error('Dry run output not clear');
|
||||
}
|
||||
|
||||
// Verify no actual changes
|
||||
const showResult = await helpers.taskMaster('show', [taskIds[0]], { cwd: testDir });
|
||||
if (showResult.stdout.includes('low') || showResult.stdout.includes('Low')) {
|
||||
throw new Error('Dry run actually modified tasks');
|
||||
}
|
||||
});
|
||||
|
||||
// Test 12: Bulk update with progress reporting
|
||||
await runTest('Progress reporting for large updates', async () => {
|
||||
// Create many tasks
|
||||
const manyTaskIds = [];
|
||||
for (let i = 1; i <= 20; i++) {
|
||||
const result = await helpers.taskMaster(
|
||||
'add-task',
|
||||
['--title', `Bulk task ${i}`],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
manyTaskIds.push(helpers.extractTaskId(result.stdout));
|
||||
}
|
||||
|
||||
const result = await helpers.taskMaster(
|
||||
'update-tasks',
|
||||
['--ids', manyTaskIds.join(','), '--set-priority', 'medium', '--verbose'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Command failed: ${result.stderr}`);
|
||||
}
|
||||
|
||||
// Should show progress or summary
|
||||
const hasProgress = result.stdout.includes('updated') &&
|
||||
result.stdout.includes('20');
|
||||
if (!hasProgress) {
|
||||
throw new Error('No progress information for bulk update');
|
||||
}
|
||||
});
|
||||
|
||||
// Test 13: Bulk update with rollback on error
|
||||
await runTest('Rollback on error', async () => {
|
||||
// Try to update with invalid data that should fail partway through
|
||||
const result = await helpers.taskMaster(
|
||||
'update-tasks',
|
||||
['--all', '--add-dependency', '99999'],
|
||||
{ cwd: testDir, allowFailure: true }
|
||||
);
|
||||
|
||||
// Should fail and indicate rollback or atomic operation
|
||||
if (result.exitCode === 0) {
|
||||
throw new Error('Should have failed with invalid dependency');
|
||||
}
|
||||
|
||||
// Verify no partial updates occurred
|
||||
const tasksJson = helpers.readJson(`${testDir}/.taskmaster/tasks/tasks.json`);
|
||||
const tasksWithBadDep = tasksJson.tasks.filter(
|
||||
t => t.dependencies && t.dependencies.includes('99999')
|
||||
);
|
||||
|
||||
if (tasksWithBadDep.length > 0) {
|
||||
throw new Error('Partial update occurred - no rollback');
|
||||
}
|
||||
});
|
||||
|
||||
// Test 14: Bulk update with template
|
||||
await runTest('Bulk update with template', async () => {
|
||||
describe('Research mode updates', () => {
|
||||
it('should update tasks with research-backed information', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'update-tasks',
|
||||
[
|
||||
'--tag', 'backend',
|
||||
'--apply-template', 'standard-backend-task',
|
||||
'--template-fields', 'add testing requirements, add documentation needs'
|
||||
'--ids', '1',
|
||||
'--prompt', 'Add OAuth2 best practices',
|
||||
'--research'
|
||||
],
|
||||
{ cwd: testDir, timeout: 120000 }
|
||||
{ cwd: testDir, timeout: 90000 }
|
||||
);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Command failed: ${result.stderr}`);
|
||||
}
|
||||
|
||||
// Check tasks were updated with template
|
||||
const listResult = await helpers.taskMaster('list', ['--tag', 'backend'], { cwd: testDir });
|
||||
const backendTaskIds = (listResult.stdout.match(/\d+(?=\s*\|)/g) || []);
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('Updated');
|
||||
|
||||
if (backendTaskIds.length > 0) {
|
||||
const showResult = await helpers.taskMaster('show', [backendTaskIds[0]], { cwd: testDir });
|
||||
const hasTemplateContent = showResult.stdout.toLowerCase().includes('test') ||
|
||||
showResult.stdout.toLowerCase().includes('documentation');
|
||||
|
||||
if (!hasTemplateContent) {
|
||||
throw new Error('Template not applied to tasks');
|
||||
// Research mode should produce more detailed updates
|
||||
const tasksPath = join(testDir, '.taskmaster/tasks/tasks.json');
|
||||
const tasks = JSON.parse(readFileSync(tasksPath, 'utf8'));
|
||||
const authTask = tasks.master.tasks.find(t => t.id === 1);
|
||||
|
||||
// Check for detailed OAuth2 information
|
||||
expect(authTask.details.length).toBeGreaterThan(100);
|
||||
const hasOAuthInfo = authTask.details.toLowerCase().includes('oauth') ||
|
||||
authTask.details.toLowerCase().includes('authorization');
|
||||
expect(hasOAuthInfo).toBe(true);
|
||||
}, 120000);
|
||||
});
|
||||
|
||||
describe('Multiple filter combinations', () => {
|
||||
it('should update tasks matching all filters', async () => {
|
||||
// Add more tasks with different combinations
|
||||
const tasksPath = join(testDir, '.taskmaster/tasks/tasks.json');
|
||||
const currentTasks = JSON.parse(readFileSync(tasksPath, 'utf8'));
|
||||
currentTasks.master.tasks.push(
|
||||
{
|
||||
id: 4,
|
||||
title: "Security audit",
|
||||
description: "Perform security review",
|
||||
priority: "high",
|
||||
status: "pending",
|
||||
details: "Initial security check"
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
title: "Performance testing",
|
||||
description: "Load testing",
|
||||
priority: "high",
|
||||
status: "in_progress",
|
||||
details: "Using JMeter"
|
||||
}
|
||||
}
|
||||
);
|
||||
writeFileSync(tasksPath, JSON.stringify(currentTasks, null, 2));
|
||||
|
||||
// Update only high priority pending tasks
|
||||
const result = await helpers.taskMaster(
|
||||
'update-tasks',
|
||||
[
|
||||
'--status', 'pending',
|
||||
'--priority', 'high',
|
||||
'--prompt', 'Add compliance requirements'
|
||||
],
|
||||
{ cwd: testDir, timeout: 45000 }
|
||||
);
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
// Should only update task 2 and 4
|
||||
expect(result.stdout).toContain('Updated 2 task');
|
||||
}, 60000);
|
||||
|
||||
it('should handle empty filter results gracefully', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'update-tasks',
|
||||
[
|
||||
'--status', 'completed',
|
||||
'--prompt', 'This should not update anything'
|
||||
],
|
||||
{ cwd: testDir, timeout: 30000 }
|
||||
);
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('No tasks found matching the criteria');
|
||||
}, 45000);
|
||||
});
|
||||
|
||||
describe('Tag support', () => {
|
||||
it('should update tasks in specific tag', async () => {
|
||||
// Create a new tag with tasks
|
||||
await helpers.taskMaster('add-tag', ['feature-x'], { cwd: testDir });
|
||||
|
||||
// Add task to the tag
|
||||
await helpers.taskMaster(
|
||||
'add-task',
|
||||
['--prompt', 'Feature X implementation', '--tag', 'feature-x'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
const result = await helpers.taskMaster(
|
||||
'update-tasks',
|
||||
[
|
||||
'--tag', 'feature-x',
|
||||
'--prompt', 'Add deployment considerations'
|
||||
],
|
||||
{ cwd: testDir, timeout: 45000 }
|
||||
);
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('Updated');
|
||||
|
||||
// Verify task in tag was updated
|
||||
const tasksPath = join(testDir, '.taskmaster/tasks/tasks.json');
|
||||
const tasks = JSON.parse(readFileSync(tasksPath, 'utf8'));
|
||||
const featureXTasks = tasks['feature-x'].tasks;
|
||||
const hasDeploymentInfo = featureXTasks.some(t =>
|
||||
t.details && t.details.toLowerCase().includes('deploy')
|
||||
);
|
||||
expect(hasDeploymentInfo).toBe(true);
|
||||
}, 60000);
|
||||
|
||||
it('should update tasks across multiple tags', async () => {
|
||||
// Create multiple tags
|
||||
await helpers.taskMaster('add-tag', ['backend'], { cwd: testDir });
|
||||
await helpers.taskMaster('add-tag', ['frontend'], { cwd: testDir });
|
||||
|
||||
// Update all tasks across all tags
|
||||
const result = await helpers.taskMaster(
|
||||
'update-tasks',
|
||||
['--prompt', 'Add error handling strategies'],
|
||||
{ cwd: testDir, timeout: 45000 }
|
||||
);
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('Updated');
|
||||
}, 60000);
|
||||
});
|
||||
|
||||
describe('Output formats', () => {
|
||||
it('should support JSON output format', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'update-tasks',
|
||||
[
|
||||
'--ids', '1',
|
||||
'--prompt', 'Add monitoring requirements',
|
||||
'--output', 'json'
|
||||
],
|
||||
{ cwd: testDir, timeout: 45000 }
|
||||
);
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
// Output should be valid JSON
|
||||
const jsonOutput = JSON.parse(result.stdout);
|
||||
expect(jsonOutput.success).toBe(true);
|
||||
expect(jsonOutput.updated).toBeDefined();
|
||||
expect(jsonOutput.tasks).toBeDefined();
|
||||
}, 60000);
|
||||
});
|
||||
|
||||
describe('Error handling', () => {
|
||||
it('should fail without prompt', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'update-tasks',
|
||||
['--ids', '1'],
|
||||
{ cwd: testDir, allowFailure: true }
|
||||
);
|
||||
|
||||
expect(result.exitCode).not.toBe(0);
|
||||
expect(result.stderr).toContain('prompt');
|
||||
});
|
||||
|
||||
// Test 15: Performance test - bulk update many tasks
|
||||
await runTest('Performance - update 50 tasks', async () => {
|
||||
// Create 50 tasks
|
||||
const perfTaskIds = [];
|
||||
for (let i = 1; i <= 50; i++) {
|
||||
const result = await helpers.taskMaster(
|
||||
'add-task',
|
||||
['--title', `Performance test task ${i}`],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
perfTaskIds.push(helpers.extractTaskId(result.stdout));
|
||||
it('should handle invalid task IDs gracefully', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'update-tasks',
|
||||
['--ids', '999,1000', '--prompt', 'Update non-existent tasks'],
|
||||
{ cwd: testDir }
|
||||
);
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('No tasks found');
|
||||
});
|
||||
|
||||
it('should handle invalid status filter', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'update-tasks',
|
||||
['--status', 'invalid-status', '--prompt', 'Test invalid status'],
|
||||
{ cwd: testDir, allowFailure: true }
|
||||
);
|
||||
|
||||
expect(result.exitCode).not.toBe(0);
|
||||
expect(result.stderr).toContain('Invalid status');
|
||||
});
|
||||
|
||||
it('should handle invalid priority filter', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'update-tasks',
|
||||
['--priority', 'urgent', '--prompt', 'Test invalid priority'],
|
||||
{ cwd: testDir, allowFailure: true }
|
||||
);
|
||||
|
||||
expect(result.exitCode).not.toBe(0);
|
||||
expect(result.stderr).toContain('Invalid priority');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Performance and edge cases', () => {
|
||||
it('should handle updating many tasks efficiently', async () => {
|
||||
// Add many tasks
|
||||
const tasksPath = join(testDir, '.taskmaster/tasks/tasks.json');
|
||||
const currentTasks = JSON.parse(readFileSync(tasksPath, 'utf8'));
|
||||
|
||||
for (let i = 4; i <= 20; i++) {
|
||||
currentTasks.master.tasks.push({
|
||||
id: i,
|
||||
title: `Task ${i}`,
|
||||
description: `Description for task ${i}`,
|
||||
priority: i % 3 === 0 ? 'high' : 'medium',
|
||||
status: 'pending',
|
||||
details: `Details for task ${i}`
|
||||
});
|
||||
}
|
||||
writeFileSync(tasksPath, JSON.stringify(currentTasks, null, 2));
|
||||
|
||||
const startTime = Date.now();
|
||||
const result = await helpers.taskMaster(
|
||||
'update-tasks',
|
||||
['--ids', perfTaskIds.join(','), '--set-priority', 'low', '--add-metadata', 'perf_test=true'],
|
||||
{ cwd: testDir }
|
||||
['--prompt', 'Add brief implementation notes'],
|
||||
{ cwd: testDir, timeout: 120000 }
|
||||
);
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Command failed: ${result.stderr}`);
|
||||
}
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('Updated 20 task');
|
||||
expect(duration).toBeLessThan(120000); // Should complete within 2 minutes
|
||||
}, 150000);
|
||||
|
||||
it('should preserve task relationships during updates', async () => {
|
||||
// Add tasks with dependencies
|
||||
const tasksPath = join(testDir, '.taskmaster/tasks/tasks.json');
|
||||
const currentTasks = JSON.parse(readFileSync(tasksPath, 'utf8'));
|
||||
currentTasks.master.tasks[1].dependencies = [1];
|
||||
currentTasks.master.tasks[2].dependencies = [1, 2];
|
||||
writeFileSync(tasksPath, JSON.stringify(currentTasks, null, 2));
|
||||
|
||||
logger.info(`Updated 50 tasks in ${duration}ms`);
|
||||
if (duration > 10000) {
|
||||
throw new Error(`Bulk update too slow: ${duration}ms`);
|
||||
}
|
||||
const result = await helpers.taskMaster(
|
||||
'update-tasks',
|
||||
['--prompt', 'Clarify implementation order'],
|
||||
{ cwd: testDir, timeout: 45000 }
|
||||
);
|
||||
|
||||
// Verify all were updated
|
||||
const updateMatch = result.stdout.match(/(\d+) tasks? updated/);
|
||||
if (!updateMatch || parseInt(updateMatch[1]) !== 50) {
|
||||
throw new Error('Not all tasks were updated');
|
||||
}
|
||||
});
|
||||
expect(result).toHaveExitCode(0);
|
||||
|
||||
// Verify dependencies are preserved
|
||||
const updatedTasks = JSON.parse(readFileSync(tasksPath, 'utf8'));
|
||||
expect(updatedTasks.master.tasks[1].dependencies).toEqual([1]);
|
||||
expect(updatedTasks.master.tasks[2].dependencies).toEqual([1, 2]);
|
||||
}, 60000);
|
||||
});
|
||||
|
||||
// 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;
|
||||
describe('Dry run mode', () => {
|
||||
it('should preview updates without applying them', async () => {
|
||||
const result = await helpers.taskMaster(
|
||||
'update-tasks',
|
||||
[
|
||||
'--ids', '1,2',
|
||||
'--prompt', 'Add test coverage requirements',
|
||||
'--dry-run'
|
||||
],
|
||||
{ cwd: testDir, timeout: 45000 }
|
||||
);
|
||||
|
||||
expect(result).toHaveExitCode(0);
|
||||
expect(result.stdout).toContain('DRY RUN');
|
||||
expect(result.stdout).toContain('Would update');
|
||||
|
||||
// Verify tasks were NOT actually updated
|
||||
const tasksPath = join(testDir, '.taskmaster/tasks/tasks.json');
|
||||
const tasks = JSON.parse(readFileSync(tasksPath, 'utf8'));
|
||||
const hasTestCoverage = tasks.master.tasks.some(t =>
|
||||
t.details && t.details.toLowerCase().includes('test coverage')
|
||||
);
|
||||
expect(hasTestCoverage).toBe(false);
|
||||
}, 60000);
|
||||
});
|
||||
|
||||
logger.info('\n=== Update-Tasks 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 update-tasks tests passed!');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
results.status = 'failed';
|
||||
results.errors.push({
|
||||
test: 'update-tasks test suite',
|
||||
error: error.message,
|
||||
stack: error.stack
|
||||
});
|
||||
logger.error(`Update-tasks test suite failed: ${error.message}`);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
describe('Integration with other commands', () => {
|
||||
it('should work with expand command on bulk-updated tasks', async () => {
|
||||
// First bulk update
|
||||
await helpers.taskMaster(
|
||||
'update-tasks',
|
||||
['--ids', '1', '--prompt', 'Add detailed specifications'],
|
||||
{ cwd: testDir, timeout: 45000 }
|
||||
);
|
||||
|
||||
// Then expand the updated task
|
||||
const expandResult = await helpers.taskMaster(
|
||||
'expand',
|
||||
['--id', '1'],
|
||||
{ cwd: testDir, timeout: 45000 }
|
||||
);
|
||||
|
||||
expect(expandResult).toHaveExitCode(0);
|
||||
expect(expandResult.stdout).toContain('Expanded task');
|
||||
}, 90000);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user