UNPKG

@debugmcp/mcp-debugger

Version:

Run-time step-through debugging for LLM agents.

332 lines (295 loc) 12.2 kB
/** * Integration tests for proxy error handling * Tests the interaction between ProxyManager and ProxyWorker for error scenarios */ import { describe, it, expect, beforeEach, vi } from 'vitest'; import { ProxyManager } from '../../../src/proxy/proxy-manager.js'; import { ProxyConfig } from '../../../src/proxy/proxy-config.js'; import { createTestDependencies } from '../../test-utils/helpers/test-dependencies.js'; import type { IDebugAdapter } from '../../../src/adapters/debug-adapter-interface.js'; import { DebugLanguage } from '../../../src/session/models.js'; import type { Dependencies } from '../../test-utils/helpers/test-dependencies.js'; import type { IProxyProcessLauncher, IProxyProcess } from '../../../src/interfaces/process-interfaces.js'; import path from 'path'; import { fileURLToPath } from 'url'; describe('Proxy Error Handling Integration', () => { let dependencies: Dependencies; let proxyManager: ProxyManager; let mockAdapter: IDebugAdapter; let mockProxyLauncher: IProxyProcessLauncher; beforeEach(async () => { vi.clearAllMocks(); // Create test dependencies with real implementations where possible dependencies = await createTestDependencies(); // Create a minimal mock adapter mockAdapter = { language: DebugLanguage.PYTHON, initialize: vi.fn().mockResolvedValue(undefined), validateEnvironment: vi.fn().mockResolvedValue({ valid: true, errors: [] }), resolveExecutablePath: vi.fn().mockResolvedValue('/usr/bin/python3'), buildAdapterCommand: vi.fn().mockReturnValue({ command: '/usr/bin/python3', args: ['-m', 'debugpy.adapter', '--host', 'localhost', '--port', '5678'], env: {} }), dispose: vi.fn().mockResolvedValue(undefined), getState: vi.fn().mockReturnValue('initialized'), getDebuggerInfo: vi.fn().mockReturnValue({ version: '1.0.0', capabilities: [] }), translateErrorMessage: vi.fn().mockImplementation(err => err.message), on: vi.fn(), once: vi.fn(), emit: vi.fn(), removeListener: vi.fn() } as unknown as IDebugAdapter; // Create a mock proxy process launcher that implements IProxyProcessLauncher mockProxyLauncher = { launchProxy: vi.fn().mockImplementation((scriptPath: string, sessionId: string, env?: NodeJS.ProcessEnv) => { // Return a mock proxy process type EventHandler = (...args: any[]) => void; const eventHandlers: Record<string, EventHandler[]> = { exit: [], message: [], error: [] }; let processKilled = false; const mockProcess = { pid: 12345, get killed() { return processKilled; }, kill: vi.fn().mockImplementation(() => { processKilled = true; return true; }), sendCommand: vi.fn().mockImplementation((cmd: any) => { // Simulate worker behavior - exit on init with non-existent script if (cmd.cmd === 'init') { // Send error message first const errorHandler = eventHandlers.message?.[0]; if (errorHandler) { errorHandler({ type: 'error', message: `Script path not found: ${cmd.scriptPath}`, sessionId: cmd.sessionId }); } // Then exit the process setTimeout(() => { processKilled = true; // Mark process as killed const exitHandler = eventHandlers.exit?.[0]; if (exitHandler) { exitHandler(1, null); } }, 10); // Small delay to simulate async behavior } }), on: vi.fn().mockImplementation((event: string, handler: EventHandler) => { if (!eventHandlers[event]) eventHandlers[event] = []; eventHandlers[event].push(handler); }), once: vi.fn().mockImplementation((event: string, handler: EventHandler) => { if (!eventHandlers[event]) eventHandlers[event] = []; eventHandlers[event].push(handler); }), removeListener: vi.fn(), stderr: { on: vi.fn() } } as unknown as IProxyProcess; return mockProcess; }) }; // Create proxy manager with the mock adapter and proxy launcher proxyManager = new ProxyManager( mockAdapter, mockProxyLauncher, dependencies.fileSystem, dependencies.logger ); }); describe('Script Path Validation', () => { it('should fail quickly when script path does not exist', async () => { // Mock file system to simulate script not found vi.mocked(dependencies.fileSystem.pathExists) .mockResolvedValueOnce(true) // proxy bootstrap script exists .mockResolvedValueOnce(false); // target script does not exist const config: ProxyConfig = { sessionId: 'test-session-1', language: DebugLanguage.PYTHON, executablePath: '/usr/bin/python3', adapterHost: 'localhost', adapterPort: 5678, logDir: '/tmp/test-logs', scriptPath: '/nonexistent/script.py', scriptArgs: [], stopOnEntry: false, justMyCode: true, initialBreakpoints: [], dryRunSpawn: false }; // Start time measurement const startTime = Date.now(); // Attempt to start proxy - should fail await expect(proxyManager.start(config)).rejects.toThrow('Script path not found'); // End time measurement const endTime = Date.now(); const elapsedTime = endTime - startTime; // Verify it failed quickly (should be well under 1 second, not 30 seconds) expect(elapsedTime).toBeLessThan(1000); // The main goal of this test is to verify that the error fails quickly, // not to test the exact cleanup timing of the proxy manager }); it('should fail quickly with meaningful error message', async () => { // Mock file system to simulate script not found vi.mocked(dependencies.fileSystem.pathExists) .mockResolvedValueOnce(true) // proxy bootstrap script exists .mockResolvedValueOnce(false); // target script does not exist const scriptPath = '/home/user/my-script.py'; const config: ProxyConfig = { sessionId: 'test-session-2', language: DebugLanguage.PYTHON, executablePath: '/usr/bin/python3', adapterHost: 'localhost', adapterPort: 5678, logDir: '/tmp/test-logs', scriptPath, scriptArgs: [], stopOnEntry: false, justMyCode: true, initialBreakpoints: [], dryRunSpawn: false }; // Attempt to start proxy - should fail with specific error await expect(proxyManager.start(config)) .rejects.toThrow(`Script path not found: ${scriptPath}`); }); it('should handle relative script paths in container mode', async () => { // Temporarily set container environment const originalEnv = process.env.MCP_CONTAINER; process.env.MCP_CONTAINER = 'true'; try { // Mock file system to return false for the container path vi.mocked(dependencies.fileSystem.pathExists) .mockResolvedValueOnce(true) // proxy bootstrap script exists .mockResolvedValueOnce(false); // script doesn't exist at /workspace/relative/script.py const config: ProxyConfig = { sessionId: 'test-session-3', language: DebugLanguage.PYTHON, executablePath: '/usr/bin/python3', adapterHost: 'localhost', adapterPort: 5678, logDir: '/tmp/test-logs', scriptPath: 'relative/script.py', // Relative path scriptArgs: [], stopOnEntry: false, justMyCode: true, initialBreakpoints: [], dryRunSpawn: false }; // This should fail because the file doesn't exist await expect(proxyManager.start(config)) .rejects.toThrow('Script path not found: relative/script.py'); // Note: The actual path check happens in the worker process (which we're mocking), // not in the ProxyManager, so we can't verify the exact path that was checked. // The test verifies that the error message is propagated correctly. } finally { // Restore original environment if (originalEnv !== undefined) { process.env.MCP_CONTAINER = originalEnv; } else { delete process.env.MCP_CONTAINER; } } }); }); describe('Error Propagation', () => { it('should properly propagate errors from worker to manager', async () => { // Mock file system to simulate other initialization errors vi.mocked(dependencies.fileSystem.pathExists) .mockResolvedValueOnce(true) // proxy bootstrap script exists .mockResolvedValueOnce(true); // script exists // We need to recreate the proxy manager with a failing launcher const failingProxyLauncher: IProxyProcessLauncher = { launchProxy: vi.fn().mockImplementation(() => { throw new Error('Failed to spawn proxy process'); }) }; proxyManager = new ProxyManager( mockAdapter, failingProxyLauncher, dependencies.fileSystem, dependencies.logger ); const config: ProxyConfig = { sessionId: 'test-session-4', language: DebugLanguage.PYTHON, executablePath: '/usr/bin/python3', adapterHost: 'localhost', adapterPort: 5678, logDir: '/tmp/test-logs', scriptPath: '/valid/script.py', scriptArgs: [], stopOnEntry: false, justMyCode: true, initialBreakpoints: [], dryRunSpawn: false }; // Should fail with the spawn error await expect(proxyManager.start(config)) .rejects.toThrow('Failed to spawn proxy process'); }); }); describe('Timing Behavior', () => { it('should not hang for 30 seconds on initialization errors', async () => { // Test various initialization errors to ensure none cause hanging const errorScenarios = [ { name: 'script not found', setup: () => { vi.mocked(dependencies.fileSystem.pathExists) .mockResolvedValueOnce(true) // proxy bootstrap exists .mockResolvedValueOnce(false); // script doesn't exist }, expectedError: 'Script path not found' }, { name: 'proxy bootstrap not found', setup: () => { vi.mocked(dependencies.fileSystem.pathExists) .mockResolvedValue(false); // Nothing exists }, expectedError: 'Bootstrap worker script not found' } ]; for (const scenario of errorScenarios) { vi.clearAllMocks(); scenario.setup(); // Create a fresh proxy manager for each scenario proxyManager = new ProxyManager( mockAdapter, mockProxyLauncher, dependencies.fileSystem, dependencies.logger ); const config: ProxyConfig = { sessionId: `test-timing-${scenario.name}`, language: DebugLanguage.PYTHON, executablePath: '/usr/bin/python3', adapterHost: 'localhost', adapterPort: 5678, logDir: '/tmp/test-logs', scriptPath: '/some/script.py', scriptArgs: [], stopOnEntry: false, justMyCode: true, initialBreakpoints: [], dryRunSpawn: false }; const startTime = Date.now(); await expect(proxyManager.start(config)) .rejects.toThrow(scenario.expectedError); const elapsedTime = Date.now() - startTime; // Should fail in under 2 seconds, not 30 expect(elapsedTime).toBeLessThan(2000); } }); }); });