test-listener-bug.jsโข6.4 kB
#!/usr/bin/env node
/**
 * Test to verify that read_process_output doesn't break future calls
 * by removing TerminalManager's listeners with removeAllListeners
 *
 * Expected behavior:
 * 1. Start Node.js REPL
 * 2. Send command with interact_with_process
 * 3. Call read_process_output - should work
 * 4. Send another command with interact_with_process
 * 5. Call read_process_output again - should STILL work (this would fail with removeAllListeners bug)
 */
import { spawn } from 'child_process';
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
async function runTest() {
  console.log('๐งช Testing listener cleanup bug...\n');
  // Start the MCP server
  const serverPath = path.join(__dirname, 'dist', 'index.js');
  const server = spawn('node', [serverPath, '--no-onboarding'], {
    stdio: ['pipe', 'pipe', 'pipe']
  });
  let responseBuffer = '';
  let requestId = 1;
  server.stdout.on('data', (data) => {
    responseBuffer += data.toString();
  });
  server.stderr.on('data', (data) => {
    console.error('Server stderr:', data.toString());
  });
  function sendRequest(method, params) {
    const request = {
      jsonrpc: '2.0',
      id: requestId++,
      method,
      params
    };
    server.stdin.write(JSON.stringify(request) + '\n');
  }
  function waitForResponse(expectedId, timeout = 5000) {
    return new Promise((resolve, reject) => {
      const startTime = Date.now();
      const checkInterval = setInterval(() => {
        const lines = responseBuffer.split('\n');
        for (const line of lines) {
          if (!line.trim()) continue;
          try {
            const response = JSON.parse(line);
            if (response.id === expectedId) {
              clearInterval(checkInterval);
              responseBuffer = responseBuffer.replace(line, '');
              resolve(response);
              return;
            }
          } catch (e) {
            // Not valid JSON yet, keep waiting
          }
        }
        if (Date.now() - startTime > timeout) {
          clearInterval(checkInterval);
          reject(new Error(`Timeout waiting for response ${expectedId}`));
        }
      }, 50);
    });
  }
  async function callTool(name, args) {
    const id = requestId;
    sendRequest('tools/call', { name, arguments: args });
    const response = await waitForResponse(id);
    if (response.error) {
      throw new Error(`Tool error: ${JSON.stringify(response.error)}`);
    }
    return response.result;
  }
  try {
    // Initialize
    sendRequest('initialize', { protocolVersion: '2024-11-05', capabilities: {} });
    await waitForResponse(requestId - 1);
    console.log('โ
 Server initialized\n');
    // Step 1: Start Node.js REPL
    console.log('๐ Step 1: Starting Node.js REPL...');
    const startResult = await callTool('start_process', {
      command: 'node -i',
      timeout_ms: 5000
    });
    const pidMatch = startResult.content[0].text.match(/PID (\d+)/);
    const pid = parseInt(pidMatch[1]);
    console.log(`โ
 Started process with PID: ${pid}\n`);
    // Step 2: Send first command
    console.log('๐ Step 2: Sending first command (1 + 1)...');
    const interact1 = await callTool('interact_with_process', {
      pid,
      input: '1 + 1',
      timeout_ms: 3000
    });
    console.log('โ
 First interact succeeded');
    console.log(`   Output snippet: ${interact1.content[0].text.substring(0, 50)}...\n`);
    // Step 3: First read_process_output (this might break listeners with removeAllListeners)
    console.log('๐ Step 3: First read_process_output...');
    const read1 = await callTool('read_process_output', {
      pid,
      timeout_ms: 1000
    });
    console.log('โ
 First read_process_output succeeded');
    console.log(`   Output: ${read1.content[0].text.substring(0, 50)}...\n`);
    // Step 4: Send second command WITHOUT waiting (so output stays in buffer)
    console.log('๐ Step 4: Sending second command (2 + 2) WITHOUT waiting...');
    const interact2 = await callTool('interact_with_process', {
      pid,
      input: '2 + 2',
      timeout_ms: 3000,
      wait_for_prompt: false  // Don't wait, let read_process_output get it
    });
    console.log('โ
 Second interact sent (not waiting for output)\n');
    // Wait a bit for output to arrive
    await new Promise(resolve => setTimeout(resolve, 500));
    // Step 5: Second read_process_output (THIS WILL FAIL if listeners were removed)
    console.log('๐ Step 5: Second read_process_output (critical test)...');
    const read2 = await callTool('read_process_output', {
      pid,
      timeout_ms: 2000
    });
    const outputText = read2.content[0].text;
    const hasOutput = !outputText.includes('No new output') &&
                      !outputText.includes('Timeout reached');
    if (!hasOutput) {
      console.error('โ BUG DETECTED: Second read_process_output returned no output!');
      console.error('   This means TerminalManager listeners were removed by removeAllListeners');
      console.error(`   Full output: ${outputText}`);
      process.exit(1);
    }
    // Validate output contains expected result from "2 + 2"
    if (!outputText.includes('4')) {
      console.error('โ BUG DETECTED: Second read_process_output has corrupt output!');
      console.error(`   Expected result "4" from "2 + 2" but got: ${outputText}`);
      process.exit(1);
    }
    // Validate output contains REPL prompt (proves detection is working)
    if (!outputText.includes('>')) {
      console.error('โ BUG DETECTED: Second read_process_output missing REPL prompt!');
      console.error(`   Expected ">" prompt but got: ${outputText}`);
      process.exit(1);
    }
    console.log('โ
 Second read_process_output succeeded!');
    console.log(`   โ Contains expected result: "4"`);
    console.log(`   โ Contains REPL prompt: ">"`);
    console.log(`   Full output: ${outputText.substring(0, 100)}...\n`);
    // Cleanup
    await callTool('force_terminate', { pid });
    console.log('โ
 Process terminated\n');
    console.log('๐ All tests passed! Listener cleanup is working correctly.');
    process.exit(0);
  } catch (error) {
    console.error('โ Test failed:', error.message);
    process.exit(1);
  } finally {
    server.kill();
  }
}
runTest();