UNPKG

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
/** * 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