@debugmcp/mcp-debugger
Version:
Run-time step-through debugging for LLM agents.
503 lines (432 loc) • 15.4 kB
text/typescript
/**
* 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 { DebugProtocol } from '@vscode/debugprotocol';
import * as path from 'path';
import {
IDebugAdapter,
AdapterState,
ValidationResult,
DependencyInfo,
AdapterCommand,
AdapterConfig,
GenericLaunchConfig,
LanguageSpecificLaunchConfig,
DebugFeature,
FeatureRequirement,
AdapterCapabilities,
AdapterError,
AdapterErrorCode,
AdapterEvents
} from '../debug-adapter-interface.js';
import { DebugLanguage } from '../../session/models.js';
import { AdapterDependencies } from '../adapter-registry-interface.js';
/**
* Mock adapter configuration
*/
export interface MockAdapterConfig {
// Timing configuration
defaultDelay?: number; // Base delay for operations (ms)
connectionDelay?: number; // Delay for connect operation
stepDelay?: number; // Delay for step operations
// Behavior configuration
supportedFeatures?: DebugFeature[]; // Which DAP features to support
maxVariableDepth?: number; // How deep variable trees can go
maxArrayLength?: number; // Maximum array size to simulate
// Error simulation
errorProbability?: number; // Random error chance (0-1)
errorScenarios?: MockErrorScenario[]; // Enabled error scenarios
// Performance simulation
cpuIntensive?: boolean; // Simulate CPU-intensive operations
memoryIntensive?: boolean; // Simulate memory pressure
}
/**
* Mock error scenarios
*/
export enum MockErrorScenario {
NONE = 'none',
EXECUTABLE_NOT_FOUND = 'executable_not_found',
ADAPTER_CRASH = 'adapter_crash',
CONNECTION_TIMEOUT = 'connection_timeout',
INVALID_BREAKPOINT = 'invalid_breakpoint',
SCRIPT_ERROR = 'script_error',
OUT_OF_MEMORY = 'out_of_memory'
}
/**
* 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: { [key in AdapterState]?: AdapterState[] } = {
[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 implements IDebugAdapter {
readonly language = DebugLanguage.MOCK;
readonly name = 'Mock Debug Adapter';
private state: AdapterState = AdapterState.UNINITIALIZED;
private config: Required<MockAdapterConfig>;
private dependencies: AdapterDependencies;
// State
private currentThreadId: number | null = null;
private connected = false;
// Error simulation
private errorScenario: MockErrorScenario = MockErrorScenario.NONE;
constructor(dependencies: AdapterDependencies, config: MockAdapterConfig = {}) {
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(): Promise<void> {
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(): Promise<void> {
this.currentThreadId = null;
this.connected = false;
this.state = AdapterState.UNINITIALIZED;
this.emit('disposed');
}
// ===== State Management =====
getState(): AdapterState {
return this.state;
}
isReady(): boolean {
return this.state === AdapterState.READY ||
this.state === AdapterState.CONNECTED ||
this.state === AdapterState.DEBUGGING;
}
getCurrentThreadId(): number | null {
return this.currentThreadId;
}
private transitionTo(newState: AdapterState): void {
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(): Promise<ValidationResult> {
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(): DependencyInfo[] {
// Mock adapter has no external dependencies
return [];
}
// ===== Executable Management =====
async resolveExecutablePath(preferredPath?: string): Promise<string> {
if (preferredPath) {
return preferredPath;
}
// Use node as the mock executable
return process.execPath;
}
getDefaultExecutableName(): string {
return 'node';
}
getExecutableSearchPaths(): string[] {
return process.env.PATH?.split(path.delimiter) || [];
}
// ===== Adapter Configuration =====
buildAdapterCommand(config: AdapterConfig): AdapterCommand {
// Get the directory of this module
// When compiled, this will be in dist/adapters/mock/
let mockAdapterPath: string;
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(): string {
return 'mock-adapter';
}
getAdapterInstallCommand(): string {
return 'echo "Mock adapter is built-in"';
}
// ===== Debug Configuration =====
transformLaunchConfig(config: GenericLaunchConfig): LanguageSpecificLaunchConfig {
return {
...config,
type: 'mock',
request: 'launch',
name: 'Mock Debug'
};
}
getDefaultLaunchConfig(): Partial<GenericLaunchConfig> {
return {
stopOnEntry: false,
justMyCode: true,
env: {},
cwd: process.cwd()
};
}
// ===== DAP Protocol Operations =====
async sendDapRequest<T extends DebugProtocol.Response>(
command: string,
args?: unknown
): Promise<T> {
// 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 {} as T;
}
handleDapEvent(event: DebugProtocol.Event): void {
// 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 as keyof AdapterEvents, event.body);
}
handleDapResponse(_response: DebugProtocol.Response): void {
// Mock adapter doesn't need special response handling
void _response; // Explicitly ignore
}
// ===== Connection Management =====
async connect(host: string, port: number): Promise<void> {
// 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(): Promise<void> {
this.connected = false;
this.currentThreadId = null;
this.transitionTo(AdapterState.DISCONNECTED);
this.emit('disconnected');
}
isConnected(): boolean {
return this.connected;
}
// ===== Error Handling =====
getInstallationInstructions(): string {
return 'The Mock Debug Adapter is built-in and requires no installation.';
}
getMissingExecutableError(): string {
return 'Mock executable not found. This should not happen with the mock adapter.';
}
translateErrorMessage(error: Error): string {
if (error.message.includes('ENOENT')) {
return 'Mock file not found: ' + error.message;
}
return error.message;
}
// ===== Feature Support =====
supportsFeature(feature: DebugFeature): boolean {
return this.config.supportedFeatures?.includes(feature) || false;
}
getFeatureRequirements(feature: DebugFeature): FeatureRequirement[] {
const requirements: FeatureRequirement[] = [];
if (feature === DebugFeature.CONDITIONAL_BREAKPOINTS) {
requirements.push({
type: 'version',
description: 'Mock adapter version 1.0+',
required: true
});
}
return requirements;
}
getCapabilities(): AdapterCapabilities {
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: MockErrorScenario): void {
this.errorScenario = scenario;
}
}