@debugmcp/mcp-debugger
Version:
Run-time step-through debugging for LLM agents.
311 lines (259 loc) • 8.03 kB
text/typescript
/**
* Test implementations of process launcher interfaces
* These provide deterministic, controllable behavior for unit testing
*/
import { EventEmitter } from 'events';
import { PassThrough } from 'stream';
import {
IProcess,
IProcessLauncher,
IProcessOptions,
IDebugTargetLauncher,
IDebugTarget,
IProxyProcessLauncher,
IProxyProcess,
IProcessLauncherFactory
} from '../../../src/interfaces/process-interfaces.js';
/**
* Fake process implementation for testing
* Provides controllable behavior without spawning real processes
*/
export class FakeProcess extends EventEmitter implements IProcess {
pid = 12345;
stdin = new PassThrough() as any;
stdout = new PassThrough() as any;
stderr = new PassThrough() as any;
private _killed = false;
private _exitCode: number | null = null;
private _signalCode: string | null = null;
get killed(): boolean {
return this._killed;
}
get exitCode(): number | null {
return this._exitCode;
}
get signalCode(): string | null {
return this._signalCode;
}
send(message: any): boolean {
if (this._killed) return false;
// Emit message event on next tick to simulate async IPC
process.nextTick(() => this.emit('message', message));
return true;
}
kill(signal = 'SIGTERM'): boolean {
if (this._killed) return false;
this._killed = true;
this._signalCode = signal;
process.nextTick(() => {
this.emit('exit', 0, signal);
this.emit('close', 0, signal);
});
return true;
}
// Test helpers
simulateOutput(data: string): void {
this.stdout.push(data);
}
simulateError(data: string): void {
this.stderr.push(data);
}
simulateExit(code: number, signal?: string): void {
this._exitCode = code;
this._signalCode = signal || null;
this._killed = true;
this.emit('exit', code, signal);
this.emit('close', code, signal);
}
simulateSpawn(): void {
process.nextTick(() => this.emit('spawn'));
}
simulateProcessError(error: Error): void {
this.emit('error', error);
}
simulateMessage(message: any): void {
this.emit('message', message);
}
}
/**
* Fake implementation of IProcessLauncher for testing
*/
export class FakeProcessLauncher implements IProcessLauncher {
public launchedProcesses: Array<{
command: string;
args: string[];
options?: IProcessOptions;
process: FakeProcess;
}> = [];
private nextProcess: FakeProcess | undefined;
launch(command: string, args: string[], options?: IProcessOptions): IProcess {
const process = this.nextProcess || new FakeProcess();
this.nextProcess = undefined;
this.launchedProcesses.push({ command, args, options, process });
// Simulate spawn event
process.simulateSpawn();
return process;
}
// Test helper: prepare a specific process for the next launch
prepareProcess(setup: (process: FakeProcess) => void): void {
const process = new FakeProcess();
setup(process);
this.nextProcess = process;
}
// Test helper: get the last launched process
getLastLaunchedProcess(): FakeProcess | undefined {
const last = this.launchedProcesses[this.launchedProcesses.length - 1];
return last?.process;
}
// Test helper: reset state
reset(): void {
this.launchedProcesses = [];
this.nextProcess = undefined;
}
}
/**
* Fake implementation of IDebugTargetLauncher for testing
*/
export class FakeDebugTargetLauncher implements IDebugTargetLauncher {
public launchedTargets: Array<{
scriptPath: string;
args: string[];
pythonPath?: string;
debugPort: number;
target: IDebugTarget;
}> = [];
private nextDebugPort = 5678;
private nextTarget: IDebugTarget | undefined;
async launchPythonDebugTarget(
scriptPath: string,
args: string[],
pythonPath = 'python',
debugPort?: number
): Promise<IDebugTarget> {
const port = debugPort || this.nextDebugPort++;
if (this.nextTarget) {
const target = this.nextTarget;
this.nextTarget = undefined;
this.launchedTargets.push({ scriptPath, args, pythonPath, debugPort: port, target });
return target;
}
const process = new FakeProcess();
const target: IDebugTarget = {
process,
debugPort: port,
terminate: async () => {
process.kill('SIGTERM');
await new Promise(resolve => process.once('exit', resolve));
}
};
this.launchedTargets.push({ scriptPath, args, pythonPath, debugPort: port, target });
return target;
}
// Test helper: prepare a specific target for the next launch
prepareTarget(target: IDebugTarget): void {
this.nextTarget = target;
}
// Test helper: reset state
reset(): void {
this.launchedTargets = [];
this.nextTarget = undefined;
this.nextDebugPort = 5678;
}
}
/**
* Fake proxy process implementation for testing
*/
export class FakeProxyProcess extends FakeProcess implements IProxyProcess {
public sentCommands: object[] = [];
constructor(public readonly sessionId: string) {
super();
}
sendCommand(command: object): void {
this.sentCommands.push(command);
const messageStr = JSON.stringify(command);
this.send(messageStr);
}
async waitForInitialization(timeout = 30000): Promise<void> {
// Simulate successful initialization by default
return Promise.resolve();
}
// Test helper: simulate initialization completion
simulateInitialization(): void {
this.simulateMessage({
type: 'status',
sessionId: this.sessionId,
status: 'adapter_configured_and_launched'
});
}
// Test helper: simulate initialization failure
simulateInitializationFailure(error: string): void {
this.simulateMessage({
type: 'error',
sessionId: this.sessionId,
message: error
});
}
}
/**
* Fake implementation of IProxyProcessLauncher for testing
*/
export class FakeProxyProcessLauncher implements IProxyProcessLauncher {
public launchedProxies: Array<{
proxyScriptPath: string;
sessionId: string;
env?: Record<string, string>;
process: FakeProxyProcess;
}> = [];
private nextProxy: FakeProxyProcess | undefined;
launchProxy(
proxyScriptPath: string,
sessionId: string,
env?: Record<string, string>
): IProxyProcess {
const proxy = this.nextProxy || new FakeProxyProcess(sessionId);
this.nextProxy = undefined;
this.launchedProxies.push({ proxyScriptPath, sessionId, env, process: proxy });
// Simulate spawn event
proxy.simulateSpawn();
return proxy;
}
// Test helper: prepare a specific proxy for the next launch
prepareProxy(setup: (proxy: FakeProxyProcess) => void): void {
const proxy = new FakeProxyProcess('test-session');
setup(proxy);
this.nextProxy = proxy;
}
// Test helper: get the last launched proxy
getLastLaunchedProxy(): FakeProxyProcess | undefined {
const last = this.launchedProxies[this.launchedProxies.length - 1];
return last?.process;
}
// Test helper: reset state
reset(): void {
this.launchedProxies = [];
this.nextProxy = undefined;
}
}
/**
* Fake implementation of IProcessLauncherFactory for testing
*/
export class FakeProcessLauncherFactory implements IProcessLauncherFactory {
public processLauncher = new FakeProcessLauncher();
public debugTargetLauncher = new FakeDebugTargetLauncher();
public proxyProcessLauncher = new FakeProxyProcessLauncher();
createProcessLauncher(): IProcessLauncher {
return this.processLauncher;
}
createDebugTargetLauncher(): IDebugTargetLauncher {
return this.debugTargetLauncher;
}
createProxyProcessLauncher(): IProxyProcessLauncher {
return this.proxyProcessLauncher;
}
// Test helper: reset all fakes
reset(): void {
this.processLauncher.reset();
this.debugTargetLauncher.reset();
this.proxyProcessLauncher.reset();
}
}