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
JavaScript
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