UNPKG

@debugmcp/mcp-debugger

Version:

Run-time step-through debugging for LLM agents.

387 lines 14.6 kB
/** * Mock Debug Adapter implementation for testing * * Provides a fully functional debug adapter that simulates debugging * without requiring external dependencies. * * @since 2.0.0 */ import { EventEmitter } from 'events'; import * as path from 'path'; import { AdapterState, DebugFeature, AdapterError, AdapterErrorCode } from '../debug-adapter-interface.js'; import { DebugLanguage } from '../../session/models.js'; /** * Mock error scenarios */ export var MockErrorScenario; (function (MockErrorScenario) { MockErrorScenario["NONE"] = "none"; MockErrorScenario["EXECUTABLE_NOT_FOUND"] = "executable_not_found"; MockErrorScenario["ADAPTER_CRASH"] = "adapter_crash"; MockErrorScenario["CONNECTION_TIMEOUT"] = "connection_timeout"; MockErrorScenario["INVALID_BREAKPOINT"] = "invalid_breakpoint"; MockErrorScenario["SCRIPT_ERROR"] = "script_error"; MockErrorScenario["OUT_OF_MEMORY"] = "out_of_memory"; })(MockErrorScenario || (MockErrorScenario = {})); /** * Valid state transitions * Made more permissive to match real adapter behavior (e.g., Python adapter) * Real adapters don't have strict state validation, so the mock shouldn't either */ const VALID_TRANSITIONS = { [AdapterState.UNINITIALIZED]: [ AdapterState.INITIALIZING, AdapterState.READY, // Allow direct ready AdapterState.CONNECTED, // Allow direct connection AdapterState.DEBUGGING, // Allow direct debugging (matches real adapter behavior) AdapterState.ERROR ], [AdapterState.INITIALIZING]: [ AdapterState.READY, AdapterState.CONNECTED, // Allow direct connection during init AdapterState.ERROR, AdapterState.UNINITIALIZED // Allow reset ], [AdapterState.READY]: [ AdapterState.CONNECTED, AdapterState.DEBUGGING, // Allow direct debugging from ready AdapterState.DISCONNECTED, // Allow disconnection AdapterState.ERROR, AdapterState.UNINITIALIZED // Allow reset ], [AdapterState.CONNECTED]: [ AdapterState.DEBUGGING, AdapterState.CONNECTED, // Allow staying connected (idempotent) AdapterState.DISCONNECTED, AdapterState.READY, // Allow going back to ready AdapterState.ERROR ], [AdapterState.DEBUGGING]: [ AdapterState.DEBUGGING, // Allow staying in debugging (idempotent) AdapterState.CONNECTED, // Allow going back to connected AdapterState.DISCONNECTED, AdapterState.READY, // Allow going back to ready AdapterState.ERROR ], [AdapterState.DISCONNECTED]: [ AdapterState.READY, AdapterState.CONNECTED, // Allow reconnection AdapterState.UNINITIALIZED, // Allow full reset AdapterState.ERROR ], [AdapterState.ERROR]: [ AdapterState.UNINITIALIZED, AdapterState.READY, // Allow recovery to ready AdapterState.DISCONNECTED // Allow recovery to disconnected ] }; /** * Mock debug adapter implementation */ export class MockDebugAdapter extends EventEmitter { language = DebugLanguage.MOCK; name = 'Mock Debug Adapter'; state = AdapterState.UNINITIALIZED; config; dependencies; // State currentThreadId = null; connected = false; // Error simulation errorScenario = MockErrorScenario.NONE; constructor(dependencies, config = {}) { super(); this.dependencies = dependencies; this.config = { defaultDelay: config.defaultDelay ?? 0, connectionDelay: config.connectionDelay ?? 50, stepDelay: config.stepDelay ?? 5, supportedFeatures: config.supportedFeatures ?? [ DebugFeature.CONDITIONAL_BREAKPOINTS, DebugFeature.FUNCTION_BREAKPOINTS, DebugFeature.VARIABLE_PAGING, DebugFeature.SET_VARIABLE ], maxVariableDepth: config.maxVariableDepth ?? 10, maxArrayLength: config.maxArrayLength ?? 100, errorProbability: config.errorProbability ?? 0, errorScenarios: config.errorScenarios ?? [], cpuIntensive: config.cpuIntensive ?? false, memoryIntensive: config.memoryIntensive ?? false }; } // ===== Lifecycle Management ===== async initialize() { this.transitionTo(AdapterState.INITIALIZING); try { // Validate environment const validation = await this.validateEnvironment(); if (!validation.valid) { this.transitionTo(AdapterState.ERROR); throw new AdapterError(validation.errors[0]?.message || 'Validation failed', AdapterErrorCode.ENVIRONMENT_INVALID); } this.transitionTo(AdapterState.READY); this.emit('initialized'); } catch (error) { this.transitionTo(AdapterState.ERROR); throw error; } } async dispose() { this.currentThreadId = null; this.connected = false; this.state = AdapterState.UNINITIALIZED; this.emit('disposed'); } // ===== State Management ===== getState() { return this.state; } isReady() { return this.state === AdapterState.READY || this.state === AdapterState.CONNECTED || this.state === AdapterState.DEBUGGING; } getCurrentThreadId() { return this.currentThreadId; } transitionTo(newState) { const oldState = this.state; const validTransitions = VALID_TRANSITIONS[oldState]; if (!validTransitions?.includes(newState)) { throw new AdapterError(`Invalid state transition: ${oldState} → ${newState}`, AdapterErrorCode.UNKNOWN_ERROR); } this.state = newState; this.emit('stateChanged', oldState, newState); } // ===== Environment Validation ===== async validateEnvironment() { if (this.errorScenario === MockErrorScenario.EXECUTABLE_NOT_FOUND) { return { valid: false, errors: [{ code: 'MOCK_NOT_FOUND', message: 'Mock executable not found', recoverable: false }], warnings: [] }; } // Mock adapter always validates successfully return { valid: true, errors: [], warnings: [] }; } getRequiredDependencies() { // Mock adapter has no external dependencies return []; } // ===== Executable Management ===== async resolveExecutablePath(preferredPath) { if (preferredPath) { return preferredPath; } // Use node as the mock executable return process.execPath; } getDefaultExecutableName() { return 'node'; } getExecutableSearchPaths() { return process.env.PATH?.split(path.delimiter) || []; } // ===== Adapter Configuration ===== buildAdapterCommand(config) { // Get the directory of this module // When compiled, this will be in dist/adapters/mock/ let mockAdapterPath; try { // Try to use import.meta.url if available const currentFileUrl = new URL(import.meta.url); let currentDir = path.dirname(currentFileUrl.pathname); // In Windows, remove the leading slash from the pathname if (process.platform === 'win32' && currentDir.startsWith('/')) { currentDir = currentDir.substring(1); } // Decode URL encoding (e.g., %20 for spaces) currentDir = decodeURIComponent(currentDir); mockAdapterPath = path.join(currentDir, 'mock-adapter-process.js'); } catch { // Fallback: assume we're running from the project root // The compiled files are in dist/adapters/mock/ const projectRoot = path.resolve(process.cwd()); mockAdapterPath = path.join(projectRoot, 'dist', 'adapters', 'mock', 'mock-adapter-process.js'); this.dependencies.logger?.debug(`[MockDebugAdapter] Using fallback path resolution: ${mockAdapterPath}`); } return { command: process.execPath, args: [ mockAdapterPath, '--port', config.adapterPort.toString(), '--host', config.adapterHost, '--session', config.sessionId ], env: { ...process.env, MOCK_ADAPTER_LOG: config.logDir } }; } getAdapterModuleName() { return 'mock-adapter'; } getAdapterInstallCommand() { return 'echo "Mock adapter is built-in"'; } // ===== Debug Configuration ===== transformLaunchConfig(config) { return { ...config, type: 'mock', request: 'launch', name: 'Mock Debug' }; } getDefaultLaunchConfig() { return { stopOnEntry: false, justMyCode: true, env: {}, cwd: process.cwd() }; } // ===== DAP Protocol Operations ===== async sendDapRequest(command, args) { // This will be handled by ProxyManager // Mock adapter just validates the request is appropriate this.dependencies.logger?.debug(`[MockDebugAdapter] DAP request: ${command}`, args); // ProxyManager will handle actual communication return {}; } handleDapEvent(event) { // Update thread ID on stopped events if (event.event === 'stopped' && event.body?.threadId) { this.currentThreadId = event.body.threadId; this.transitionTo(AdapterState.DEBUGGING); } else if (event.event === 'continued') { this.transitionTo(AdapterState.DEBUGGING); } else if (event.event === 'terminated' || event.event === 'exited') { this.currentThreadId = null; if (this.connected) { this.transitionTo(AdapterState.CONNECTED); } } this.emit(event.event, event.body); } handleDapResponse(_response) { // Mock adapter doesn't need special response handling void _response; // Explicitly ignore } // ===== Connection Management ===== async connect(host, port) { // Simulate connection delay if configured if (this.config.connectionDelay > 0) { await new Promise(resolve => setTimeout(resolve, this.config.connectionDelay)); } if (this.errorScenario === MockErrorScenario.CONNECTION_TIMEOUT) { throw new AdapterError('Connection timeout', AdapterErrorCode.CONNECTION_TIMEOUT, true); } // Connection is handled by ProxyManager // Store connection info for debugging purposes this.dependencies.logger?.debug(`[MockDebugAdapter] Connect request to ${host}:${port}`); this.connected = true; this.transitionTo(AdapterState.CONNECTED); this.emit('connected'); } async disconnect() { this.connected = false; this.currentThreadId = null; this.transitionTo(AdapterState.DISCONNECTED); this.emit('disconnected'); } isConnected() { return this.connected; } // ===== Error Handling ===== getInstallationInstructions() { return 'The Mock Debug Adapter is built-in and requires no installation.'; } getMissingExecutableError() { return 'Mock executable not found. This should not happen with the mock adapter.'; } translateErrorMessage(error) { if (error.message.includes('ENOENT')) { return 'Mock file not found: ' + error.message; } return error.message; } // ===== Feature Support ===== supportsFeature(feature) { return this.config.supportedFeatures?.includes(feature) || false; } getFeatureRequirements(feature) { const requirements = []; if (feature === DebugFeature.CONDITIONAL_BREAKPOINTS) { requirements.push({ type: 'version', description: 'Mock adapter version 1.0+', required: true }); } return requirements; } getCapabilities() { return { supportsConfigurationDoneRequest: true, supportsFunctionBreakpoints: this.supportsFeature(DebugFeature.FUNCTION_BREAKPOINTS), supportsConditionalBreakpoints: this.supportsFeature(DebugFeature.CONDITIONAL_BREAKPOINTS), supportsHitConditionalBreakpoints: false, supportsEvaluateForHovers: this.supportsFeature(DebugFeature.EVALUATE_FOR_HOVERS), exceptionBreakpointFilters: [], supportsStepBack: false, supportsSetVariable: this.supportsFeature(DebugFeature.SET_VARIABLE), supportsRestartFrame: false, supportsGotoTargetsRequest: false, supportsStepInTargetsRequest: false, supportsCompletionsRequest: false, supportsModulesRequest: false, supportsRestartRequest: false, supportsExceptionOptions: false, supportsValueFormattingOptions: false, supportsExceptionInfoRequest: false, supportTerminateDebuggee: true, supportSuspendDebuggee: false, supportsDelayedStackTraceLoading: false, supportsLoadedSourcesRequest: false, supportsLogPoints: this.supportsFeature(DebugFeature.LOG_POINTS), supportsTerminateThreadsRequest: false, supportsSetExpression: false, supportsTerminateRequest: true, supportsDataBreakpoints: false, supportsReadMemoryRequest: false, supportsWriteMemoryRequest: false, supportsDisassembleRequest: false, supportsCancelRequest: false, supportsBreakpointLocationsRequest: false, supportsClipboardContext: false, supportsSteppingGranularity: false, supportsInstructionBreakpoints: false, supportsExceptionFilterOptions: false, supportsSingleThreadExecutionRequests: false }; } // ===== Mock-specific methods ===== /** * Set error scenario for testing */ setErrorScenario(scenario) { this.errorScenario = scenario; } } //# sourceMappingURL=mock-debug-adapter.js.map