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