feat: more e2e

This commit is contained in:
Ralph Khreish
2025-07-10 19:38:28 +03:00
parent bb1b36e891
commit 74232d0e0d
5 changed files with 2193 additions and 2316 deletions

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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