UNPKG

@debugmcp/mcp-debugger

Version:

Run-time step-through debugging for LLM agents.

416 lines (354 loc) 15.3 kB
/** * Error Scenarios E2E Tests * Tests various error conditions and edge cases */ import { describe, it, expect, afterEach, beforeEach } from 'vitest'; import * as path from 'path'; import { Client } from '@modelcontextprotocol/sdk/client/index.js'; import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'; import { parseSdkToolResult, callToolSafely } from './smoke-test-utils.js'; import { waitForSessionState, waitForAnyState, EventRecorder, PYTHON_TIMEOUT } from './test-event-utils.js'; let mcpClient: Client | null = null; let debugSessionIds: string[] = []; describe('Error Scenarios E2E', () => { beforeEach(async () => { console.log('[E2E Error Scenarios] Setting up MCP client...'); debugSessionIds = []; mcpClient = new Client({ name: "e2e-error-scenarios-test", version: "0.1.0" }); const transport = new StdioClientTransport({ command: 'node', args: [path.join(process.cwd(), 'dist', 'index.js'), 'stdio'], }); transport.onerror = (error) => { console.error('[E2E Error Scenarios] Transport error:', error); }; await mcpClient.connect(transport); console.log('[E2E Error Scenarios] MCP client connected.'); }); afterEach(async () => { // Clean up any remaining sessions for (const sessionId of debugSessionIds) { try { await mcpClient!.callTool({ name: 'close_debug_session', arguments: { sessionId } }); } catch { // Expected for invalid sessions } } // Close MCP client if (mcpClient) { await mcpClient.close(); mcpClient = null; } }); describe('Language Support Errors', () => { it('should reject unsupported language', async () => { console.log('\n[E2E Error Scenarios] Testing unsupported language...'); const result = await callToolSafely( mcpClient!, 'create_debug_session', { language: 'ruby', name: 'Ruby Test' } ); expect(result.success).toBe(false); expect(result.message).toContain('not supported'); console.log('[E2E Error Scenarios] Correctly rejected unsupported language'); }); it('should reject javascript (now that it is replaced by mock)', async () => { console.log('\n[E2E Error Scenarios] Testing javascript language (should be rejected)...'); const result = await callToolSafely( mcpClient!, 'create_debug_session', { language: 'javascript', name: 'JavaScript Test' } ); expect(result.success).toBe(false); expect(result.message).toContain('not supported'); console.log('[E2E Error Scenarios] Correctly rejected javascript language'); }); }); describe('Session Management Errors', () => { it('should handle invalid session ID', async () => { console.log('\n[E2E Error Scenarios] Testing invalid session ID...'); const invalidSessionId = 'invalid-session-12345'; // Try to set breakpoint with invalid session const breakpointResult = await callToolSafely( mcpClient!, 'set_breakpoint', { sessionId: invalidSessionId, file: path.join(process.cwd(), 'tests/fixtures/debug-scripts/simple.py'), line: 4 } ); expect(breakpointResult.success).toBe(false); console.log('[E2E Error Scenarios] Correctly rejected invalid session ID for breakpoint'); // Try to start debugging with invalid session const debugResult = await callToolSafely( mcpClient!, 'start_debugging', { sessionId: invalidSessionId, scriptPath: path.join(process.cwd(), 'tests/fixtures/debug-scripts/simple.py') } ); expect(debugResult.success).toBe(false); console.log('[E2E Error Scenarios] Correctly rejected invalid session ID for debugging'); }); it('should handle operations on closed session', async () => { console.log('\n[E2E Error Scenarios] Testing operations on closed session...'); // Create and immediately close a session const createResponse = await mcpClient!.callTool({ name: 'create_debug_session', arguments: { language: 'mock', name: 'Closed Session Test' } }); const createResult = parseSdkToolResult(createResponse); expect(createResult.success).toBe(true); const sessionId = createResult.sessionId!; const closeResponse = await mcpClient!.callTool({ name: 'close_debug_session', arguments: { sessionId } }); const closeResult = parseSdkToolResult(closeResponse); expect(closeResult.success).toBe(true); // Verify session is actually closed const eventRecorder = new EventRecorder(); const isClosed = await waitForSessionState(mcpClient!, sessionId, 'stopped', { timeout: 1000, eventRecorder }); expect(isClosed).toBe(true); // Now try to use the closed session const breakpointResult = await callToolSafely( mcpClient!, 'set_breakpoint', { sessionId, file: path.join(process.cwd(), 'tests/fixtures/debug-scripts/simple-mock.js'), line: 4 } ); expect(breakpointResult.success).toBe(false); console.log('[E2E Error Scenarios] Correctly rejected operation on closed session'); }); }); describe('File and Path Errors', () => { it('should handle non-existent file for breakpoint', async () => { console.log('\n[E2E Error Scenarios] Testing non-existent file for breakpoint...'); // Create a valid session const createResponse = await mcpClient!.callTool({ name: 'create_debug_session', arguments: { language: 'mock', name: 'File Error Test' } }); const createResult = parseSdkToolResult(createResponse); const sessionId = createResult.sessionId!; debugSessionIds.push(sessionId); // Try to set breakpoint in non-existent file const breakpointResponse = await mcpClient!.callTool({ name: 'set_breakpoint', arguments: { sessionId, file: path.join(process.cwd(), 'tests/fixtures/debug-scripts/does-not-exist.py'), line: 4 } }); const breakpointResult = parseSdkToolResult(breakpointResponse); // Breakpoint might be set but not verified, or might fail entirely if (breakpointResult.success) { expect(breakpointResult.verified).toBe(false); } console.log('[E2E Error Scenarios] Handled non-existent file for breakpoint'); }); it('should handle invalid script path for debugging', async () => { console.log('\n[E2E Error Scenarios] Testing invalid script path...'); const eventRecorder = new EventRecorder(); // Create a valid session const createResponse = await mcpClient!.callTool({ name: 'create_debug_session', arguments: { language: 'python', name: 'Invalid Script Test' } }); const createResult = parseSdkToolResult(createResponse); const sessionId = createResult.sessionId!; debugSessionIds.push(sessionId); // Try to start debugging with non-existent script const debugResponse = await mcpClient!.callTool({ name: 'start_debugging', arguments: { sessionId, scriptPath: path.join(process.cwd(), 'tests/fixtures/debug-scripts/does-not-exist.py') } }); const debugResult = parseSdkToolResult(debugResponse); // With "hands-off" approach, the start_debugging call might succeed initially // but the session should transition to error state when debugpy can't find the file if (debugResult.success) { console.log('[E2E Error Scenarios] start_debugging returned success, waiting for error state...'); // Wait for the session to transition to error state OR stopped state (terminated) const result = await waitForAnyState(mcpClient!, sessionId, ['error', 'stopped'], { timeout: 20000, // Increased timeout for Python error reporting eventRecorder }); expect(result.success).toBe(true); console.log(`[E2E Error Scenarios] Session transitioned to '${result.state}' state due to invalid script path`); // Either error or stopped state is acceptable expect(['error', 'stopped']).toContain(result.state); } else { // If it failed immediately, that's also acceptable console.log('[E2E Error Scenarios] start_debugging immediately rejected invalid script path:', debugResult.message); expect(debugResult.message).toBeDefined(); } }, 60000); // Increase timeout further as Python operations can be slow }); describe('Debug Operation Errors', () => { it('should handle step operations when not paused', async () => { console.log('\n[E2E Error Scenarios] Testing step operations when not paused...'); // Create a session but don't start debugging const createResponse = await mcpClient!.callTool({ name: 'create_debug_session', arguments: { language: 'mock', name: 'Step Error Test' } }); const createResult = parseSdkToolResult(createResponse); const sessionId = createResult.sessionId!; debugSessionIds.push(sessionId); // Try to step over without being in debug mode const stepResult = await callToolSafely( mcpClient!, 'step_over', { sessionId } ); expect(stepResult.success).toBe(false); console.log('[E2E Error Scenarios] Correctly rejected step operation when not debugging'); }); it('should handle get_stack_trace when not paused', async () => { console.log('\n[E2E Error Scenarios] Testing get_stack_trace when not paused...'); // Create a session but don't start debugging const createResponse = await mcpClient!.callTool({ name: 'create_debug_session', arguments: { language: 'mock', name: 'Stack Trace Error Test' } }); const createResult = parseSdkToolResult(createResponse); const sessionId = createResult.sessionId!; debugSessionIds.push(sessionId); // Try to get stack trace without being in debug mode const stackResult = await callToolSafely( mcpClient!, 'get_stack_trace', { sessionId } ); expect(stackResult.success).toBe(false); console.log('[E2E Error Scenarios] Correctly rejected stack trace when not debugging'); }); }); describe('Invalid Parameters', () => { it('should handle invalid breakpoint line numbers', async () => { console.log('\n[E2E Error Scenarios] Testing invalid breakpoint line numbers...'); // Create a valid session const createResponse = await mcpClient!.callTool({ name: 'create_debug_session', arguments: { language: 'mock', name: 'Invalid Line Test' } }); const createResult = parseSdkToolResult(createResponse); const sessionId = createResult.sessionId!; debugSessionIds.push(sessionId); // Try negative line number const negativeLineResponse = await mcpClient!.callTool({ name: 'set_breakpoint', arguments: { sessionId, file: path.join(process.cwd(), 'tests/fixtures/debug-scripts/simple-mock.js'), line: -1 } }); const negativeLineResult = parseSdkToolResult(negativeLineResponse); // Depending on implementation, this might succeed but not be verified if (negativeLineResult.success) { expect(negativeLineResult.verified).toBe(false); } console.log('[E2E Error Scenarios] Handled negative line number'); // Try very large line number const largeLineResponse = await mcpClient!.callTool({ name: 'set_breakpoint', arguments: { sessionId, file: path.join(process.cwd(), 'tests/fixtures/debug-scripts/simple-mock.js'), line: 999999 } }); const largeLineResult = parseSdkToolResult(largeLineResponse); // Breakpoint might be set but not verified if (largeLineResult.success) { expect(largeLineResult.verified).toBe(false); } console.log('[E2E Error Scenarios] Handled very large line number'); }); it('should handle missing required parameters', async () => { console.log('\n[E2E Error Scenarios] Testing missing required parameters...'); // Try to create session without language try { await mcpClient!.callTool({ name: 'create_debug_session', arguments: { name: 'No Language Test' } }); // If we get here, the test should fail expect(true).toBe(false); } catch { // Expected to throw or return error console.log('[E2E Error Scenarios] Correctly rejected missing language parameter'); } }); }); describe('Session State Errors', () => { it('should handle operations when session is in wrong state', async () => { console.log('\n[E2E Error Scenarios] Testing operations in wrong state...'); const eventRecorder = new EventRecorder(); // Create a session and start debugging const createResponse = await mcpClient!.callTool({ name: 'create_debug_session', arguments: { language: 'python', name: 'State Error Test' } }); const createResult = parseSdkToolResult(createResponse); const sessionId = createResult.sessionId!; debugSessionIds.push(sessionId); const pythonFile = path.join(process.cwd(), 'tests/fixtures/debug-scripts/simple.py'); const debugResponse = await mcpClient!.callTool({ name: 'start_debugging', arguments: { sessionId, scriptPath: pythonFile, dapLaunchArgs: { stopOnEntry: true } } }); const debugResult = parseSdkToolResult(debugResponse); expect(debugResult.success).toBe(true); // Wait for session to be paused const isPaused = await waitForSessionState(mcpClient!, sessionId, 'paused', { timeout: PYTHON_TIMEOUT, eventRecorder }); expect(isPaused).toBe(true); // Now continue execution const continueResponse = await mcpClient!.callTool({ name: 'continue_execution', arguments: { sessionId } }); const continueResult = parseSdkToolResult(continueResponse); expect(continueResult.success).toBe(true); // Try to get variables while running (should fail) const variablesResult = await callToolSafely( mcpClient!, 'get_variables', { sessionId, scope: 1 } ); // Should either fail or return empty array if (variablesResult.success) { expect(variablesResult.variables).toEqual([]); } console.log('[E2E Error Scenarios] Correctly handled operation in wrong state'); }); }); });