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

665 lines โ€ข 30.7 kB
/** * TDD Cycle Integration Handler * Revolutionary TDD workflow integration based on Cycle 31 feedback * Bridges test assertions with runtime behavior validation */ import { BaseToolHandler } from './base-handler.js'; import { execSync } from 'child_process'; import { readFileSync, existsSync } from 'fs'; import path from 'path'; export class TDDCycleIntegrationHandler extends BaseToolHandler { activeCycles = new Map(); tools = [ { name: 'tdd_cycle_start', description: 'Revolutionary TDD integration: Bridge test assertions with runtime behavior. Automatically runs tests โ†’ monitors real app โ†’ validates test expectations match reality.', inputSchema: { type: 'object', properties: { testFile: { type: 'string', description: 'Test file path (e.g., "test/chores_screen_test.dart")' }, sessionId: { type: 'string', description: 'Debug session ID for runtime validation' }, cycleType: { type: 'string', enum: ['red', 'green', 'refactor'], description: 'TDD cycle type', default: 'red' }, autoValidate: { type: 'boolean', description: 'Automatically validate runtime behavior against test assertions', default: true } }, required: ['testFile'] } }, { name: 'tdd_validate_runtime', description: 'Validate that runtime application behavior matches test assertions. Bridges the gap between test expectations and actual app behavior.', inputSchema: { type: 'object', properties: { cycleId: { type: 'string', description: 'TDD cycle ID from tdd_cycle_start' }, sessionId: { type: 'string', description: 'Debug session ID' }, assertions: { type: 'array', description: 'Specific assertions to validate', items: { type: 'object', properties: { type: { type: 'string', enum: ['ui', 'behavior', 'state', 'performance'] }, selector: { type: 'string' }, expectedValue: {} } } } }, required: ['cycleId', 'sessionId'] } }, { name: 'tdd_cycle_complete', description: 'Complete TDD cycle with comprehensive validation report comparing test assertions with runtime behavior.', inputSchema: { type: 'object', properties: { cycleId: { type: 'string', description: 'TDD cycle ID' }, nextCycle: { type: 'string', enum: ['red', 'green', 'refactor'], description: 'Next cycle type' }, generateReport: { type: 'boolean', description: 'Generate comprehensive validation report', default: true } }, required: ['cycleId'] } }, { name: 'tdd_analyze_discrepancies', description: 'Analyze discrepancies between test assertions and runtime behavior. Identifies where tests and reality diverge.', inputSchema: { type: 'object', properties: { cycleId: { type: 'string', description: 'TDD cycle ID' }, includeRecommendations: { type: 'boolean', description: 'Include recommendations for fixing discrepancies', default: true } }, required: ['cycleId'] } } ]; async handle(toolName, args, sessions) { try { switch (toolName) { case 'tdd_cycle_start': return this.startTDDCycle(args, sessions); case 'tdd_validate_runtime': return this.validateRuntime(args, sessions); case 'tdd_cycle_complete': return this.completeTDDCycle(args); case 'tdd_analyze_discrepancies': return this.analyzeDiscrepancies(args); default: return this.createErrorResponse(`Unknown TDD cycle tool: ${toolName}`); } } catch (error) { return this.createErrorResponse(error instanceof Error ? error.message : String(error)); } } async startTDDCycle(args, sessions) { const cycleId = this.generateCycleId(); const testFile = args.testFile; // Validate test file exists if (!existsSync(testFile)) { return this.createErrorResponse(`Test file not found: ${testFile}`); } // Run tests and capture results const testResults = await this.runTests(testFile); // Extract test assertions for runtime validation const assertions = await this.extractTestAssertions(testFile); // Initialize TDD cycle state const cycleState = { testFile, testResults, runtimeValidation: [], assertions, cycle: args.cycleType || 'red', sessionId: args.sessionId }; this.activeCycles.set(cycleId, cycleState); // Auto-validate runtime if session provided and enabled if (args.sessionId && args.autoValidate && sessions?.has(args.sessionId)) { try { await this.performRuntimeValidation(cycleId, args.sessionId, sessions); } catch (error) { // Continue even if runtime validation fails console.warn('Runtime validation failed:', error); } } const sections = [ '๐Ÿงช **TDD Cycle Started**', '', `**Cycle ID**: ${cycleId}`, `**Test File**: ${testFile}`, `**Cycle Type**: ${cycleState.cycle.toUpperCase()}`, `**Session ID**: ${args.sessionId || 'None - manual validation only'}`, '', '## ๐Ÿ“‹ **Test Results**', '' ]; // Test results summary const passed = testResults.filter(t => t.status === 'passed').length; const failed = testResults.filter(t => t.status === 'failed').length; const skipped = testResults.filter(t => t.status === 'skipped').length; sections.push(`**Total Tests**: ${testResults.length}`); sections.push(`**โœ… Passed**: ${passed}`); sections.push(`**โŒ Failed**: ${failed}`); sections.push(`**โญ๏ธ Skipped**: ${skipped}`); sections.push(''); // Show failing tests in RED cycle if (cycleState.cycle === 'red' && failed > 0) { sections.push('**โŒ Failing Tests** (Expected in RED cycle):'); testResults.filter(t => t.status === 'failed').forEach(test => { sections.push(`- **${test.name}**: ${test.error || 'Test failed'}`); }); sections.push(''); } // Show extracted assertions if (assertions.length > 0) { sections.push('## ๐ŸŽฏ **Extracted Test Assertions**'); sections.push(''); assertions.forEach((assertion, index) => { sections.push(`${index + 1}. ${assertion.type.toUpperCase()}: ${assertion.selector || 'General'}`); sections.push(` Expected: ${JSON.stringify(assertion.expectedValue)}`); if (assertion.actualValue !== undefined) { sections.push(` Runtime: ${JSON.stringify(assertion.actualValue)}`); sections.push(` **Matches: ${assertion.runtimeMatches ? 'โœ…' : 'โŒ'}**`); } sections.push(''); }); } // Runtime validation summary if (args.sessionId && cycleState.runtimeValidation.length > 0) { const validated = cycleState.runtimeValidation.filter(v => v.validated).length; sections.push('## ๐Ÿ” **Runtime Validation**'); sections.push(`**Validated**: ${validated}/${cycleState.runtimeValidation.length} assertions`); sections.push(''); // Show discrepancies const discrepancies = cycleState.runtimeValidation.filter(v => !v.validated); if (discrepancies.length > 0) { sections.push('**โš ๏ธ Discrepancies Found**:'); discrepancies.forEach(disc => { sections.push(`- **${disc.assertion}**`); sections.push(` Expected: ${JSON.stringify(disc.expectedBehavior)}`); sections.push(` Actual: ${JSON.stringify(disc.actualBehavior)}`); sections.push(` Issue: ${disc.discrepancy}`); sections.push(''); }); } } sections.push('## ๐Ÿš€ **Next Steps**'); sections.push(`1. Use \`tdd_validate_runtime\` with cycle ID: \`${cycleId}\``); sections.push(`2. Use \`tdd_analyze_discrepancies\` to identify test/runtime gaps`); sections.push(`3. Use \`tdd_cycle_complete\` when ready to move to next cycle`); return this.createTextResponse(sections.join('\n')); } async validateRuntime(args, sessions) { const cycleState = this.activeCycles.get(args.cycleId); if (!cycleState) { return this.createErrorResponse(`TDD cycle not found: ${args.cycleId}`); } const session = sessions?.get(args.sessionId); if (!session?.page) { return this.createErrorResponse(`Debug session not found: ${args.sessionId}`); } // Perform runtime validation if (sessions) { await this.performRuntimeValidation(args.cycleId, args.sessionId, sessions); } const updatedState = this.activeCycles.get(args.cycleId); const validations = updatedState.runtimeValidation; const sections = [ '๐Ÿ” **Runtime Validation Complete**', '', `**Cycle ID**: ${args.cycleId}`, `**Validations**: ${validations.length}`, '' ]; const validated = validations.filter(v => v.validated).length; const failed = validations.filter(v => !v.validated).length; sections.push(`**โœ… Validated**: ${validated}`); sections.push(`**โŒ Failed**: ${failed}`); sections.push(`**Success Rate**: ${Math.round((validated / validations.length) * 100)}%`); sections.push(''); // Show validation details if (validations.length > 0) { sections.push('## ๐Ÿ“Š **Validation Details**'); sections.push(''); validations.forEach((validation, index) => { const status = validation.validated ? 'โœ…' : 'โŒ'; sections.push(`${index + 1}. ${status} **${validation.assertion}**`); if (!validation.validated) { sections.push(` **Expected**: ${JSON.stringify(validation.expectedBehavior)}`); sections.push(` **Actual**: ${JSON.stringify(validation.actualBehavior)}`); sections.push(` **Issue**: ${validation.discrepancy}`); } sections.push(''); }); } return this.createTextResponse(sections.join('\n')); } async completeTDDCycle(args) { const cycleState = this.activeCycles.get(args.cycleId); if (!cycleState) { return this.createErrorResponse(`TDD cycle not found: ${args.cycleId}`); } const sections = [ '๐Ÿ **TDD Cycle Complete**', '', `**Cycle ID**: ${args.cycleId}`, `**Cycle Type**: ${cycleState.cycle.toUpperCase()}`, `**Test File**: ${cycleState.testFile}`, '' ]; // Comprehensive summary const testsPassed = cycleState.testResults.filter(t => t.status === 'passed').length; const testsFailed = cycleState.testResults.filter(t => t.status === 'failed').length; const runtimeValidated = cycleState.runtimeValidation.filter(v => v.validated).length; const runtimeTotal = cycleState.runtimeValidation.length; sections.push('## ๐Ÿ“ˆ **Cycle Summary**'); sections.push(`**Tests**: ${testsPassed} passed, ${testsFailed} failed`); sections.push(`**Runtime Validation**: ${runtimeValidated}/${runtimeTotal} assertions validated`); // Calculate overall success const testSuccessRate = testsPassed / (testsPassed + testsFailed); const runtimeSuccessRate = runtimeTotal > 0 ? runtimeValidated / runtimeTotal : 1; const overallSuccess = (testSuccessRate + runtimeSuccessRate) / 2; sections.push(`**Overall Success**: ${Math.round(overallSuccess * 100)}%`); sections.push(''); // Cycle-specific analysis sections.push('## ๐ŸŽฏ **Cycle Analysis**'); if (cycleState.cycle === 'red') { sections.push('**RED Cycle**: Write failing tests first'); if (testsFailed > 0) { sections.push('โœ… Good - Tests are failing as expected'); sections.push('**Next**: Implement minimal code to make tests pass (GREEN cycle)'); } else { sections.push('โš ๏ธ Warning - All tests passing in RED cycle'); sections.push('**Recommendation**: Write more specific failing tests'); } } else if (cycleState.cycle === 'green') { sections.push('**GREEN Cycle**: Make tests pass with minimal code'); if (testsPassed > testsFailed) { sections.push('โœ… Good - Tests are now passing'); sections.push('**Next**: Refactor code while keeping tests green (REFACTOR cycle)'); } else { sections.push('โŒ Tests still failing - continue implementing'); } } else if (cycleState.cycle === 'refactor') { sections.push('**REFACTOR Cycle**: Improve code while keeping tests green'); if (testsPassed === cycleState.testResults.length) { sections.push('โœ… Excellent - All tests still passing after refactor'); sections.push('**Next**: Start new RED cycle for next feature'); } else { sections.push('โŒ Refactoring broke tests - revert and try again'); } } sections.push(''); // Runtime validation insights if (runtimeTotal > 0) { sections.push('## ๐Ÿ” **Runtime Validation Insights**'); const discrepancies = cycleState.runtimeValidation.filter(v => !v.validated); if (discrepancies.length === 0) { sections.push('โœ… Perfect - All test assertions match runtime behavior'); } else { sections.push(`โš ๏ธ ${discrepancies.length} discrepancies between tests and runtime`); sections.push('**Recommendations**:'); discrepancies.forEach(disc => { sections.push(`- Update test for: ${disc.assertion}`); }); } sections.push(''); } // Next cycle recommendation if (args.nextCycle) { sections.push(`## ๐Ÿš€ **Next Cycle: ${args.nextCycle.toUpperCase()}**`); sections.push(`Ready to start next TDD cycle with: \`tdd_cycle_start\``); } // Clean up cycle state this.activeCycles.delete(args.cycleId); return this.createTextResponse(sections.join('\n')); } async analyzeDiscrepancies(args) { const cycleState = this.activeCycles.get(args.cycleId); if (!cycleState) { return this.createErrorResponse(`TDD cycle not found: ${args.cycleId}`); } const discrepancies = cycleState.runtimeValidation.filter(v => !v.validated); const sections = [ '๐Ÿ”ฌ **Test-Runtime Discrepancy Analysis**', '', `**Cycle ID**: ${args.cycleId}`, `**Discrepancies Found**: ${discrepancies.length}`, '' ]; if (discrepancies.length === 0) { sections.push('โœ… **Perfect Alignment**'); sections.push('All test assertions match runtime behavior exactly.'); sections.push('Your TDD cycle is working perfectly!'); return this.createTextResponse(sections.join('\n')); } sections.push('## โš ๏ธ **Discrepancies Detected**'); sections.push(''); discrepancies.forEach((disc, index) => { sections.push(`### ${index + 1}. ${disc.assertion}`); sections.push(`**Expected (Test)**: ${JSON.stringify(disc.expectedBehavior)}`); sections.push(`**Actual (Runtime)**: ${JSON.stringify(disc.actualBehavior)}`); sections.push(`**Issue**: ${disc.discrepancy}`); sections.push(''); if (args.includeRecommendations) { sections.push('**๐Ÿ’ก Recommendations**:'); // Generate specific recommendations based on discrepancy type if (disc.assertion.includes('selector')) { sections.push('- Check if UI element exists with correct selector'); sections.push('- Verify element is visible and interactive'); sections.push('- Update test selector if UI changed'); } else if (disc.assertion.includes('text') || disc.assertion.includes('content')) { sections.push('- Verify text content matches exactly (case-sensitive)'); sections.push('- Check for extra whitespace or formatting differences'); sections.push('- Update test expectation if UI text changed'); } else if (disc.assertion.includes('state')) { sections.push('- Verify application state is correctly set'); sections.push('- Check if state changes are properly triggered'); sections.push('- Update test to match current state behavior'); } else { sections.push('- Investigate why runtime behavior differs from test expectation'); sections.push('- Consider if test assertion needs updating'); sections.push('- Verify implementation matches test requirements'); } sections.push(''); } }); // Overall recommendations if (args.includeRecommendations) { sections.push('## ๐ŸŽฏ **Overall Recommendations**'); sections.push(''); if (discrepancies.length > 5) { sections.push('**High discrepancy count suggests**:'); sections.push('- Tests may be outdated relative to implementation'); sections.push('- Implementation may not match original requirements'); sections.push('- Consider reviewing TDD process'); } else { sections.push('**Low discrepancy count suggests**:'); sections.push('- Minor alignment issues'); sections.push('- Good TDD process overall'); sections.push('- Focus on fixing specific issues'); } sections.push(''); sections.push('**Next Steps**:'); sections.push('1. Fix highest priority discrepancies first'); sections.push('2. Re-run `tdd_validate_runtime` to verify fixes'); sections.push('3. Update tests or implementation as needed'); sections.push('4. Aim for 100% test-runtime alignment'); } return this.createTextResponse(sections.join('\n')); } async runTests(testFile) { try { // Determine test framework and run appropriate command const ext = path.extname(testFile); let command = ''; if (ext === '.dart') { // Flutter/Dart tests command = `flutter test ${testFile}`; } else if (ext === '.js' || ext === '.ts') { // JavaScript/TypeScript tests if (existsSync('package.json')) { const pkg = JSON.parse(readFileSync('package.json', 'utf-8')); if (pkg.scripts?.test) { command = 'npm test'; } else { command = `jest ${testFile}`; } } } else if (ext === '.py') { // Python tests command = `pytest ${testFile} -v`; } if (!command) { return []; } const output = execSync(command, { encoding: 'utf-8', timeout: 30000 }); // Parse test output to extract results return this.parseTestOutput(output, ext); } catch (error) { // Handle test failures (which is expected in RED cycle) if (error.stdout || error.stderr) { return this.parseTestOutput(error.stdout || error.stderr, path.extname(testFile)); } return []; } } parseTestOutput(output, fileType) { const results = []; if (fileType === '.dart') { // Parse Flutter test output const lines = output.split('\n'); for (const line of lines) { if (line.includes('โœ“') || line.includes('โœ—')) { const match = line.match(/(โœ“|โœ—)\s+(.+?)(?:\s+\((\d+)ms\))?/); if (match) { results.push({ name: match[2].trim(), status: match[1] === 'โœ“' ? 'passed' : 'failed', duration: parseInt(match[3] || '0'), assertion: match[2].trim(), error: match[1] === 'โœ—' ? 'Test failed' : undefined }); } } } } else if (fileType === '.js' || fileType === '.ts') { // Parse Jest output const lines = output.split('\n'); for (const line of lines) { if (line.includes('โœ“') || line.includes('โœ—') || line.includes('PASS') || line.includes('FAIL')) { // Simple parsing - could be enhanced results.push({ name: 'Test case', status: line.includes('โœ“') || line.includes('PASS') ? 'passed' : 'failed', duration: 0, assertion: line.trim() }); } } } // If no specific results found, create generic ones if (results.length === 0) { results.push({ name: 'Test execution', status: output.includes('failed') || output.includes('error') ? 'failed' : 'passed', duration: 0, assertion: 'General test execution' }); } return results; } async extractTestAssertions(testFile) { try { const content = readFileSync(testFile, 'utf-8'); const assertions = []; // Extract common test assertion patterns const patterns = [ // Flutter test patterns /expect\(find\.text\(['"`]([^'"`]+)['"`]\), findsOneWidget\)/g, /expect\(find\.byKey\(.*?['"`]([^'"`]+)['"`].*?\), findsOneWidget\)/g, /expect\(find\.byType\((\w+)\), findsOneWidget\)/g, // Jest/testing-library patterns /expect\(.*?\.toHaveTextContent\(['"`]([^'"`]+)['"`]\)/g, /expect\(.*?\.toBeVisible\(\)/g, /expect\(.*?\.toBeInTheDocument\(\)/g, // General expect patterns /expect\(([^)]+)\)\.(?:toBe|toEqual)\(([^)]+)\)/g ]; for (const pattern of patterns) { let match; while ((match = pattern.exec(content)) !== null) { let type = 'ui'; let selector = ''; let expectedValue = ''; if (match[0].includes('text') || match[0].includes('Text')) { type = 'ui'; selector = `text="${match[1]}"`; expectedValue = match[1]; } else if (match[0].includes('byKey') || match[0].includes('Key')) { type = 'ui'; selector = `[key="${match[1]}"]`; expectedValue = 'exists'; } else if (match[0].includes('byType') || match[0].includes('Type')) { type = 'ui'; selector = match[1]; expectedValue = 'exists'; } else { type = 'behavior'; expectedValue = match[2] || match[1]; } assertions.push({ type, selector, expectedValue, runtimeMatches: false }); } } return assertions; } catch (error) { console.warn('Failed to extract test assertions:', error); return []; } } async performRuntimeValidation(cycleId, sessionId, sessions) { const cycleState = this.activeCycles.get(cycleId); const session = sessions.get(sessionId); if (!cycleState || !session?.page) { return; } const validations = []; for (const assertion of cycleState.assertions) { try { let actualBehavior; let validated = false; let discrepancy = ''; if (assertion.type === 'ui' && assertion.selector) { // Validate UI assertions if (assertion.selector.startsWith('text=')) { const text = assertion.selector.replace('text=', '').replace(/"/g, ''); const elements = await session.page.$$eval('*', (els) => els.filter(el => el.textContent?.includes(text)).length); actualBehavior = elements > 0 ? 'exists' : 'not found'; validated = elements > 0; if (!validated) { discrepancy = `Text "${text}" not found in page`; } } else if (assertion.selector.includes('[key=')) { const key = assertion.selector.match(/\[key="([^"]+)"\]/)?.[1]; if (key) { const element = await session.page.$(`[data-testid="${key}"], [key="${key}"]`); actualBehavior = element ? 'exists' : 'not found'; validated = !!element; if (!validated) { discrepancy = `Element with key "${key}" not found`; } } } else { // Generic selector const element = await session.page.$(assertion.selector); actualBehavior = element ? 'exists' : 'not found'; validated = !!element; if (!validated) { discrepancy = `Element "${assertion.selector}" not found`; } } } else { // For non-UI assertions, mark as validated for now actualBehavior = assertion.expectedValue; validated = true; } validations.push({ assertion: `${assertion.type}: ${assertion.selector || 'general'}`, actualBehavior, expectedBehavior: assertion.expectedValue, validated, discrepancy: validated ? undefined : discrepancy }); // Update assertion with runtime data assertion.actualValue = actualBehavior; assertion.runtimeMatches = validated; } catch (error) { validations.push({ assertion: `${assertion.type}: ${assertion.selector || 'general'}`, actualBehavior: 'error', expectedBehavior: assertion.expectedValue, validated: false, discrepancy: `Runtime validation error: ${error}` }); } } cycleState.runtimeValidation = validations; this.activeCycles.set(cycleId, cycleState); } generateCycleId() { return `tdd_cycle_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } } //# sourceMappingURL=tdd-cycle-integration-handler.js.map