@ai-capabilities-suite/mcp-debugger-core
Version:
Core debugging engine for Node.js and TypeScript applications. Provides Inspector Protocol integration, breakpoint management, variable inspection, execution control, profiling, hang detection, and source map support.
402 lines • 15 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.parseJestOutput = parseJestOutput;
exports.parseMochaOutput = parseMochaOutput;
exports.parseVitestOutput = parseVitestOutput;
exports.executeTests = executeTests;
const child_process_1 = require("child_process");
/**
* Parse Jest JSON output
*/
function parseJestOutput(stdout, stderr) {
const suites = [];
let totalTests = 0;
let passedTests = 0;
let failedTests = 0;
let skippedTests = 0;
try {
// Jest outputs JSON when using --json flag
// Try to find JSON in stdout
const jsonMatch = stdout.match(/\{[\s\S]*"testResults"[\s\S]*\}/);
if (jsonMatch) {
const result = JSON.parse(jsonMatch[0]);
if (result.testResults) {
for (const testResult of result.testResults) {
const tests = [];
if (testResult.assertionResults) {
for (const assertion of testResult.assertionResults) {
const test = {
name: assertion.fullName || assertion.title,
status: assertion.status === 'passed'
? 'passed'
: assertion.status === 'pending'
? 'skipped'
: 'failed',
duration: assertion.duration,
};
if (assertion.status === 'failed' && assertion.failureMessages) {
test.failureMessage = assertion.failureMessages.join('\n');
test.failureStack = assertion.failureMessages.join('\n');
}
tests.push(test);
if (test.status === 'passed')
passedTests++;
else if (test.status === 'failed')
failedTests++;
else if (test.status === 'skipped')
skippedTests++;
totalTests++;
}
}
suites.push({
name: testResult.name || 'Unknown Suite',
tests,
duration: testResult.perfStats?.runtime,
});
}
}
}
}
catch (error) {
// If JSON parsing fails, try to parse text output
// This is a fallback for when --json is not used
const lines = stdout.split('\n');
let currentSuite = null;
for (const line of lines) {
// Look for test results in text format
if (line.includes('PASS') || line.includes('FAIL')) {
if (currentSuite) {
suites.push(currentSuite);
}
currentSuite = {
name: line.trim(),
tests: [],
};
}
else if (line.includes('✓') || line.includes('✔')) {
passedTests++;
totalTests++;
if (currentSuite) {
currentSuite.tests.push({
name: line.trim(),
status: 'passed',
});
}
}
else if (line.includes('✕') || line.includes('×')) {
failedTests++;
totalTests++;
if (currentSuite) {
currentSuite.tests.push({
name: line.trim(),
status: 'failed',
});
}
}
}
if (currentSuite) {
suites.push(currentSuite);
}
}
return {
suites,
totalTests,
passedTests,
failedTests,
skippedTests,
};
}
/**
* Parse Mocha output
*/
function parseMochaOutput(stdout, stderr) {
const suites = [];
let totalTests = 0;
let passedTests = 0;
let failedTests = 0;
let skippedTests = 0;
try {
// Mocha can output JSON with --reporter json
const jsonMatch = stdout.match(/\{[\s\S]*"tests"[\s\S]*\}/);
if (jsonMatch) {
const result = JSON.parse(jsonMatch[0]);
if (result.tests) {
const suiteMap = new Map();
for (const test of result.tests) {
const suiteName = test.fullTitle?.split(' ')[0] || 'Unknown Suite';
if (!suiteMap.has(suiteName)) {
suiteMap.set(suiteName, []);
}
const testCase = {
name: test.title || test.fullTitle,
status: test.pass ? 'passed' : test.pending ? 'skipped' : 'failed',
duration: test.duration,
};
if (test.err) {
testCase.failureMessage = test.err.message;
testCase.failureStack = test.err.stack;
}
suiteMap.get(suiteName).push(testCase);
if (testCase.status === 'passed')
passedTests++;
else if (testCase.status === 'failed')
failedTests++;
else if (testCase.status === 'skipped')
skippedTests++;
totalTests++;
}
for (const [name, tests] of suiteMap.entries()) {
suites.push({ name, tests });
}
// Return early if we successfully parsed JSON
return {
suites,
totalTests,
passedTests,
failedTests,
skippedTests,
};
}
}
}
catch (error) {
// JSON parsing failed, fall through to text parsing
}
// Fallback to text parsing (if no JSON or JSON parsing failed)
const lines = stdout.split('\n');
for (const line of lines) {
if (line.includes('passing')) {
const match = line.match(/(\d+) passing/);
if (match)
passedTests = parseInt(match[1]);
}
else if (line.includes('failing')) {
const match = line.match(/(\d+) failing/);
if (match)
failedTests = parseInt(match[1]);
}
else if (line.includes('pending')) {
const match = line.match(/(\d+) pending/);
if (match)
skippedTests = parseInt(match[1]);
}
}
totalTests = passedTests + failedTests + skippedTests;
return {
suites,
totalTests,
passedTests,
failedTests,
skippedTests,
};
}
/**
* Parse Vitest output
*/
function parseVitestOutput(stdout, stderr) {
const suites = [];
let totalTests = 0;
let passedTests = 0;
let failedTests = 0;
let skippedTests = 0;
try {
// Vitest can output JSON with --reporter=json
const jsonMatch = stdout.match(/\{[\s\S]*"testResults"[\s\S]*\}/);
if (jsonMatch) {
const result = JSON.parse(jsonMatch[0]);
if (result.testResults) {
for (const testResult of result.testResults) {
const tests = [];
if (testResult.assertionResults) {
for (const assertion of testResult.assertionResults) {
const test = {
name: assertion.fullName || assertion.title,
status: assertion.status === 'passed'
? 'passed'
: assertion.status === 'skipped'
? 'skipped'
: 'failed',
duration: assertion.duration,
};
if (assertion.status === 'failed' && assertion.failureMessages) {
test.failureMessage = assertion.failureMessages.join('\n');
test.failureStack = assertion.failureMessages.join('\n');
}
tests.push(test);
if (test.status === 'passed')
passedTests++;
else if (test.status === 'failed')
failedTests++;
else if (test.status === 'skipped')
skippedTests++;
totalTests++;
}
}
suites.push({
name: testResult.name || 'Unknown Suite',
tests,
});
}
}
}
}
catch (error) {
// Fallback to text parsing
const lines = stdout.split('\n');
for (const line of lines) {
if (line.includes('Test Files')) {
const match = line.match(/(\d+) passed/);
if (match)
passedTests = parseInt(match[1]);
}
else if (line.includes('Tests')) {
const match = line.match(/(\d+) passed/);
if (match)
passedTests = parseInt(match[1]);
const failMatch = line.match(/(\d+) failed/);
if (failMatch)
failedTests = parseInt(failMatch[1]);
}
}
totalTests = passedTests + failedTests + skippedTests;
}
return {
suites,
totalTests,
passedTests,
failedTests,
skippedTests,
};
}
/**
* Execute tests with a test framework
* Spawns the test runner with inspector attached if requested
* Captures stdout/stderr and parses test results
* Requirements: 6.1, 6.2, 6.3, 6.4, 6.5
*/
async function executeTests(config) {
const { framework, testFile, args = [], cwd, timeout = 30000, attachInspector = false, } = config;
return new Promise((resolve, reject) => {
let command;
let commandArgs = [];
// Build command based on framework
switch (framework) {
case 'jest':
command = 'npx';
commandArgs = ['jest'];
if (testFile)
commandArgs.push(testFile);
// Add --json for structured output
if (!args.includes('--json')) {
commandArgs.push('--json');
}
commandArgs.push(...args);
break;
case 'mocha':
command = 'npx';
commandArgs = ['mocha'];
if (testFile)
commandArgs.push(testFile);
// Add --reporter json for structured output
if (!args.some((arg) => arg.includes('--reporter'))) {
commandArgs.push('--reporter', 'json');
}
commandArgs.push(...args);
break;
case 'vitest':
command = 'npx';
commandArgs = ['vitest'];
if (testFile)
commandArgs.push(testFile);
// Add --reporter=json for structured output
if (!args.some((arg) => arg.includes('--reporter'))) {
commandArgs.push('--reporter=json');
}
// Add --run to prevent watch mode
if (!args.includes('--run')) {
commandArgs.push('--run');
}
commandArgs.push(...args);
break;
default:
reject(new Error(`Unsupported test framework: ${framework}`));
return;
}
// Add inspector flags if requested
if (attachInspector) {
commandArgs.unshift('--inspect-brk=0', '--enable-source-maps');
}
const child = (0, child_process_1.spawn)(command, commandArgs, {
cwd,
stdio: ['ignore', 'pipe', 'pipe'],
env: { ...process.env, NODE_OPTIONS: '--enable-source-maps' },
});
let stdout = '';
let stderr = '';
let wsUrl;
const startTime = Date.now();
// Capture stdout
child.stdout?.on('data', (data) => {
const output = data.toString();
stdout += output;
});
// Capture stderr and look for inspector URL
child.stderr?.on('data', (data) => {
const output = data.toString();
stderr += output;
// Look for inspector WebSocket URL
if (attachInspector && !wsUrl) {
const match = output.match(/ws:\/\/127\.0\.0\.1:\d+\/[a-f0-9-]+/);
if (match) {
wsUrl = match[0];
}
}
});
// Set timeout
const timeoutHandle = setTimeout(() => {
child.kill();
reject(new Error(`Test execution timed out after ${timeout}ms`));
}, timeout);
// Handle process exit
child.on('exit', (code) => {
clearTimeout(timeoutHandle);
const duration = Date.now() - startTime;
// Parse output based on framework
let parsed;
switch (framework) {
case 'jest':
parsed = parseJestOutput(stdout, stderr);
break;
case 'mocha':
parsed = parseMochaOutput(stdout, stderr);
break;
case 'vitest':
parsed = parseVitestOutput(stdout, stderr);
break;
default:
parsed = {};
}
const result = {
framework,
success: code === 0,
exitCode: code,
suites: parsed.suites || [],
stdout,
stderr,
totalTests: parsed.totalTests || 0,
passedTests: parsed.passedTests || 0,
failedTests: parsed.failedTests || 0,
skippedTests: parsed.skippedTests || 0,
duration,
wsUrl,
};
resolve(result);
});
// Handle spawn errors
child.on('error', (error) => {
clearTimeout(timeoutHandle);
reject(error);
});
});
}
//# sourceMappingURL=test-runner.js.map