@re-shell/cli
Version:
Full-stack development platform uniting microservices and microfrontends. Build complete applications with .NET (ASP.NET Core Web API, Minimal API), Java (Spring Boot, Quarkus, Micronaut, Vert.x), Rust (Actix-Web, Warp, Rocket, Axum), Python (FastAPI, Dja
924 lines (923 loc) • 34.9 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.ErrorScenarioTesting = void 0;
exports.createErrorScenario = createErrorScenario;
exports.createErrorTrigger = createErrorTrigger;
exports.runErrorScenarios = runErrorScenarios;
const events_1 = require("events");
const fs = __importStar(require("fs-extra"));
const path = __importStar(require("path"));
const os = __importStar(require("os"));
const child_process_1 = require("child_process");
class ErrorScenarioTesting extends events_1.EventEmitter {
constructor(config) {
super();
this.results = [];
this.logs = [];
this.config = {
retryAttempts: 3,
timeout: 30000,
parallel: false,
generateReport: true,
captureStdout: true,
captureStderr: true,
validateRecovery: true,
cleanupAfterEach: true,
...config
};
this.workDir = path.join(os.tmpdir(), 're-shell-error-test');
}
async run() {
this.emit('errortest:start', { scenarios: this.config.scenarios.length });
const startTime = Date.now();
try {
await this.setup();
const scenarios = this.config.scenarios.filter(s => !s.skip);
if (this.config.parallel) {
await this.runParallel(scenarios);
}
else {
await this.runSequential(scenarios);
}
const report = this.generateReport(Date.now() - startTime);
if (this.config.generateReport) {
await this.saveReport(report);
}
this.emit('errortest:complete', report);
return report;
}
catch (error) {
this.emit('errortest:error', error);
throw error;
}
finally {
await this.cleanup();
}
}
async setup() {
await fs.ensureDir(this.workDir);
this.log('info', 'Test environment prepared', { workDir: this.workDir });
}
async runSequential(scenarios) {
for (const scenario of scenarios) {
const result = await this.runScenario(scenario);
this.results.push(result);
if (this.config.cleanupAfterEach) {
await this.cleanupScenario(scenario);
}
}
}
async runParallel(scenarios) {
const promises = scenarios.map(scenario => this.runScenario(scenario));
const results = await Promise.all(promises);
this.results.push(...results);
}
async runScenario(scenario) {
this.emit('scenario:start', scenario);
this.log('info', `Running error scenario: ${scenario.name}`);
const result = {
scenario: scenario.name,
success: false,
duration: 0,
attempts: 0,
logs: [],
artifacts: [],
timestamp: new Date()
};
const startTime = Date.now();
try {
// Setup prerequisites
if (scenario.prerequisites) {
await this.runPrerequisites(scenario.prerequisites);
}
// Trigger error condition
result.error = await this.triggerError(scenario);
result.attempts++;
// Validate expected error
if (scenario.expectedError && result.error) {
result.error.matched = this.validateExpectedError(result.error, scenario.expectedError);
}
// Attempt recovery if configured
if (scenario.recovery && this.config.validateRecovery) {
result.recovery = await this.attemptRecovery(scenario.recovery, result.error);
result.success = result.recovery.successful;
}
else {
result.success = result.error?.matched ?? false;
}
// Run validation checks
if (scenario.validation) {
result.validation = await this.runValidation(scenario.validation);
result.success = result.success &&
result.validation.every(v => v.success);
}
result.duration = Date.now() - startTime;
this.log('info', `Scenario completed: ${scenario.name}`, {
success: result.success,
duration: result.duration
});
this.emit('scenario:complete', result);
return result;
}
catch (error) {
result.duration = Date.now() - startTime;
this.log('error', `Scenario failed: ${scenario.name}`, { error: error.message });
this.emit('scenario:error', { scenario, error });
return result;
}
}
async runPrerequisites(prerequisites) {
for (const prereq of prerequisites) {
try {
await this.executeCommand(prereq);
this.log('debug', `Prerequisite completed: ${prereq}`);
}
catch (error) {
this.log('warn', `Prerequisite failed: ${prereq}`, { error: error.message });
}
}
}
async triggerError(scenario) {
this.log('debug', `Triggering error: ${scenario.trigger.type}`);
const trigger = scenario.trigger;
let capturedError;
try {
switch (trigger.type) {
case 'command':
capturedError = await this.triggerCommandError(trigger);
break;
case 'file':
capturedError = await this.triggerFileError(trigger);
break;
case 'network':
capturedError = await this.triggerNetworkError(trigger);
break;
case 'process':
capturedError = await this.triggerProcessError(trigger);
break;
case 'memory':
capturedError = await this.triggerMemoryError(trigger);
break;
case 'signal':
capturedError = await this.triggerSignalError(trigger);
break;
case 'custom':
capturedError = await this.triggerCustomError(trigger);
break;
default:
throw new Error(`Unknown trigger type: ${trigger.type}`);
}
}
catch (error) {
capturedError = {
type: 'exception',
message: error.message,
code: error.code,
signal: error.signal,
stack: error.stack,
stdout: error.stdout,
stderr: error.stderr,
matched: false
};
}
this.log('debug', 'Error triggered', { error: capturedError });
return capturedError;
}
async triggerCommandError(trigger) {
const command = trigger.action;
const args = trigger.args || {};
try {
const result = (0, child_process_1.execSync)(command, {
cwd: this.workDir,
timeout: this.config.timeout,
encoding: 'utf-8',
stdio: 'pipe',
env: { ...process.env, ...args.env }
});
// Command succeeded when we expected failure
return {
type: 'unexpected_success',
message: 'Command succeeded unexpectedly',
stdout: result,
matched: false
};
}
catch (error) {
return {
type: 'command_error',
message: error.message,
code: error.status,
signal: error.signal,
stdout: error.stdout,
stderr: error.stderr,
matched: false
};
}
}
async triggerFileError(trigger) {
const action = trigger.action;
const args = trigger.args || {};
try {
switch (action) {
case 'delete_required':
const requiredFile = path.join(this.workDir, args.file || 'required.txt');
await fs.remove(requiredFile);
break;
case 'corrupt_file':
const corruptFile = path.join(this.workDir, args.file || 'data.json');
await fs.writeFile(corruptFile, '{"invalid": json}');
break;
case 'permission_denied':
const restrictedFile = path.join(this.workDir, args.file || 'restricted.txt');
await fs.writeFile(restrictedFile, 'restricted content');
await fs.chmod(restrictedFile, 0o000);
break;
case 'disk_full':
// Simulate disk full by creating a large file
const largeContent = 'x'.repeat(1024 * 1024 * 100); // 100MB
await fs.writeFile(path.join(this.workDir, 'large.tmp'), largeContent);
break;
}
// Now try to use the file to trigger the error
const testCommand = args.testCommand || `cat ${args.file || 'required.txt'}`;
(0, child_process_1.execSync)(testCommand, { cwd: this.workDir, encoding: 'utf-8' });
return {
type: 'file_error_not_triggered',
message: 'File error was not triggered as expected',
matched: false
};
}
catch (error) {
return {
type: 'file_error',
message: error.message,
code: error.code,
stdout: error.stdout,
stderr: error.stderr,
matched: false
};
}
}
async triggerNetworkError(trigger) {
const action = trigger.action;
const args = trigger.args || {};
try {
switch (action) {
case 'connection_refused':
const command = `curl -f ${args.url || 'http://localhost:99999'} --max-time 5`;
(0, child_process_1.execSync)(command, { encoding: 'utf-8' });
break;
case 'timeout':
const timeoutCommand = `curl -f ${args.url || 'http://httpbin.org/delay/30'} --max-time 5`;
(0, child_process_1.execSync)(timeoutCommand, { encoding: 'utf-8' });
break;
case 'dns_failure':
const dnsCommand = `curl -f http://nonexistent.invalid --max-time 5`;
(0, child_process_1.execSync)(dnsCommand, { encoding: 'utf-8' });
break;
}
return {
type: 'network_error_not_triggered',
message: 'Network error was not triggered as expected',
matched: false
};
}
catch (error) {
return {
type: 'network_error',
message: error.message,
code: error.status,
stdout: error.stdout,
stderr: error.stderr,
matched: false
};
}
}
async triggerProcessError(trigger) {
const action = trigger.action;
const args = trigger.args || {};
try {
switch (action) {
case 'spawn_failure':
(0, child_process_1.spawn)('/nonexistent/command', [], { stdio: 'pipe' });
break;
case 'child_exit':
const child = (0, child_process_1.spawn)('node', ['-e', 'process.exit(1)'], { stdio: 'pipe' });
await new Promise((resolve, reject) => {
child.on('exit', resolve);
child.on('error', reject);
});
break;
case 'resource_exhaustion':
// Try to spawn too many processes
const processes = [];
for (let i = 0; i < 1000; i++) {
try {
const proc = (0, child_process_1.spawn)('sleep', ['1'], { stdio: 'ignore' });
processes.push(proc);
}
catch (error) {
// Clean up spawned processes
processes.forEach(p => p.kill());
throw error;
}
}
// Clean up
processes.forEach(p => p.kill());
break;
}
return {
type: 'process_error_not_triggered',
message: 'Process error was not triggered as expected',
matched: false
};
}
catch (error) {
return {
type: 'process_error',
message: error.message,
code: error.code,
matched: false
};
}
}
async triggerMemoryError(trigger) {
const action = trigger.action;
const args = trigger.args || {};
try {
switch (action) {
case 'out_of_memory':
// Allocate large amounts of memory
const arrays = [];
for (let i = 0; i < 100; i++) {
arrays.push(new Array(1024 * 1024).fill(0));
}
break;
case 'memory_leak':
// Simulate memory leak
const leak = [];
const interval = setInterval(() => {
leak.push(new Array(1024).fill(Math.random()));
}, 1);
setTimeout(() => clearInterval(interval), 5000);
break;
}
return {
type: 'memory_error_not_triggered',
message: 'Memory error was not triggered as expected',
matched: false
};
}
catch (error) {
return {
type: 'memory_error',
message: error.message,
matched: false
};
}
}
async triggerSignalError(trigger) {
const action = trigger.action;
const args = trigger.args || {};
try {
const child = (0, child_process_1.spawn)('node', ['-e', 'setInterval(() => {}, 1000)'], {
stdio: 'pipe'
});
// Send signal after delay
setTimeout(() => {
child.kill(args.signal || 'SIGTERM');
}, args.delay || 100);
await new Promise((resolve, reject) => {
child.on('exit', (code, signal) => {
resolve({ code, signal });
});
child.on('error', reject);
});
return {
type: 'signal_error',
message: `Process killed with signal: ${args.signal || 'SIGTERM'}`,
signal: args.signal || 'SIGTERM',
matched: false
};
}
catch (error) {
return {
type: 'signal_error',
message: error.message,
signal: args.signal,
matched: false
};
}
}
async triggerCustomError(trigger) {
// Custom error triggers would be implemented by extending this class
return {
type: 'custom_error',
message: `Custom error: ${trigger.action}`,
matched: false
};
}
validateExpectedError(actual, expected) {
switch (expected.type) {
case 'exception':
if (expected.pattern) {
const regex = typeof expected.pattern === 'string' ?
new RegExp(expected.pattern) :
expected.pattern;
return regex.test(actual.message);
}
return true;
case 'exit_code':
return actual.code === expected.code;
case 'signal':
return actual.signal === expected.signal;
case 'output':
if (expected.pattern && actual.stdout) {
const regex = typeof expected.pattern === 'string' ?
new RegExp(expected.pattern) :
expected.pattern;
return regex.test(actual.stdout);
}
return false;
default:
return false;
}
}
async attemptRecovery(recoveryActions, error) {
this.log('info', 'Attempting recovery');
const result = {
attempted: true,
successful: false,
actions: [],
duration: 0,
finalState: 'failed'
};
const startTime = Date.now();
for (const action of recoveryActions) {
const actionResult = await this.executeRecoveryAction(action, error);
result.actions.push(actionResult);
if (!actionResult.success) {
this.log('warn', `Recovery action failed: ${action.action}`);
break;
}
}
result.duration = Date.now() - startTime;
result.successful = result.actions.every(a => a.success);
result.finalState = result.successful ? 'recovered' :
result.actions.some(a => a.success) ? 'partial' : 'failed';
this.log('info', 'Recovery attempt completed', {
successful: result.successful,
state: result.finalState
});
return result;
}
async executeRecoveryAction(action, error) {
this.log('debug', `Executing recovery action: ${action.type}`);
const result = {
action: action.action,
success: false,
duration: 0
};
const startTime = Date.now();
try {
switch (action.type) {
case 'retry':
await this.retryAction(action);
break;
case 'fallback':
await this.fallbackAction(action);
break;
case 'cleanup':
await this.cleanupAction(action);
break;
case 'restart':
await this.restartAction(action);
break;
case 'custom':
await this.customRecoveryAction(action);
break;
}
result.success = true;
result.duration = Date.now() - startTime;
}
catch (recoveryError) {
result.error = recoveryError.message;
result.duration = Date.now() - startTime;
}
return result;
}
async retryAction(action) {
const maxAttempts = action.maxAttempts || 3;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
await this.executeCommand(action.action);
return;
}
catch (error) {
if (attempt === maxAttempts) {
throw error;
}
await this.wait(1000 * attempt); // Exponential backoff
}
}
}
async fallbackAction(action) {
await this.executeCommand(action.action);
}
async cleanupAction(action) {
if (action.action.startsWith('rm ') || action.action.startsWith('delete ')) {
const target = action.action.split(' ')[1];
await fs.remove(path.join(this.workDir, target));
}
else {
await this.executeCommand(action.action);
}
}
async restartAction(action) {
// Simulate service restart
await this.executeCommand(action.action);
}
async customRecoveryAction(action) {
// Custom recovery actions would be implemented by extending this class
this.log('debug', `Custom recovery: ${action.action}`);
}
async runValidation(checks) {
const results = [];
for (const check of checks) {
const result = await this.executeValidationCheck(check);
results.push(result);
}
return results;
}
async executeValidationCheck(check) {
const result = {
check: `${check.type}:${check.target}`,
success: false
};
try {
switch (check.type) {
case 'file':
await this.validateFile(check, result);
break;
case 'process':
await this.validateProcess(check, result);
break;
case 'state':
await this.validateState(check, result);
break;
case 'output':
await this.validateOutput(check, result);
break;
case 'custom':
await this.validateCustom(check, result);
break;
}
}
catch (error) {
result.error = error.message;
}
return result;
}
async validateFile(check, result) {
const filePath = path.join(this.workDir, check.target);
switch (check.condition) {
case 'exists':
const exists = await fs.pathExists(filePath);
result.success = exists;
result.actual = exists;
result.expected = true;
break;
case 'contains':
if (await fs.pathExists(filePath)) {
const content = await fs.readFile(filePath, 'utf-8');
result.success = content.includes(check.value);
result.actual = content.substring(0, 100);
result.expected = `contains "${check.value}"`;
}
break;
}
}
async validateProcess(check, result) {
switch (check.condition) {
case 'running':
try {
(0, child_process_1.execSync)(`pgrep -f "${check.target}"`, { encoding: 'utf-8' });
result.success = true;
result.actual = 'running';
result.expected = 'running';
}
catch {
result.success = false;
result.actual = 'not running';
result.expected = 'running';
}
break;
}
}
async validateState(check, result) {
// State validation implementation
result.success = true;
}
async validateOutput(check, result) {
// Output validation implementation
result.success = true;
}
async validateCustom(check, result) {
// Custom validation implementation
result.success = true;
}
async executeCommand(command) {
return (0, child_process_1.execSync)(command, {
cwd: this.workDir,
encoding: 'utf-8',
timeout: this.config.timeout
});
}
async cleanupScenario(scenario) {
if (scenario.cleanup) {
for (const cleanup of scenario.cleanup) {
try {
await this.executeCommand(cleanup);
}
catch {
// Ignore cleanup errors
}
}
}
}
async cleanup() {
try {
await fs.remove(this.workDir);
}
catch {
// Ignore cleanup errors
}
}
generateReport(duration) {
const summary = this.generateSummary(duration);
const analysis = this.analyzeResults();
const recommendations = this.generateRecommendations(analysis);
const patterns = this.identifyPatterns();
return {
summary,
results: this.results,
analysis,
recommendations,
patterns,
timestamp: new Date()
};
}
generateSummary(duration) {
const categories = new Map();
const severity = new Map();
for (const result of this.results) {
const scenario = this.config.scenarios.find(s => s.name === result.scenario);
if (scenario) {
categories.set(scenario.category, (categories.get(scenario.category) || 0) + 1);
const sev = scenario.expectedError?.severity || 'medium';
severity.set(sev, (severity.get(sev) || 0) + 1);
}
}
return {
totalScenarios: this.results.length,
passed: this.results.filter(r => r.success).length,
failed: this.results.filter(r => !r.success).length,
recovered: this.results.filter(r => r.recovery?.successful).length,
unrecovered: this.results.filter(r => r.recovery && !r.recovery.successful).length,
categories,
severity,
duration: duration / 1000
};
}
analyzeResults() {
const resilience = this.calculateResilience();
const hotspots = this.identifyHotspots();
const trends = this.analyzeTrends();
const gaps = this.identifyGaps();
return { resilience, hotspots, trends, gaps };
}
calculateResilience() {
const total = this.results.length;
const errors = this.results.filter(r => r.error).length;
const recovered = this.results.filter(r => r.recovery?.successful).length;
const partialRecoveries = this.results.filter(r => r.recovery?.finalState === 'partial').length;
const recoveryTimes = this.results
.filter(r => r.recovery)
.map(r => r.recovery.duration);
const criticalFailures = this.results.filter(r => {
const scenario = this.config.scenarios.find(s => s.name === r.scenario);
return scenario?.expectedError?.severity === 'critical' && !r.success;
}).length;
return {
errorRate: total > 0 ? (errors / total) * 100 : 0,
recoveryRate: errors > 0 ? (recovered / errors) * 100 : 0,
averageRecoveryTime: recoveryTimes.length > 0 ?
recoveryTimes.reduce((a, b) => a + b, 0) / recoveryTimes.length : 0,
criticalFailures,
partialRecoveries
};
}
identifyHotspots() {
const hotspots = [];
const categoryStats = new Map();
for (const result of this.results) {
const scenario = this.config.scenarios.find(s => s.name === result.scenario);
if (scenario) {
const stats = categoryStats.get(scenario.category) || { count: 0, failures: 0 };
stats.count++;
if (!result.success)
stats.failures++;
categoryStats.set(scenario.category, stats);
}
}
for (const [category, stats] of categoryStats) {
const failureRate = (stats.failures / stats.count) * 100;
if (failureRate > 50) {
hotspots.push({
category,
count: stats.failures,
severity: failureRate > 80 ? 'critical' : 'high',
description: `High failure rate in ${category} operations (${failureRate.toFixed(1)}%)`,
impact: `${stats.failures} out of ${stats.count} scenarios failed`,
suggestion: `Review and strengthen ${category} error handling`
});
}
}
return hotspots;
}
analyzeTrends() {
// Simple trend analysis
return [];
}
identifyGaps() {
const gaps = [];
for (const result of this.results) {
if (result.recovery && !result.recovery.successful) {
gaps.push({
scenario: result.scenario,
issue: 'Recovery failed',
impact: 'high',
recommendation: 'Implement additional recovery mechanisms'
});
}
if (!result.recovery && result.error) {
gaps.push({
scenario: result.scenario,
issue: 'No recovery mechanism defined',
impact: 'medium',
recommendation: 'Add recovery actions for this error scenario'
});
}
}
return gaps;
}
generateRecommendations(analysis) {
const recommendations = [];
if (analysis.resilience.recoveryRate < 80) {
recommendations.push(`Improve recovery mechanisms - current rate: ${analysis.resilience.recoveryRate.toFixed(1)}%`);
}
if (analysis.resilience.criticalFailures > 0) {
recommendations.push(`Address ${analysis.resilience.criticalFailures} critical failure(s) immediately`);
}
for (const hotspot of analysis.hotspots) {
if (hotspot.severity === 'critical') {
recommendations.push(hotspot.suggestion);
}
}
if (analysis.gaps.length > 0) {
recommendations.push(`Implement recovery mechanisms for ${analysis.gaps.length} identified gap(s)`);
}
return recommendations;
}
identifyPatterns() {
const patterns = [];
const errorMessages = this.results
.filter(r => r.error)
.map(r => r.error.message);
// Common error patterns
const commonPatterns = [
{ name: 'Permission Denied', pattern: /permission denied|access denied/i },
{ name: 'File Not Found', pattern: /no such file|file not found/i },
{ name: 'Network Error', pattern: /connection refused|timeout|network/i },
{ name: 'Out of Memory', pattern: /out of memory|cannot allocate/i },
{ name: 'Process Error', pattern: /child process|spawn|exit code/i }
];
for (const patternDef of commonPatterns) {
const matches = errorMessages.filter(msg => patternDef.pattern.test(msg));
if (matches.length > 0) {
patterns.push({
name: patternDef.name,
pattern: patternDef.pattern,
category: 'common',
frequency: matches.length,
examples: matches.slice(0, 3)
});
}
}
return patterns;
}
async saveReport(report) {
const reportPath = path.join(process.cwd(), 'error-test-report.json');
await fs.writeJson(reportPath, report, { spaces: 2 });
const summaryPath = reportPath.replace('.json', '-summary.txt');
await fs.writeFile(summaryPath, this.formatSummary(report));
this.emit('report:saved', { json: reportPath, summary: summaryPath });
}
formatSummary(report) {
const lines = [
'Error Scenario Testing Report',
'============================',
'',
`Date: ${report.timestamp.toISOString()}`,
'',
'Summary:',
` Total Scenarios: ${report.summary.totalScenarios}`,
` Passed: ${report.summary.passed}`,
` Failed: ${report.summary.failed}`,
` Recovered: ${report.summary.recovered}`,
` Unrecovered: ${report.summary.unrecovered}`,
'',
'Resilience Metrics:',
` Error Rate: ${report.analysis.resilience.errorRate.toFixed(1)}%`,
` Recovery Rate: ${report.analysis.resilience.recoveryRate.toFixed(1)}%`,
` Avg Recovery Time: ${report.analysis.resilience.averageRecoveryTime.toFixed(0)}ms`,
` Critical Failures: ${report.analysis.resilience.criticalFailures}`,
'',
'Error Patterns:'
];
report.patterns.forEach(pattern => {
lines.push(` ${pattern.name}: ${pattern.frequency} occurrences`);
});
lines.push('', 'Recommendations:');
report.recommendations.forEach(rec => {
lines.push(` - ${rec}`);
});
return lines.join('\n');
}
log(level, message, context) {
const entry = {
timestamp: new Date(),
level,
message,
context
};
this.logs.push(entry);
this.emit('log', entry);
}
wait(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
exports.ErrorScenarioTesting = ErrorScenarioTesting;
// Export utility functions
function createErrorScenario(name, category, trigger, options) {
return {
name,
category,
trigger,
...options
};
}
function createErrorTrigger(type, action, args) {
return { type, action, args };
}
async function runErrorScenarios(scenarios, config) {
const tester = new ErrorScenarioTesting({
scenarios,
...config
});
return tester.run();
}