chore: improve add-task command e2e test
This commit is contained in:
109
tests/e2e/utils/logger.cjs
Normal file
109
tests/e2e/utils/logger.cjs
Normal file
@@ -0,0 +1,109 @@
|
||||
// Simple console colors fallback if chalk is not available
|
||||
const colors = {
|
||||
green: (text) => `\x1b[32m${text}\x1b[0m`,
|
||||
red: (text) => `\x1b[31m${text}\x1b[0m`,
|
||||
yellow: (text) => `\x1b[33m${text}\x1b[0m`,
|
||||
blue: (text) => `\x1b[34m${text}\x1b[0m`,
|
||||
cyan: (text) => `\x1b[36m${text}\x1b[0m`,
|
||||
gray: (text) => `\x1b[90m${text}\x1b[0m`
|
||||
};
|
||||
|
||||
class TestLogger {
|
||||
constructor(testName = 'test') {
|
||||
this.testName = testName;
|
||||
this.startTime = Date.now();
|
||||
this.stepCount = 0;
|
||||
this.logBuffer = [];
|
||||
this.totalCost = 0;
|
||||
}
|
||||
|
||||
_formatMessage(level, message, options = {}) {
|
||||
const timestamp = new Date().toISOString();
|
||||
const elapsed = ((Date.now() - this.startTime) / 1000).toFixed(2);
|
||||
const formattedMessage = `[${timestamp}] [${elapsed}s] [${level}] ${message}`;
|
||||
|
||||
// Add to buffer for later saving if needed
|
||||
this.logBuffer.push(formattedMessage);
|
||||
|
||||
return formattedMessage;
|
||||
}
|
||||
|
||||
_log(level, message, color) {
|
||||
const formatted = this._formatMessage(level, message);
|
||||
|
||||
if (process.env.E2E_VERBOSE !== 'false') {
|
||||
console.log(color ? color(formatted) : formatted);
|
||||
}
|
||||
}
|
||||
|
||||
info(message) {
|
||||
this._log('INFO', message, colors.blue);
|
||||
}
|
||||
|
||||
success(message) {
|
||||
this._log('SUCCESS', message, colors.green);
|
||||
}
|
||||
|
||||
error(message) {
|
||||
this._log('ERROR', message, colors.red);
|
||||
}
|
||||
|
||||
warning(message) {
|
||||
this._log('WARNING', message, colors.yellow);
|
||||
}
|
||||
|
||||
step(message) {
|
||||
this.stepCount++;
|
||||
this._log('STEP', `Step ${this.stepCount}: ${message}`, colors.cyan);
|
||||
}
|
||||
|
||||
debug(message) {
|
||||
if (process.env.DEBUG) {
|
||||
this._log('DEBUG', message, colors.gray);
|
||||
}
|
||||
}
|
||||
|
||||
flush() {
|
||||
// In CommonJS version, we'll just clear the buffer
|
||||
// Real implementation would write to file if needed
|
||||
this.logBuffer = [];
|
||||
}
|
||||
|
||||
summary() {
|
||||
const duration = ((Date.now() - this.startTime) / 1000).toFixed(2);
|
||||
const summary = `Test completed in ${duration}s`;
|
||||
this.info(summary);
|
||||
return {
|
||||
duration: parseFloat(duration),
|
||||
steps: this.stepCount,
|
||||
totalCost: this.totalCost
|
||||
};
|
||||
}
|
||||
|
||||
extractAndAddCost(output) {
|
||||
// Extract cost information from LLM output
|
||||
const costPatterns = [
|
||||
/Total Cost: \$?([\d.]+)/i,
|
||||
/Cost: \$?([\d.]+)/i,
|
||||
/Estimated cost: \$?([\d.]+)/i
|
||||
];
|
||||
|
||||
for (const pattern of costPatterns) {
|
||||
const match = output.match(pattern);
|
||||
if (match) {
|
||||
const cost = parseFloat(match[1]);
|
||||
this.totalCost += cost;
|
||||
this.debug(
|
||||
`Added cost: $${cost} (Total: $${this.totalCost.toFixed(4)})`
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getTotalCost() {
|
||||
return this.totalCost;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { TestLogger };
|
||||
246
tests/e2e/utils/test-helpers.cjs
Normal file
246
tests/e2e/utils/test-helpers.cjs
Normal file
@@ -0,0 +1,246 @@
|
||||
const { spawn } = require('child_process');
|
||||
const { readFileSync, existsSync, copyFileSync, writeFileSync, readdirSync } = require('fs');
|
||||
const { join } = require('path');
|
||||
|
||||
class TestHelpers {
|
||||
constructor(logger) {
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a command and return output
|
||||
* @param {string} command - Command to execute
|
||||
* @param {string[]} args - Command arguments
|
||||
* @param {Object} options - Execution options
|
||||
* @returns {Promise<{stdout: string, stderr: string, exitCode: number}>}
|
||||
*/
|
||||
async executeCommand(command, args = [], options = {}) {
|
||||
return new Promise((resolve) => {
|
||||
const spawnOptions = {
|
||||
cwd: options.cwd || process.cwd(),
|
||||
env: { ...process.env, ...options.env },
|
||||
shell: true
|
||||
};
|
||||
|
||||
// When using shell: true, pass the full command as a single string
|
||||
const fullCommand =
|
||||
args.length > 0 ? `${command} ${args.join(' ')}` : command;
|
||||
const child = spawn(fullCommand, [], spawnOptions);
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
|
||||
child.stdout.on('data', (data) => {
|
||||
stdout += data.toString();
|
||||
});
|
||||
|
||||
child.stderr.on('data', (data) => {
|
||||
stderr += data.toString();
|
||||
});
|
||||
|
||||
child.on('close', (exitCode) => {
|
||||
const output = stdout + stderr;
|
||||
|
||||
// Extract and log costs
|
||||
this.logger.extractAndAddCost(output);
|
||||
|
||||
resolve({ stdout, stderr, exitCode });
|
||||
});
|
||||
|
||||
// Handle timeout
|
||||
if (options.timeout) {
|
||||
setTimeout(() => {
|
||||
child.kill('SIGTERM');
|
||||
}, options.timeout);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute task-master command
|
||||
* @param {string} subcommand - Task-master subcommand
|
||||
* @param {string[]} args - Command arguments
|
||||
* @param {Object} options - Execution options
|
||||
*/
|
||||
async taskMaster(subcommand, args = [], options = {}) {
|
||||
const fullArgs = [subcommand, ...args];
|
||||
this.logger.info(`Executing: task-master ${fullArgs.join(' ')}`);
|
||||
|
||||
const result = await this.executeCommand('task-master', fullArgs, options);
|
||||
|
||||
if (result.exitCode !== 0 && !options.allowFailure) {
|
||||
this.logger.error(`Command failed with exit code ${result.exitCode}`);
|
||||
this.logger.error(`stderr: ${result.stderr}`);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a file exists
|
||||
*/
|
||||
fileExists(filePath) {
|
||||
return existsSync(filePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read JSON file
|
||||
*/
|
||||
readJson(filePath) {
|
||||
try {
|
||||
const content = readFileSync(filePath, 'utf8');
|
||||
return JSON.parse(content);
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to read JSON file ${filePath}: ${error.message}`
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy file
|
||||
*/
|
||||
copyFile(source, destination) {
|
||||
try {
|
||||
copyFileSync(source, destination);
|
||||
return true;
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to copy file from ${source} to ${destination}: ${error.message}`
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write file
|
||||
*/
|
||||
writeFile(filePath, content) {
|
||||
try {
|
||||
writeFileSync(filePath, content, 'utf8');
|
||||
return true;
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to write file ${filePath}: ${error.message}`
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read file
|
||||
*/
|
||||
readFile(filePath) {
|
||||
try {
|
||||
return readFileSync(filePath, 'utf8');
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to read file ${filePath}: ${error.message}`
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* List files in directory
|
||||
*/
|
||||
listFiles(dirPath) {
|
||||
try {
|
||||
return readdirSync(dirPath);
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to list files in ${dirPath}: ${error.message}`
|
||||
);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for a specified duration
|
||||
*/
|
||||
async wait(milliseconds) {
|
||||
return new Promise((resolve) => setTimeout(resolve, milliseconds));
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify task exists in tasks.json
|
||||
*/
|
||||
verifyTaskExists(tasksFile, taskId, tagName = 'master') {
|
||||
const tasks = this.readJson(tasksFile);
|
||||
if (!tasks || !tasks[tagName]) return false;
|
||||
|
||||
return tasks[tagName].tasks.some((task) => task.id === taskId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get task count for a tag
|
||||
*/
|
||||
getTaskCount(tasksFile, tagName = 'master') {
|
||||
const tasks = this.readJson(tasksFile);
|
||||
if (!tasks || !tasks[tagName]) return 0;
|
||||
|
||||
return tasks[tagName].tasks.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract task ID from command output
|
||||
*/
|
||||
extractTaskId(output) {
|
||||
// First try to match the new numbered format (#123)
|
||||
const numberedMatch = output.match(/#(\d+(?:\.\d+)?)/);
|
||||
if (numberedMatch) {
|
||||
return numberedMatch[1];
|
||||
}
|
||||
|
||||
// Fallback to older patterns
|
||||
const patterns = [
|
||||
/✓ Added new task #(\d+(?:\.\d+)?)/,
|
||||
/✅ New task created successfully:.*?(\d+(?:\.\d+)?)/,
|
||||
/Task (\d+(?:\.\d+)?) Created Successfully/,
|
||||
/Task created with ID: (\d+(?:\.\d+)?)/,
|
||||
/Created task (\d+(?:\.\d+)?)/
|
||||
];
|
||||
|
||||
for (const pattern of patterns) {
|
||||
const match = output.match(pattern);
|
||||
if (match) {
|
||||
return match[1];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run multiple async operations in parallel
|
||||
*/
|
||||
async runParallel(operations) {
|
||||
return Promise.all(operations);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run operations with concurrency limit
|
||||
*/
|
||||
async runWithConcurrency(operations, limit = 3) {
|
||||
const results = [];
|
||||
const executing = [];
|
||||
|
||||
for (const operation of operations) {
|
||||
const promise = operation().then((result) => {
|
||||
executing.splice(executing.indexOf(promise), 1);
|
||||
return result;
|
||||
});
|
||||
|
||||
results.push(promise);
|
||||
executing.push(promise);
|
||||
|
||||
if (executing.length >= limit) {
|
||||
await Promise.race(executing);
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.all(results);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { TestHelpers };
|
||||
Reference in New Issue
Block a user