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

338 lines (336 loc) • 13.5 kB
import { ASTChangeDetector } from './ast-change-detector.js'; import { SelectorAutoUpdater } from './selector-auto-updater.js'; import { SmartTestMaintenance } from './smart-test-maintenance.js'; import { exec } from 'child_process'; import { promisify } from 'util'; import * as fs from 'fs/promises'; import * as path from 'path'; const execAsync = promisify(exec); export class TestEvolutionEngine { astDetector; selectorUpdater; maintenanceEngine; constructor() { this.astDetector = new ASTChangeDetector(); this.selectorUpdater = new SelectorAutoUpdater(this.astDetector); this.maintenanceEngine = new SmartTestMaintenance(); } /** * Evolve tests based on git changes */ async evolveTestsFromGitChanges(options = {}) { const result = { success: false, filesUpdated: [], testsEvolved: 0, errors: [], suggestions: [] }; try { // Get uncommitted changes const changes = await this.getGitChanges(); if (changes.length === 0) { result.suggestions.push('No uncommitted changes detected'); result.success = true; return result; } // Analyze each changed file for (const change of changes) { if (change.status === 'deleted') { // Handle deleted files await this.handleDeletedFile(change, result); } else { // Analyze code changes await this.analyzeAndEvolveTests(change, result, options); } } // Run test maintenance on affected test files if (result.filesUpdated.length > 0) { await this.runTestMaintenance(result.filesUpdated, result); } // Optionally commit changes if (options.autoCommit && !options.dryRun && result.filesUpdated.length > 0) { await this.commitTestUpdates(result); } result.success = true; } catch (error) { result.errors.push(`Evolution failed: ${error}`); } return result; } /** * Monitor file changes and evolve tests in real-time */ async watchAndEvolve(watchPath, options = {}) { // This would use a file watcher like chokidar // For now, this is a placeholder console.log(`Watching ${watchPath} for changes...`); // In a real implementation: // 1. Watch for file changes // 2. Debounce changes // 3. Run evolution on changed files // 4. Update tests automatically } /** * Get git changes for analysis */ async getGitChanges() { const changes = []; try { // Get list of changed files const { stdout } = await execAsync('git status --porcelain'); const lines = stdout.trim().split('\n').filter(line => line); for (const line of lines) { const status = line.substring(0, 2).trim(); const file = line.substring(3); // Skip test files and non-code files if (file.includes('.test.') || file.includes('.spec.') || !this.isCodeFile(file)) { continue; } const change = { file, status: status === 'M' ? 'modified' : status === 'A' ? 'added' : 'deleted' }; // Get file contents for modified files if (change.status === 'modified') { try { // Get current content change.newContent = await fs.readFile(file, 'utf-8'); // Get original content const { stdout: oldContent } = await execAsync(`git show HEAD:${file}`); change.oldContent = oldContent; } catch (error) { // File might be new or have issues console.warn(`Could not get content for ${file}: ${error}`); } } else if (change.status === 'added') { change.newContent = await fs.readFile(file, 'utf-8'); } changes.push(change); } } catch (error) { console.error('Error getting git changes:', error); } return changes; } /** * Analyze changes and evolve related tests */ async analyzeAndEvolveTests(change, result, options) { if (!change.oldContent || !change.newContent) { return; } // Detect AST changes const language = this.getLanguageFromFile(change.file); const astChanges = this.astDetector.detectChanges(change.oldContent, change.newContent, language); if (!astChanges.hasChanges) { return; } // Find related test files const testFiles = await this.findRelatedTestFiles(change.file); if (testFiles.length === 0) { result.suggestions.push(`No test files found for ${change.file}`); return; } // Analyze selector changes and update tests const selectorAnalysis = await this.selectorUpdater.analyzeSelectorChangesFromAST(astChanges, change.file); if (selectorAnalysis.hasChanges) { // Update selectors in test files const updateResult = await this.selectorUpdater.analyzeAndUpdateSelectors({ componentFile: { path: change.file, originalCode: change.oldContent, modifiedCode: change.newContent }, testFiles: testFiles.map(f => ({ path: f, code: '' })) // Code will be loaded by updater }); if (updateResult.hasUpdates) { result.filesUpdated.push(...updateResult.selectorUpdates.map((u) => u.testFile)); result.testsEvolved += updateResult.selectorUpdates.length; } } // Generate test update suggestions if (astChanges.testSuggestions) { result.suggestions.push(...astChanges.testSuggestions); } // Handle specific change types // Combine all change types from astChanges const allChanges = [ ...astChanges.functionChanges.map(c => ({ ...c, type: 'function' })), ...astChanges.componentChanges.map(c => ({ ...c, type: 'component' })), ...astChanges.liveViewChanges.map(c => ({ ...c, type: 'liveview' })) ]; for (const change of allChanges) { await this.handleSpecificChange(change, testFiles, result, options); } } /** * Handle specific types of changes */ async handleSpecificChange(change, testFiles, result, options) { switch (change.type) { case 'function': const funcChange = change; if (funcChange.changeType === 'removed') { result.suggestions.push(`Function ${funcChange.functionName} was deleted - consider removing related tests`); } else if (funcChange.changeType === 'signature_modified' && funcChange.newParams) { result.suggestions.push(`Function ${funcChange.functionName} signature changed - update test calls`); } break; case 'component': const compChange = change; if (compChange.changeType === 'props_modified' && compChange.addedProps?.length) { result.suggestions.push(`Component ${compChange.componentName} props changed - update test props`); } break; case 'liveview': const lvChange = change; if (lvChange.changeType === 'event_handlers_modified') { result.suggestions.push(`LiveView ${lvChange.moduleName} event handlers changed - update test interactions`); } break; } } /** * Handle deleted files by deprecating related tests */ async handleDeletedFile(change, result) { const testFiles = await this.findRelatedTestFiles(change.file); for (const testFile of testFiles) { result.suggestions.push(`File ${change.file} was deleted - consider removing ${testFile}`); // Mark tests as deprecated try { const content = await fs.readFile(testFile, 'utf-8'); const updatedContent = `// DEPRECATED: Source file ${change.file} was deleted\n// TODO: Remove this test file or update for new implementation\n${content}`; await fs.writeFile(testFile, updatedContent); result.filesUpdated.push(testFile); } catch (error) { result.errors.push(`Could not deprecate ${testFile}: ${error}`); } } } /** * Find test files related to a source file */ async findRelatedTestFiles(sourceFile) { const testFiles = []; const baseName = path.basename(sourceFile, path.extname(sourceFile)); const dir = path.dirname(sourceFile); // Common test file patterns const patterns = [ `${baseName}.test.ts`, `${baseName}.test.js`, `${baseName}.spec.ts`, `${baseName}.spec.js`, `${baseName}_test.exs`, `test/${baseName}.test.ts`, `tests/${baseName}.test.ts`, `__tests__/${baseName}.test.ts`, `test/${baseName}_test.exs` ]; // Check each pattern for (const pattern of patterns) { const testPath = path.join(dir, pattern); try { await fs.access(testPath); testFiles.push(testPath); } catch { // Try from project root try { await fs.access(pattern); testFiles.push(pattern); } catch { // File doesn't exist } } } return testFiles; } /** * Run test maintenance on updated files */ async runTestMaintenance(testFiles, result) { for (const testFile of testFiles) { try { const testCode = await fs.readFile(testFile, 'utf-8'); // Detect common issues in test code const issues = this.detectTestIssues(testCode); if (issues.length > 0) { // Auto-fix simple issues let fixedCode = testCode; let fixCount = 0; for (const issue of issues) { if (issue.type === 'outdated_selector') { // Update selectors based on changes we detected earlier fixedCode = fixedCode.replace(issue.oldValue, issue.newValue); fixCount++; } } if (fixedCode !== testCode) { await fs.writeFile(testFile, fixedCode); result.suggestions.push(`Auto-fixed ${fixCount} issues in ${testFile}`); } } } catch (error) { result.errors.push(`Maintenance failed for ${testFile}: ${error}`); } } } /** * Detect common test issues without needing a browser page */ detectTestIssues(testCode) { const issues = []; // Simple pattern matching for common issues // This is a placeholder - in a real implementation, this would be more sophisticated return issues; } /** * Commit test updates */ async commitTestUpdates(result) { try { // Stage updated test files for (const file of result.filesUpdated) { await execAsync(`git add ${file}`); } // Create commit message const message = `test: Auto-evolve tests for code changes - Updated ${result.testsEvolved} tests across ${result.filesUpdated.length} files - Automated selector updates and test maintenance - Generated by Test Evolution Engine šŸ¤– Generated with AI Debug Local MCP`; await execAsync(`git commit -m "${message}"`); result.suggestions.push('Test updates committed successfully'); } catch (error) { result.errors.push(`Could not commit changes: ${error}`); } } /** * Check if file is a code file */ isCodeFile(file) { const codeExtensions = ['.ts', '.tsx', '.js', '.jsx', '.ex', '.exs', '.vue', '.svelte']; return codeExtensions.some(ext => file.endsWith(ext)); } /** * Get language from file extension */ getLanguageFromFile(file) { if (file.endsWith('.ex') || file.endsWith('.exs')) { return 'elixir'; } else if (file.endsWith('.vue') || file.endsWith('.svelte')) { return 'template'; } return 'javascript'; } } //# sourceMappingURL=test-evolution-engine.js.map