ai-debug-local-mcp
Version:
๐ฏ ENHANCED AI GUIDANCE v4.1.2: Dramatically improved tool descriptions help AI users choose the right tools instead of 'close enough' options. Ultra-fast keyboard automation (10x speed), universal recording, multi-ecosystem debugging support, and compreh
858 lines โข 36.4 kB
JavaScript
/**
* TDD Debug Engine - Revolutionary Test-Runtime Bridging
*
* Implements Cycle 30 #1 priority: TDD automation with systematic debugging integration
* Building on proven MCP integration success (Memory MCP + Sequential Thinking MCP)
*/
import { spawn } from 'child_process';
import { promises as fs } from 'fs';
import { join, dirname, extname } from 'path';
import { EventEmitter } from 'events';
export class TDDDebugEngine {
sessionManager;
memoryManager;
toolSafety;
tddSessions = new Map();
runningTests = new Map();
constructor(sessionManager, memoryManager, toolSafety) {
this.sessionManager = sessionManager;
this.memoryManager = memoryManager;
this.toolSafety = toolSafety;
}
/**
* Enable TDD Mode - Revolutionary Test-Runtime Bridging
*/
async enableTDDMode(options) {
const sessionId = `tdd_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
// Detect test framework
const framework = options.frameworkHint || await this.detectTestFramework(options.testFile);
// Create TDD session with memory tracking
const session = {
sessionId,
testFile: options.testFile,
implementationFile: options.implementationFile,
framework,
redGreenRefactorState: 'red', // Start with failing test assumption
startTime: Date.now(),
eventEmitter: options.enableStreaming ? new EventEmitter() : undefined
};
// Register session with memory management
this.sessionManager.createSession(sessionId, {
type: 'tdd_debugging',
testFile: options.testFile,
framework,
startTime: Date.now()
});
this.tddSessions.set(sessionId, session);
console.log(`๐งช TDD Mode Enabled:`);
console.log(` Session: ${sessionId}`);
console.log(` Test File: ${options.testFile}`);
console.log(` Framework: ${framework}`);
console.log(` Implementation: ${options.implementationFile || 'Auto-detected'}`);
return session;
}
/**
* Run Tests with Coverage - Core TDD automation tool
*/
async runTestsWithCoverage(sessionId, options = {}) {
const session = this.tddSessions.get(sessionId);
if (!session) {
throw new Error(`TDD session ${sessionId} not found`);
}
const executionResult = await this.toolSafety.executeTool('run_tests_with_coverage', async () => {
const startTime = Date.now();
try {
// Execute tests with framework-specific logic
const testResults = await this.executeTests(session, options.testPattern);
const coverageData = await this.analyzeCoverage(session);
const runtimeBehavior = options.enableRuntimeBridging
? await this.captureRuntimeBehavior(session)
: undefined;
// Create execution results
const results = {
success: testResults.failedTests === 0,
testResults,
coverageData,
runtimeBehavior,
timestamp: Date.now(),
executionTime: Date.now() - startTime
};
// Update TDD session state based on results
this.updateTDDState(session, results);
// Store baseline if requested
if (options.generateBaseline) {
session.testBaseline = testResults;
session.coverageBaseline = coverageData;
}
session.lastResults = results;
console.log(`โ
Tests executed (${results.executionTime}ms):`);
console.log(` Total: ${testResults.totalTests}, Passed: ${testResults.passedTests}, Failed: ${testResults.failedTests}`);
console.log(` Coverage: ${coverageData?.percentage.toFixed(1)}%`);
console.log(` TDD State: ${session.redGreenRefactorState}`);
return results;
}
catch (error) {
console.error(`โ Test execution failed:`, error);
throw error;
}
}, { timeout: 120000, maxRetries: 1 } // Allow longer timeout for test execution
);
if (!executionResult.success) {
throw executionResult.error || new Error('Test execution failed');
}
return executionResult.result;
}
/**
* Test Impact Analysis - Detect refactoring breakage
*/
async testImpactAnalysis(sessionId, changes) {
const session = this.tddSessions.get(sessionId);
if (!session) {
throw new Error(`TDD session ${sessionId} not found`);
}
const executionResult = await this.toolSafety.executeTool('test_impact_analysis', async () => {
// Analyze which tests could be impacted by file changes
const impactedTests = await this.analyzeTestImpact(session, changes.modifiedFiles);
// Assess risk based on change scope and test coverage
const riskAssessment = this.assessChangeRisk(changes.modifiedFiles, impactedTests);
// Recommend optimal test execution strategy
const recommendedTestScope = this.recommendTestScope(impactedTests, changes.testScope);
const executionStrategy = this.generateExecutionStrategy(riskAssessment, recommendedTestScope);
console.log(`๐ฏ Impact Analysis Complete:`);
console.log(` Modified Files: ${changes.modifiedFiles.length}`);
console.log(` Impacted Tests: ${impactedTests.length}`);
console.log(` Risk: ${riskAssessment}`);
console.log(` Strategy: ${executionStrategy}`);
return {
impactedTests,
riskAssessment,
recommendedTestScope,
executionStrategy
};
});
if (!executionResult.success) {
throw executionResult.error || new Error('Impact analysis failed');
}
return executionResult.result;
}
/**
* Regression Detection - Automated regression analysis
*/
async regressionDetection(sessionId) {
const session = this.tddSessions.get(sessionId);
if (!session) {
throw new Error(`TDD session ${sessionId} not found`);
}
if (!session.testBaseline || !session.coverageBaseline) {
throw new Error(`No baseline data available for regression detection. Run tests with generateBaseline: true first.`);
}
const executionResult = await this.toolSafety.executeTool('regression_detection', async () => {
const currentResults = session.lastResults;
if (!currentResults) {
throw new Error(`No current test results available for comparison`);
}
// Analyze test regressions
const testRegression = this.analyzeTestRegression(session.testBaseline, currentResults.testResults);
// Analyze coverage regressions
const coverageRegression = session.coverageBaseline && currentResults.coverageData ?
this.analyzeCoverageRegression(session.coverageBaseline, currentResults.coverageData) :
undefined;
// Analyze performance regressions
const performanceRegression = this.analyzePerformanceRegression(session.testBaseline, currentResults.testResults);
const hasRegression = testRegression.newFailures.length > 0 ||
(coverageRegression?.coverageDelta || 0) < -5 ||
(performanceRegression?.percentageChange || 0) > 20;
const recommendations = this.generateRegressionRecommendations({
testRegression,
coverageRegression,
performanceRegression
});
const analysis = {
hasRegression,
testRegression,
coverageRegression,
performanceRegression,
recommendations
};
console.log(`๐ Regression Analysis:`);
console.log(` Has Regression: ${hasRegression}`);
console.log(` New Failures: ${testRegression.newFailures.length}`);
console.log(` Coverage Delta: ${coverageRegression?.coverageDelta?.toFixed(1)}%`);
console.log(` Recommendations: ${recommendations.length}`);
return analysis;
});
if (!executionResult.success) {
throw executionResult.error || new Error('Regression detection failed');
}
return executionResult.result;
}
/**
* Private implementation methods
*/
async detectTestFramework(testFile) {
try {
const content = await fs.readFile(testFile, 'utf-8');
const ext = extname(testFile);
// Flutter/Dart tests
if (ext === '.dart' && content.includes('testWidgets')) {
return 'flutter';
}
// Jest tests
if ((ext === '.js' || ext === '.ts') &&
(content.includes('describe(') || content.includes('test(') || content.includes('it('))) {
return 'jest';
}
// Phoenix/Elixir tests
if (ext === '.exs' && content.includes('ExUnit')) {
return 'exunit';
}
// Python tests
if (ext === '.py' && (content.includes('pytest') || content.includes('unittest'))) {
return 'pytest';
}
return 'unknown';
}
catch (error) {
console.warn(`Could not detect test framework for ${testFile}:`, error);
return 'unknown';
}
}
findFlutterProjectRoot(testFilePath) {
let currentDir = dirname(testFilePath);
// Walk up the directory tree looking for pubspec.yaml
while (currentDir !== dirname(currentDir)) { // Stop at filesystem root
try {
const pubspecPath = join(currentDir, 'pubspec.yaml');
if (require('fs').existsSync(pubspecPath)) {
console.log(`โ
Found Flutter project root: ${currentDir}`);
return currentDir;
}
}
catch (error) {
// Continue searching
}
currentDir = dirname(currentDir);
}
// Fallback to test file directory
console.log(`โ ๏ธ No pubspec.yaml found, using test file directory: ${dirname(testFilePath)}`);
return dirname(testFilePath);
}
async executeTests(session, testPattern) {
// Framework-specific test execution
switch (session.framework) {
case 'jest':
return await this.executeJestTests(session, testPattern);
case 'flutter':
return await this.executeFlutterTests(session, testPattern);
case 'exunit':
return await this.executeExUnitTests(session, testPattern);
case 'pytest':
return await this.executePytestTests(session, testPattern);
default:
throw new Error(`Unsupported test framework: ${session.framework}`);
}
}
async executeJestTests(session, testPattern) {
return new Promise((resolve, reject) => {
const args = ['--json', '--coverage'];
if (testPattern)
args.push(testPattern);
else
args.push(session.testFile);
const jest = spawn('npx', ['jest', ...args], {
cwd: dirname(session.testFile),
stdio: 'pipe'
});
let output = '';
jest.stdout?.on('data', (data) => { output += data; });
jest.stderr?.on('data', (data) => { output += data; });
jest.on('close', (code) => {
try {
// Parse Jest JSON output
const lines = output.split('\n');
const jsonLine = lines.find(line => line.startsWith('{') && line.includes('"testResults"'));
if (jsonLine) {
const jestResults = JSON.parse(jsonLine);
resolve(this.parseJestResults(jestResults));
}
else {
// Fallback parsing for non-JSON output
resolve(this.parseJestTextOutput(output));
}
}
catch (error) {
reject(new Error(`Failed to parse Jest results: ${error}`));
}
});
jest.on('error', reject);
this.runningTests.set(session.sessionId, jest);
});
}
async executeFlutterTests(session, testPattern) {
return new Promise((resolve, reject) => {
// Enhanced Flutter test discovery and execution
console.log(`๐ฆ EXECUTING FLUTTER TESTS for session ${session.sessionId}`);
console.log(` Test file: ${session.testFile}`);
console.log(` Working directory: ${dirname(session.testFile)}`);
// Check if this is a Flutter project
const workingDir = this.findFlutterProjectRoot(session.testFile);
console.log(` Flutter project root: ${workingDir}`);
const args = ['test', '--reporter=json'];
// Improved test targeting
if (testPattern) {
args.push(testPattern);
}
else {
// If testFile is provided, try to find it relative to the project
const relativePath = session.testFile.replace(workingDir + '/', '');
args.push(relativePath);
}
console.log(` Flutter command: flutter ${args.join(' ')}`);
const flutter = spawn('flutter', args, {
cwd: workingDir,
stdio: 'pipe',
env: { ...process.env }
});
let output = '';
let errorOutput = '';
flutter.stdout?.on('data', (data) => {
const text = data.toString();
output += text;
console.log(`๐ Flutter stdout: ${text.trim()}`);
});
flutter.stderr?.on('data', (data) => {
const text = data.toString();
errorOutput += text;
console.log(`โ ๏ธ Flutter stderr: ${text.trim()}`);
});
flutter.on('close', (code) => {
console.log(`๐ฆ Flutter test execution finished with code: ${code}`);
console.log(`๐ Output length: ${output.length} chars, Error length: ${errorOutput.length} chars`);
try {
if (code !== 0 && errorOutput) {
console.log(`โ Flutter test failed: ${errorOutput}`);
// Still try to parse output for partial results
}
const results = this.parseFlutterTestOutput(output, errorOutput);
console.log(`โ
Parsed Flutter results: ${results.totalTests} total tests, ${results.passedTests} passed`);
resolve(results);
}
catch (error) {
console.error(`๐ฅ Failed to parse Flutter test results:`, error);
reject(new Error(`Failed to parse Flutter test results: ${error}`));
}
});
flutter.on('error', (error) => {
console.error(`๐ฅ Flutter process error:`, error);
reject(error);
});
this.runningTests.set(session.sessionId, flutter);
});
}
async executeExUnitTests(session, testPattern) {
return new Promise((resolve, reject) => {
console.log(`๐ฎ EXECUTING EXUNIT TESTS for session ${session.sessionId}`);
console.log(` Test file: ${session.testFile}`);
const args = ['test'];
// Add coverage if available
args.push('--cover');
// Add formatter for better output
args.push('--formatter', 'ExUnit.CLIFormatter');
// Add specific test file or pattern
if (testPattern) {
args.push(testPattern);
}
else if (session.testFile) {
args.push(session.testFile);
}
// Get project directory (go up from test file to find mix.exs)
let projectDir = dirname(session.testFile);
while (projectDir !== '/' && !require('fs').existsSync(join(projectDir, 'mix.exs'))) {
projectDir = dirname(projectDir);
}
if (!require('fs').existsSync(join(projectDir, 'mix.exs'))) {
reject(new Error('Could not find mix.exs - not an Elixir project'));
return;
}
const exunit = spawn('mix', args, {
cwd: projectDir,
env: { ...process.env, MIX_ENV: 'test' },
stdio: 'pipe'
});
let output = '';
let errorOutput = '';
let testCount = 0;
let passedCount = 0;
let failedCount = 0;
// Emit streaming events for real-time updates
const emitProgress = (data) => {
if (session.eventEmitter) {
session.eventEmitter.emit('test-progress', {
sessionId: session.sessionId,
...data
});
}
};
exunit.stdout?.on('data', (data) => {
output += data;
const chunk = data.toString();
// Stream progress for large test suites
const lines = chunk.split('\n');
for (const line of lines) {
// Progress indicators
if (line.includes('.') && !line.includes('..')) {
// Each dot represents a passing test
const dots = (line.match(/\./g) || []).length;
passedCount += dots;
testCount += dots;
emitProgress({
type: 'test-pass',
count: dots,
totalTests: testCount,
passedTests: passedCount
});
}
if (line.includes('F')) {
// F represents a failing test
const failures = (line.match(/F/g) || []).length;
failedCount += failures;
testCount += failures;
emitProgress({
type: 'test-fail',
count: failures,
totalTests: testCount,
failedTests: failedCount
});
}
// Test summary
if (line.includes('tests,')) {
console.log(`๐ ${line.trim()}`);
emitProgress({
type: 'summary',
message: line.trim()
});
}
// Timing information
if (line.includes('Finished in')) {
console.log(`โฑ๏ธ ${line.trim()}`);
emitProgress({
type: 'timing',
message: line.trim()
});
}
// Test file being run
if (line.includes('Running ExUnit with')) {
emitProgress({
type: 'start',
message: `Starting ExUnit tests...`
});
}
}
});
exunit.stderr?.on('data', (data) => {
errorOutput += data;
// Stream compilation errors
const error = data.toString();
if (error.includes('** (') || error.includes('Compilation error')) {
emitProgress({
type: 'error',
message: error.trim()
});
}
});
// Set longer timeout for Elixir tests (10 minutes)
const timeout = setTimeout(() => {
exunit.kill();
reject(new Error('ExUnit tests timed out after 10 minutes'));
}, 600000);
exunit.on('close', (code) => {
clearTimeout(timeout);
emitProgress({
type: 'complete',
exitCode: code,
totalTests: testCount,
passedTests: passedCount,
failedTests: failedCount
});
try {
resolve(this.parseExUnitResults(output, code || 0));
}
catch (error) {
reject(new Error(`Failed to parse ExUnit results: ${error}`));
}
});
exunit.on('error', (error) => {
clearTimeout(timeout);
reject(error);
});
this.runningTests.set(session.sessionId, exunit);
});
}
parseExUnitResults(output, exitCode) {
const lines = output.split('\n');
let totalTests = 0;
let passedTests = 0;
let failedTests = 0;
let skippedTests = 0;
const testSuites = [];
// Parse summary line (e.g., "16 tests, 2 failures")
const summaryMatch = output.match(/(\d+) tests?, (\d+) failures?(?:, (\d+) skipped)?/);
if (summaryMatch) {
totalTests = parseInt(summaryMatch[1]);
failedTests = parseInt(summaryMatch[2]);
skippedTests = parseInt(summaryMatch[3] || '0');
passedTests = totalTests - failedTests - skippedTests;
}
// Parse individual test failures
const failurePattern = /\d+\) test (.+) \((.+)\)/g;
let match;
const tests = [];
while ((match = failurePattern.exec(output)) !== null) {
const [, testName, moduleName] = match;
tests.push({
name: testName,
status: 'failed',
duration: 0,
error: 'Test failed - see output for details'
});
}
// Create a test suite from the results
if (totalTests > 0) {
testSuites.push({
name: 'ExUnit Tests',
tests: tests,
passed: passedTests,
failed: failedTests,
duration: 0
});
}
return {
totalTests,
passedTests,
failedTests,
skippedTests,
testSuites
};
}
async executePytestTests(session, testPattern) {
// Placeholder for pytest implementation
throw new Error('Pytest test execution not yet implemented');
}
parseJestResults(jestResults) {
const testSuites = jestResults.testResults?.map((suite) => ({
name: suite.name,
tests: suite.assertionResults?.map((test) => ({
name: test.title,
status: test.status === 'passed' ? 'passed' : 'failed',
duration: test.duration || 0,
error: test.failureMessages?.[0]
})) || [],
passed: suite.numPassingTests || 0,
failed: suite.numFailingTests || 0,
duration: suite.endTime - suite.startTime || 0
})) || [];
return {
totalTests: jestResults.numTotalTests || 0,
passedTests: jestResults.numPassedTests || 0,
failedTests: jestResults.numFailedTests || 0,
skippedTests: jestResults.numTodoTests || 0,
testSuites
};
}
parseJestTextOutput(output) {
// Fallback text parsing for Jest output
const lines = output.split('\n');
let totalTests = 0;
let passedTests = 0;
let failedTests = 0;
lines.forEach(line => {
if (line.includes('Tests:')) {
const match = line.match(/(\d+) passed.*?(\d+) total/);
if (match) {
passedTests = parseInt(match[1]);
totalTests = parseInt(match[2]);
failedTests = totalTests - passedTests;
}
}
});
return {
totalTests,
passedTests,
failedTests,
skippedTests: 0,
testSuites: []
};
}
parseFlutterTestOutput(output, errorOutput) {
console.log('๐ PARSING FLUTTER TEST OUTPUT...');
// Try to parse JSON reporter output first
let jsonResults = null;
const lines = output.split('\n');
for (const line of lines) {
if (line.trim().startsWith('{') && line.includes('"suite"')) {
try {
jsonResults = JSON.parse(line);
break;
}
catch (e) {
// Not valid JSON, continue
}
}
}
if (jsonResults) {
console.log('โ
Found JSON test results');
return this.parseFlutterJsonResults(jsonResults);
}
// Fallback to text parsing
console.log('๐ Using text parsing fallback');
let totalTests = 0;
let passedTests = 0;
let failedTests = 0;
const testSuites = [];
// Look for test completion messages
lines.forEach(line => {
if (line.includes('All tests passed!')) {
const match = line.match(/(\d+)/);
if (match) {
totalTests = passedTests = parseInt(match[1]);
}
}
else if (line.includes('Some tests failed')) {
// Parse failure count
const match = line.match(/(\d+) failed/);
if (match) {
failedTests = parseInt(match[1]);
}
}
else if (line.includes('tests passed')) {
// Try to extract passed count
const match = line.match(/(\d+) tests passed/);
if (match) {
passedTests = parseInt(match[1]);
}
}
});
// If we found nothing in output, check error output for diagnostic info
if (totalTests === 0 && errorOutput) {
console.log('โ ๏ธ No tests found in stdout, checking stderr for diagnostics');
if (errorOutput.includes('No tests found')) {
console.log('โ Flutter reports: No tests found');
}
else if (errorOutput.includes('Could not find package')) {
console.log('โ Flutter reports: Package/dependency issues');
}
else if (errorOutput.includes('build failed')) {
console.log('โ Flutter reports: Build failed');
failedTests = 1; // Indicate build failure
}
}
const results = {
totalTests: totalTests || (passedTests + failedTests),
passedTests,
failedTests,
skippedTests: 0,
testSuites
};
console.log(`๐ Flutter parsing result: ${JSON.stringify(results)}`);
return results;
}
parseFlutterJsonResults(jsonData) {
// Parse JSON format Flutter test results
// This depends on the specific JSON format Flutter uses
console.log('๐ Parsing JSON Flutter results:', Object.keys(jsonData));
return {
totalTests: jsonData.count || 0,
passedTests: jsonData.success || 0,
failedTests: jsonData.failure || 0,
skippedTests: jsonData.skip || 0,
testSuites: []
};
}
async analyzeCoverage(session) {
// Framework-specific coverage analysis
try {
switch (session.framework) {
case 'jest':
return await this.analyzeJestCoverage(session);
case 'flutter':
return await this.analyzeFlutterCoverage(session);
default:
return undefined;
}
}
catch (error) {
console.warn(`Coverage analysis failed for ${session.framework}:`, error);
return undefined;
}
}
async analyzeJestCoverage(session) {
// Read Jest coverage data if available
const coveragePath = join(dirname(session.testFile), 'coverage/coverage-final.json');
try {
const coverageData = JSON.parse(await fs.readFile(coveragePath, 'utf-8'));
let totalLines = 0;
let coveredLines = 0;
let totalBranches = 0;
let coveredBranches = 0;
const filesCovered = {};
Object.keys(coverageData).forEach(filePath => {
const file = coverageData[filePath];
totalLines += Object.keys(file.s).length;
coveredLines += Object.values(file.s).filter((hits) => hits > 0).length;
totalBranches += Object.keys(file.b).length * 2; // Assume 2 branches per statement
coveredBranches += Object.values(file.b).flat().filter((hits) => hits > 0).length;
filesCovered[filePath] = {
lines: file.s,
branches: file.b,
functions: file.f
};
});
return {
linesCovered: coveredLines,
totalLines,
percentage: totalLines > 0 ? (coveredLines / totalLines) * 100 : 0,
branchesCovered: coveredBranches,
totalBranches,
branchPercentage: totalBranches > 0 ? (coveredBranches / totalBranches) * 100 : 0,
filesCovered
};
}
catch (error) {
console.warn('Could not read Jest coverage data:', error);
return undefined;
}
}
async analyzeFlutterCoverage(session) {
// Flutter coverage analysis would require additional setup
return undefined;
}
async captureRuntimeBehavior(session) {
// Runtime behavior capture - placeholder for advanced implementation
return undefined;
}
updateTDDState(session, results) {
// Update Red-Green-Refactor state based on test results
if (results.testResults.failedTests > 0) {
session.redGreenRefactorState = 'red';
}
else if (session.redGreenRefactorState === 'red') {
session.redGreenRefactorState = 'green';
}
else {
session.redGreenRefactorState = 'refactor';
}
}
async analyzeTestImpact(session, modifiedFiles) {
// Analyze which tests might be impacted by file changes
// This is a simplified implementation - real version would use dependency analysis
return [session.testFile];
}
assessChangeRisk(modifiedFiles, impactedTests) {
if (modifiedFiles.length > 5 || impactedTests.length > 10)
return 'high';
if (modifiedFiles.length > 2 || impactedTests.length > 5)
return 'medium';
return 'low';
}
recommendTestScope(impactedTests, requestedScope) {
// Recommend optimal test scope based on impact analysis
return impactedTests;
}
generateExecutionStrategy(risk, testScope) {
switch (risk) {
case 'high': return 'Full test suite + integration tests';
case 'medium': return 'Affected tests + smoke tests';
default: return 'Affected tests only';
}
}
analyzeTestRegression(baseline, current) {
const newFailures = [];
const previouslyPassingTests = [];
// Compare current results with baseline
baseline.testSuites.forEach(baselineSuite => {
const currentSuite = current.testSuites.find(s => s.name === baselineSuite.name);
if (!currentSuite)
return;
baselineSuite.tests.forEach(baselineTest => {
const currentTest = currentSuite.tests.find(t => t.name === baselineTest.name);
if (baselineTest.status === 'passed' && currentTest?.status === 'failed') {
previouslyPassingTests.push(baselineTest.name);
newFailures.push({
testName: baselineTest.name,
suiteName: baselineSuite.name,
error: currentTest.error || 'Unknown failure'
});
}
});
});
return { previouslyPassingTests, newFailures };
}
analyzeCoverageRegression(baseline, current) {
if (!current)
return undefined;
const coverageDelta = current.percentage - baseline.percentage;
return {
baselineCoverage: baseline.percentage,
currentCoverage: current.percentage,
coverageDelta
};
}
analyzePerformanceRegression(baseline, current) {
// Simplified performance regression analysis
const baselineDuration = baseline.testSuites.reduce((sum, s) => sum + s.duration, 0);
const currentDuration = current.testSuites.reduce((sum, s) => sum + s.duration, 0);
if (baselineDuration === 0)
return undefined;
const percentageChange = ((currentDuration - baselineDuration) / baselineDuration) * 100;
return {
metric: 'test_execution_time',
baselineValue: baselineDuration,
currentValue: currentDuration,
percentageChange
};
}
generateRegressionRecommendations(analysis) {
const recommendations = [];
if (analysis.testRegression?.newFailures.length > 0) {
recommendations.push(`Review ${analysis.testRegression.newFailures.length} newly failing tests`);
}
if (analysis.coverageRegression?.coverageDelta < -5) {
recommendations.push(`Coverage decreased by ${Math.abs(analysis.coverageRegression.coverageDelta).toFixed(1)}% - add missing tests`);
}
if (analysis.performanceRegression?.percentageChange > 20) {
recommendations.push(`Test execution time increased by ${analysis.performanceRegression.percentageChange.toFixed(1)}% - investigate performance issues`);
}
return recommendations;
}
/**
* Clean up TDD session and free resources
*/
async closeTDDSession(sessionId) {
const session = this.tddSessions.get(sessionId);
if (!session)
return;
// Kill any running test processes
const runningTest = this.runningTests.get(sessionId);
if (runningTest) {
runningTest.kill();
this.runningTests.delete(sessionId);
}
// Clean up session data
this.sessionManager.updateSession(sessionId, { status: 'closed' });
this.tddSessions.delete(sessionId);
console.log(`๐งช TDD Session ${sessionId} closed`);
}
/**
* Get TDD session status and metrics
*/
getTDDSessionStatus(sessionId) {
return this.tddSessions.get(sessionId);
}
/**
* List all active TDD sessions
*/
getActiveTDDSessions() {
return Array.from(this.tddSessions.values());
}
}
//# sourceMappingURL=tdd-debug-engine.js.map