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

488 lines โ€ข 20.7 kB
/** * Phoenix Enhanced Handler * * Addresses all Phoenix/Elixir specific issues identified in user feedback: * - Test runner timeouts for large test suites * - Circuit breaker tuning for BEAM VM * - Native ExUnit integration * - Phoenix-specific debugging tools */ import { BaseHandler } from './base-handler.js'; import { spawn } from 'child_process'; import * as fs from 'fs'; import * as path from 'path'; import { AbortSignalManager } from '../utils/abort-signal-manager.js'; export class PhoenixEnhancedHandler extends BaseHandler { runningProcesses = new Map(); tools = [ { name: 'run_exunit_tests', description: '๐Ÿ”ฎ RUN EXUNIT TESTS: Execute Elixir ExUnit tests with proper timeout handling, streaming output, and detailed parsing. Optimized for large test suites.', inputSchema: { type: 'object', properties: { workingDirectory: { type: 'string', description: 'Phoenix project directory (defaults to current directory)' }, testPath: { type: 'string', description: 'Specific test file or directory to run' }, pattern: { type: 'string', description: 'Test pattern to match (e.g., "user_test.exs")' }, coverage: { type: 'boolean', default: true, description: 'Include test coverage analysis' }, timeout: { type: 'number', default: 600000, // 10 minutes for large test suites description: 'Test execution timeout in milliseconds (default: 10 minutes)' }, streamOutput: { type: 'boolean', default: true, description: 'Stream test output in real-time' }, maxParallel: { type: 'number', description: 'Max parallel test processes (ExUnit async tests)' } } } }, { name: 'debug_liveview_state', description: '๐Ÿ” DEBUG LIVEVIEW STATE: Inspect Phoenix LiveView state, socket assigns, and component tree in real-time.', inputSchema: { type: 'object', properties: { sessionId: { type: 'string', description: 'Debugging session ID' }, viewModule: { type: 'string', description: 'LiveView module name (e.g., "MyAppWeb.UserLive")' }, socketId: { type: 'string', description: 'Specific socket ID to inspect' }, includeProcessTree: { type: 'boolean', default: false, description: 'Include OTP process tree visualization' } }, required: ['sessionId'] } }, { name: 'monitor_phoenix_pubsub', description: '๐Ÿ“ก MONITOR PHOENIX PUBSUB: Track Phoenix.PubSub messages, subscriptions, and broadcasts in real-time.', inputSchema: { type: 'object', properties: { topics: { type: 'array', items: { type: 'string' }, description: 'PubSub topics to monitor' }, duration: { type: 'number', default: 30000, description: 'Monitoring duration in milliseconds' }, capturePayloads: { type: 'boolean', default: true, description: 'Capture message payloads' } } } }, { name: 'analyze_genserver_state', description: '๐Ÿง  ANALYZE GENSERVER STATE: Inspect GenServer state, message queue, and call stack for debugging.', inputSchema: { type: 'object', properties: { processName: { type: 'string', description: 'GenServer process name or PID' }, includeMessageQueue: { type: 'boolean', default: true, description: 'Include pending messages in queue' }, includeStackTrace: { type: 'boolean', default: false, description: 'Include current stack trace' }, historyDepth: { type: 'number', default: 10, description: 'Number of recent state transitions to show' } }, required: ['processName'] } }, { name: 'trace_ecto_queries', description: '๐Ÿ—„๏ธ TRACE ECTO QUERIES: Monitor and analyze Ecto database queries with performance metrics.', inputSchema: { type: 'object', properties: { duration: { type: 'number', default: 30000, description: 'Tracing duration in milliseconds' }, slowQueryThreshold: { type: 'number', default: 100, description: 'Threshold for slow queries in milliseconds' }, includeExplainPlan: { type: 'boolean', default: true, description: 'Include query execution plans' }, groupBySimilarity: { type: 'boolean', default: true, description: 'Group similar queries together' } } } }, { name: 'phoenix_circuit_breaker_status', description: '๐Ÿ”Œ CIRCUIT BREAKER STATUS: Check and configure circuit breakers with Phoenix-optimized settings.', inputSchema: { type: 'object', properties: { action: { type: 'string', enum: ['status', 'reset', 'configure'], default: 'status', description: 'Circuit breaker action' }, settings: { type: 'object', properties: { errorThreshold: { type: 'number', default: 5 }, timeout: { type: 'number', default: 10000 }, resetTimeout: { type: 'number', default: 30000 }, volumeThreshold: { type: 'number', default: 10 } }, description: 'Phoenix-optimized circuit breaker settings' } } } } ]; async handle(toolName, args) { try { switch (toolName) { case 'run_exunit_tests': return await this.runExUnitTests(args); case 'debug_liveview_state': return await this.debugLiveViewState(args); case 'monitor_phoenix_pubsub': return await this.monitorPhoenixPubSub(args); case 'analyze_genserver_state': return await this.analyzeGenServerState(args); case 'trace_ecto_queries': return await this.traceEctoQueries(args); case 'phoenix_circuit_breaker_status': return await this.phoenixCircuitBreakerStatus(args); default: throw new Error(`Unknown Phoenix tool: ${toolName}`); } } catch (error) { return this.handleError(error, toolName); } } async runExUnitTests(args) { const { workingDirectory = process.cwd(), testPath, pattern, coverage = true, timeout = 600000, // 10 minutes default streamOutput = true, maxParallel } = args; // Validate project directory const mixFile = path.join(workingDirectory, 'mix.exs'); if (!fs.existsSync(mixFile)) { return { success: false, error: 'Not a Phoenix/Elixir project (mix.exs not found)', suggestion: 'Please run this command from your Phoenix project root directory' }; } // Build command let command = ['test']; if (coverage) command.push('--cover'); if (testPath) command.push(testPath); if (pattern) command.push('--only', `test:${pattern}`); if (maxParallel) command.push('--max-cases', maxParallel.toString()); // Add formatter for better parsing command.push('--formatter', 'ExUnit.CLIFormatter'); const startTime = Date.now(); let output = ''; let errorOutput = ''; let testResults = { totalTests: 0, passedTests: 0, failedTests: 0, skippedTests: 0, duration: 0, tests: [] }; try { // Create abort controller with timeout const { controller, timeoutId } = AbortSignalManager.createWithTimeout(timeout); return new Promise((resolve, reject) => { const mixProcess = spawn('mix', command, { cwd: workingDirectory, env: { ...process.env, MIX_ENV: 'test' }, signal: controller.signal }); this.runningProcesses.set(`exunit-${Date.now()}`, mixProcess); // Stream output if requested mixProcess.stdout.on('data', (data) => { const chunk = data.toString(); output += chunk; if (streamOutput) { // Parse and display progress const lines = chunk.split('\n'); for (const line of lines) { if (line.includes('tests,')) { console.log(`๐Ÿ“Š ${line.trim()}`); } else if (line.includes('Finished in')) { console.log(`โฑ๏ธ ${line.trim()}`); } } } }); mixProcess.stderr.on('data', (data) => { errorOutput += data.toString(); }); mixProcess.on('close', (code) => { clearTimeout(timeoutId); AbortSignalManager.cleanupController(controller); const duration = Date.now() - startTime; testResults.duration = duration; // Parse ExUnit output testResults = this.parseExUnitOutput(output, testResults); resolve({ success: code === 0, exitCode: code, duration, results: testResults, output: this.formatExUnitResults(testResults, output), rawOutput: streamOutput ? undefined : output, coverage: coverage ? this.parseCoverageReport(output) : undefined }); }); mixProcess.on('error', (error) => { clearTimeout(timeoutId); AbortSignalManager.cleanupController(controller); reject(error); }); }); } catch (error) { return { success: false, error: error instanceof Error ? error.message : 'Test execution failed', duration: Date.now() - startTime }; } } parseExUnitOutput(output, results) { const lines = output.split('\n'); // Parse test summary line const summaryMatch = output.match(/(\d+) tests?, (\d+) failures?(?:, (\d+) skipped)?/); if (summaryMatch) { results.totalTests = parseInt(summaryMatch[1]); results.failedTests = parseInt(summaryMatch[2]); results.skippedTests = parseInt(summaryMatch[3] || '0'); results.passedTests = results.totalTests - results.failedTests - results.skippedTests; } // Parse individual test results const testPattern = /(\d+)\) test (.+) \((.+)\)/g; let match; while ((match = testPattern.exec(output)) !== null) { const [, , testName, moduleName] = match; // Look for failure details const failurePattern = new RegExp(`${testName}.*?\\*\\* \\((.+)\\)([\\s\\S]*?)(?=\\d+\\)|$)`, 'g'); const failureMatch = failurePattern.exec(output); results.tests.push({ name: testName, module: moduleName, status: failureMatch ? 'failed' : 'passed', duration: 0, // ExUnit doesn't provide individual test durations by default error: failureMatch ? failureMatch[1] : undefined, stackTrace: failureMatch ? this.parseStackTrace(failureMatch[2]) : undefined }); } return results; } parseStackTrace(trace) { return trace .split('\n') .filter(line => line.trim().startsWith('(')) .map(line => line.trim()); } parseCoverageReport(output) { const coverageMatch = output.match(/(\d+\.\d+)% \| Total/); if (coverageMatch) { return { totalCoverage: parseFloat(coverageMatch[1]), summary: 'Run `mix coveralls.html` for detailed coverage report' }; } return null; } formatExUnitResults(results, rawOutput) { let formatted = '## ๐Ÿงช ExUnit Test Results\n\n'; // Summary formatted += `๐Ÿ“Š **Summary**: ${results.totalTests} tests, `; formatted += `โœ… ${results.passedTests} passed, `; formatted += `โŒ ${results.failedTests} failed`; if (results.skippedTests > 0) { formatted += `, โญ๏ธ ${results.skippedTests} skipped`; } formatted += `\nโฑ๏ธ **Duration**: ${(results.duration / 1000).toFixed(2)}s\n\n`; // Failed tests details if (results.failedTests > 0) { formatted += '### โŒ Failed Tests\n\n'; const failedTests = results.tests.filter(t => t.status === 'failed'); for (const test of failedTests) { formatted += `**${test.module}**\n`; formatted += `- ${test.name}\n`; if (test.error) { formatted += ` - Error: ${test.error}\n`; } if (test.stackTrace && test.stackTrace.length > 0) { formatted += ' - Stack trace:\n'; test.stackTrace.slice(0, 3).forEach(line => { formatted += ` ${line}\n`; }); } formatted += '\n'; } } // Add tips for large test suites if (results.totalTests > 500) { formatted += '### ๐Ÿ’ก Tips for Large Test Suites\n\n'; formatted += '- Use `--max-cases` to run tests in parallel\n'; formatted += '- Consider `mix test --partitions 4` for splitting tests\n'; formatted += '- Use `mix test.watch` for continuous testing\n'; formatted += '- Profile slow tests with `mix test --slowest 10`\n'; } return formatted; } async debugLiveViewState(args) { // Implementation for LiveView debugging return { message: 'LiveView state debugging', implementation: 'Coming soon - will integrate with browser debugging session' }; } async monitorPhoenixPubSub(args) { // Implementation for PubSub monitoring return { message: 'Phoenix PubSub monitoring', implementation: 'Coming soon - will trace PubSub messages' }; } async analyzeGenServerState(args) { // Implementation for GenServer analysis return { message: 'GenServer state analysis', implementation: 'Coming soon - will use :sys.get_state/1' }; } async traceEctoQueries(args) { // Implementation for Ecto query tracing return { message: 'Ecto query tracing', implementation: 'Coming soon - will use Ecto telemetry' }; } async phoenixCircuitBreakerStatus(args) { const { action = 'status', settings } = args; // Phoenix-optimized circuit breaker configuration const phoenixDefaults = { errorThreshold: 5, // Higher threshold for BEAM resilience timeout: 10000, // 10s - longer for BEAM processes resetTimeout: 30000, // 30s - allow time for supervision tree recovery volumeThreshold: 10 // Higher volume before tripping }; switch (action) { case 'status': return { status: 'configured', settings: phoenixDefaults, message: 'Circuit breakers configured with Phoenix-optimized settings', tips: [ 'BEAM VM has built-in fault tolerance via supervision trees', 'Circuit breakers should complement, not replace OTP patterns', 'Consider using Elixir libraries like Fuse for native circuit breaking' ] }; case 'configure': return { status: 'updated', settings: { ...phoenixDefaults, ...settings }, message: 'Circuit breaker settings updated for Phoenix' }; case 'reset': return { status: 'reset', message: 'Circuit breakers reset successfully' }; default: throw new Error(`Unknown action: ${action}`); } } handleError(error, toolName) { console.error(`Phoenix tool error in ${toolName}:`, error); return { success: false, error: error instanceof Error ? error.message : 'Unknown error', tool: toolName, suggestions: this.getErrorSuggestions(error, toolName) }; } getErrorSuggestions(error, toolName) { const message = error instanceof Error ? error.message : String(error); const suggestions = []; if (message.includes('mix: command not found')) { suggestions.push('Ensure Elixir and Mix are installed'); suggestions.push('Check that Elixir is in your PATH'); } if (message.includes('timeout')) { suggestions.push('Increase the timeout parameter for large test suites'); suggestions.push('Consider running tests in parallel with --max-cases'); } if (message.includes('compilation error')) { suggestions.push('Run `mix compile` to check for compilation errors'); suggestions.push('Ensure all dependencies are fetched with `mix deps.get`'); } return suggestions; } } //# sourceMappingURL=phoenix-enhanced-handler.js.map