Skip to main content
Glama
ooples

MCP Console Automation Server

TestReplayEngine.ts10.1 kB
/** * TestReplayEngine - Replays recorded console tests * * This class loads test recordings and replays them step-by-step, * with configurable speed control and timing preservation. */ import { TestRecording, RecordingStep, ReplayResult, StepResult, } from '../types/test-framework.js'; import { TestRecorder } from './TestRecorder.js'; import { ConsoleManager } from '../core/ConsoleManager.js'; export interface ReplayOptions { speed?: number; // 1.0 = normal, 2.0 = 2x speed, 0 = fast-forward preserveTiming?: boolean; // Whether to preserve original timing validateOutput?: boolean; // Whether to validate output matches recording stopOnError?: boolean; // Whether to stop replay on first error timeout?: number; // Overall timeout for replay (ms) } export class TestReplayEngine { private consoleManager: ConsoleManager; private sessionMap: Map<string, string> = new Map(); // Maps recorded session IDs to actual session IDs constructor(consoleManager?: ConsoleManager) { this.consoleManager = consoleManager || new ConsoleManager(); } /** * Replay a recording by name */ public async replayByName( name: string, options: ReplayOptions = {}, outputDir = 'data/recordings' ): Promise<ReplayResult> { const recording = TestRecorder.loadRecording(name, outputDir); return this.replay(recording, options); } /** * Replay a test recording */ public async replay( recording: TestRecording, options: ReplayOptions = {} ): Promise<ReplayResult> { const startTime = Date.now(); const results: StepResult[] = []; const speed = options.speed !== undefined ? options.speed : 1.0; const preserveTiming = options.preserveTiming !== undefined ? options.preserveTiming : true; const validateOutput = options.validateOutput !== undefined ? options.validateOutput : false; const stopOnError = options.stopOnError !== undefined ? options.stopOnError : false; const timeout = options.timeout || 300000; // 5 minutes default this.sessionMap.clear(); let lastStepTime = 0; let overallStatus: 'success' | 'failure' | 'error' = 'success'; let replayError: Error | undefined; try { for (let i = 0; i < recording.steps.length; i++) { const step = recording.steps[i]; const stepStartTime = Date.now(); // Check overall timeout if (Date.now() - startTime > timeout) { throw new Error(`Replay timeout exceeded: ${timeout}ms`); } // Handle timing if (preserveTiming && i > 0 && speed > 0) { const timeDiff = step.timestamp - lastStepTime; const waitTime = timeDiff / speed; if (waitTime > 0) { await this.sleep(waitTime); } } lastStepTime = step.timestamp; // Execute step const stepResult = await this.executeStep(step, validateOutput); results.push(stepResult); // Check step result if (stepResult.status === 'fail' || stepResult.status === 'skip') { overallStatus = 'failure'; if (stopOnError) { break; } } } } catch (error) { overallStatus = 'error'; replayError = error instanceof Error ? error : new Error(String(error)); } finally { // Cleanup sessions await this.cleanupSessions(); } const duration = Date.now() - startTime; return { recording: recording.name, status: overallStatus, duration, steps: results, error: replayError, }; } /** * Execute a single recording step */ private async executeStep( step: RecordingStep, validateOutput: boolean ): Promise<StepResult> { const startTime = Date.now(); let status: 'pass' | 'fail' | 'skip' = 'pass'; let error: Error | undefined; let output: string | undefined; try { switch (step.type) { case 'create_session': output = await this.executeCreateSession(step); break; case 'send_input': output = await this.executeSendInput(step); break; case 'send_key': output = await this.executeSendKey(step); break; case 'wait_for_output': output = await this.executeWaitForOutput(step); break; case 'assert': // Phase 2 - Assertion framework status = 'skip'; output = 'Assertions not yet implemented (Phase 2)'; break; case 'snapshot': // Phase 2 - Snapshot framework status = 'skip'; output = 'Snapshots not yet implemented (Phase 2)'; break; default: throw new Error(`Unknown step type: ${(step as any).type}`); } // Validate output if requested if (validateOutput && step.output && output !== step.output) { status = 'fail'; error = new Error( `Output mismatch: expected "${step.output}", got "${output}"` ); } } catch (err) { status = 'fail'; error = err instanceof Error ? err : new Error(String(err)); output = error.message; } const duration = Date.now() - startTime; return { step, status, duration, error, output, }; } /** * Execute create_session step */ private async executeCreateSession(step: RecordingStep): Promise<string> { const { data } = step; const recordedSessionId = step.sessionId || 'default'; try { const actualSessionId = await this.consoleManager.createSession(data); // Map recorded session ID to actual session ID this.sessionMap.set(recordedSessionId, actualSessionId); return `Session created: ${actualSessionId}`; } catch (error) { throw new Error( `Failed to create session: ${error instanceof Error ? error.message : String(error)}` ); } } /** * Execute send_input step */ private async executeSendInput(step: RecordingStep): Promise<string> { const { input } = step.data; const sessionId = this.getActualSessionId(step.sessionId); try { await this.consoleManager.sendInput(sessionId, input); return 'Input sent successfully'; } catch (error) { throw new Error( `Failed to send input: ${error instanceof Error ? error.message : String(error)}` ); } } /** * Execute send_key step */ private async executeSendKey(step: RecordingStep): Promise<string> { const { key } = step.data; const sessionId = this.getActualSessionId(step.sessionId); try { await this.consoleManager.sendKey(sessionId, key); return 'Key sent successfully'; } catch (error) { throw new Error( `Failed to send key: ${error instanceof Error ? error.message : String(error)}` ); } } /** * Execute wait_for_output step */ private async executeWaitForOutput(step: RecordingStep): Promise<string> { const { pattern, timeout } = step.data; const sessionId = this.getActualSessionId(step.sessionId); try { const result = await this.consoleManager.waitForOutput( sessionId, pattern, { timeout } ); return result.output || `Matched pattern: ${pattern}`; } catch (error) { throw new Error( `Failed to wait for output: ${error instanceof Error ? error.message : String(error)}` ); } } /** * Get actual session ID from recorded session ID */ private getActualSessionId(recordedSessionId?: string): string { if (!recordedSessionId) { // Use the first session if no specific session ID const firstSession = this.sessionMap.values().next().value; if (!firstSession) { throw new Error('No session available'); } return firstSession; } const actualSessionId = this.sessionMap.get(recordedSessionId); if (!actualSessionId) { throw new Error(`Session not found: ${recordedSessionId}`); } return actualSessionId; } /** * Cleanup all sessions created during replay */ private async cleanupSessions(): Promise<void> { const cleanupPromises = Array.from(this.sessionMap.values()).map( async (sessionId) => { try { await this.consoleManager.stopSession(sessionId); } catch (error) { // Ignore cleanup errors } } ); await Promise.allSettled(cleanupPromises); this.sessionMap.clear(); } /** * Sleep for specified milliseconds */ private sleep(ms: number): Promise<void> { return new Promise((resolve) => setTimeout(resolve, ms)); } /** * Get replay statistics */ public static getReplayStats(result: ReplayResult): { total: number; passed: number; failed: number; skipped: number; passRate: number; } { const total = result.steps.length; const passed = result.steps.filter((s) => s.status === 'pass').length; const failed = result.steps.filter((s) => s.status === 'fail').length; const skipped = result.steps.filter((s) => s.status === 'skip').length; const passRate = total > 0 ? passed / total : 0; return { total, passed, failed, skipped, passRate, }; } /** * Format replay result as human-readable string */ public static formatResult(result: ReplayResult): string { const stats = TestReplayEngine.getReplayStats(result); const lines: string[] = []; lines.push(`Replay Result: ${result.recording}`); lines.push(`Status: ${result.status.toUpperCase()}`); lines.push(`Duration: ${result.duration}ms`); lines.push( `Steps: ${stats.passed}/${stats.total} passed (${(stats.passRate * 100).toFixed(1)}%)` ); if (stats.failed > 0) { lines.push(`Failed: ${stats.failed}`); } if (stats.skipped > 0) { lines.push(`Skipped: ${stats.skipped}`); } if (result.error) { lines.push(`Error: ${result.error.message}`); } return lines.join('\n'); } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/ooples/mcp-console-automation'

If you have feedback or need assistance with the MCP directory API, please join our Discord server