@debugmcp/mcp-debugger
Version:
Run-time step-through debugging for LLM agents.
387 lines • 14.6 kB
JavaScript
/**
* 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