Skip to main content
Glama
ooples

MCP Console Automation Server

TestReplayEngine.test.ts14.1 kB
/** * Unit tests for TestReplayEngine */ import { TestReplayEngine } from '../testing/TestReplayEngine.js'; import { TestRecorder } from '../testing/TestRecorder.js'; import { TestRecording } from '../types/test-framework.js'; import { ConsoleManager } from '../core/ConsoleManager.js'; // Mock ConsoleManager jest.mock('../core/ConsoleManager'); describe('TestReplayEngine', () => { let engine: TestReplayEngine; let mockConsoleManager: jest.Mocked<ConsoleManager>; const testOutputDir = 'data/recordings/test'; beforeEach(() => { mockConsoleManager = new ConsoleManager() as jest.Mocked<ConsoleManager>; engine = new TestReplayEngine(mockConsoleManager); // Setup default mocks mockConsoleManager.createSession = jest.fn().mockResolvedValue({ sessionId: 'test-session-123', success: true, }); mockConsoleManager.sendInput = jest.fn().mockResolvedValue({ success: true, output: 'Command executed', }); mockConsoleManager.sendKey = jest.fn().mockResolvedValue({ success: true, output: 'Key sent', }); mockConsoleManager.waitForOutput = jest.fn().mockResolvedValue({ success: true, output: 'Pattern matched', }); mockConsoleManager.stopSession = jest.fn().mockResolvedValue({ success: true, }); }); describe('replay', () => { it('should replay a simple recording', async () => { const recording: TestRecording = { name: 'test-recording', version: '1.0.0', createdAt: new Date().toISOString(), duration: 1000, steps: [ { type: 'create_session', timestamp: 0, data: { command: 'bash' }, sessionId: 'session1', }, { type: 'send_input', timestamp: 100, data: { input: 'echo hello' }, sessionId: 'session1', }, ], metadata: {}, }; const result = await engine.replay(recording); expect(result.status).toBe('success'); expect(result.steps.length).toBe(2); expect(result.steps[0].status).toBe('pass'); expect(result.steps[1].status).toBe('pass'); expect(mockConsoleManager.createSession).toHaveBeenCalledWith({ command: 'bash', }); expect(mockConsoleManager.sendInput).toHaveBeenCalled(); }); it('should handle recording with wait_for_output', async () => { const recording: TestRecording = { name: 'test-wait', version: '1.0.0', createdAt: new Date().toISOString(), duration: 500, steps: [ { type: 'create_session', timestamp: 0, data: { command: 'bash' }, sessionId: 'session1', }, { type: 'wait_for_output', timestamp: 100, data: { pattern: 'prompt>', timeout: 5000 }, sessionId: 'session1', }, ], metadata: {}, }; const result = await engine.replay(recording); expect(result.status).toBe('success'); expect(mockConsoleManager.waitForOutput).toHaveBeenCalledWith({ sessionId: 'test-session-123', pattern: 'prompt>', timeout: 5000, }); }); it('should skip assertion and snapshot steps (Phase 2)', async () => { const recording: TestRecording = { name: 'test-skip', version: '1.0.0', createdAt: new Date().toISOString(), duration: 100, steps: [ { type: 'assert', timestamp: 0, data: { type: 'output_contains', expected: 'hello' }, }, { type: 'snapshot', timestamp: 50, data: { name: 'state1' }, }, ], metadata: {}, }; const result = await engine.replay(recording); expect(result.status).toBe('success'); expect(result.steps[0].status).toBe('skip'); expect(result.steps[1].status).toBe('skip'); }); it('should handle errors in steps', async () => { mockConsoleManager.sendInput = jest .fn() .mockRejectedValue(new Error('Send failed')); const recording: TestRecording = { name: 'test-error', version: '1.0.0', createdAt: new Date().toISOString(), duration: 100, steps: [ { type: 'create_session', timestamp: 0, data: { command: 'bash' }, sessionId: 'session1', }, { type: 'send_input', timestamp: 10, data: { input: 'test' }, sessionId: 'session1', }, ], metadata: {}, }; const result = await engine.replay(recording); expect(result.status).toBe('failure'); expect(result.steps[1].status).toBe('fail'); expect(result.steps[1].error?.message).toContain('Send failed'); }); it('should stop on error if stopOnError is true', async () => { mockConsoleManager.createSession = jest .fn() .mockResolvedValueOnce({ sessionId: 'session1' }) .mockRejectedValue(new Error('Failed')); const recording: TestRecording = { name: 'test-stop', version: '1.0.0', createdAt: new Date().toISOString(), duration: 100, steps: [ { type: 'create_session', timestamp: 0, data: {}, sessionId: 's1' }, { type: 'create_session', timestamp: 10, data: {}, sessionId: 's2' }, { type: 'send_input', timestamp: 20, data: { input: 'test' } }, ], metadata: {}, }; const result = await engine.replay(recording, { stopOnError: true }); expect(result.steps.length).toBe(2); // Should stop after second step fails expect(result.status).toBe('failure'); }); it('should continue on error if stopOnError is false', async () => { mockConsoleManager.sendInput = jest .fn() .mockRejectedValueOnce(new Error('Error 1')) .mockResolvedValueOnce({ success: true }); const recording: TestRecording = { name: 'test-continue', version: '1.0.0', createdAt: new Date().toISOString(), duration: 100, steps: [ { type: 'create_session', timestamp: 0, data: {}, sessionId: 's1' }, { type: 'send_input', timestamp: 10, data: { input: 'fail' } }, { type: 'send_input', timestamp: 20, data: { input: 'success' } }, ], metadata: {}, }; const result = await engine.replay(recording, { stopOnError: false }); expect(result.steps.length).toBe(3); expect(result.status).toBe('failure'); // Overall failure due to one failed step }); it('should respect timeout option', async () => { const recording: TestRecording = { name: 'test-timeout', version: '1.0.0', createdAt: new Date().toISOString(), duration: 100, steps: [ { type: 'create_session', timestamp: 0, data: {}, sessionId: 's1' }, ], metadata: {}, }; // Mock a slow operation mockConsoleManager.createSession = jest.fn( (options: any) => new Promise((resolve) => setTimeout(() => resolve('s1'), 200)) ) as any; const result = await engine.replay(recording, { timeout: 100 }); expect(result.status).toBe('error'); expect(result.error?.message).toContain('timeout'); }, 10000); it('should validate output if validateOutput is true', async () => { mockConsoleManager.sendInput = jest.fn().mockResolvedValue({ success: true, output: 'different output', }); const recording: TestRecording = { name: 'test-validate', version: '1.0.0', createdAt: new Date().toISOString(), duration: 100, steps: [ { type: 'create_session', timestamp: 0, data: {}, sessionId: 's1' }, { type: 'send_input', timestamp: 10, data: { input: 'test' }, output: 'expected output', sessionId: 's1', }, ], metadata: {}, }; const result = await engine.replay(recording, { validateOutput: true }); expect(result.steps[1].status).toBe('fail'); expect(result.steps[1].error?.message).toContain('Output mismatch'); }); it('should cleanup sessions after replay', async () => { const recording: TestRecording = { name: 'test-cleanup', version: '1.0.0', createdAt: new Date().toISOString(), duration: 100, steps: [ { type: 'create_session', timestamp: 0, data: {}, sessionId: 's1' }, { type: 'create_session', timestamp: 10, data: {}, sessionId: 's2' }, ], metadata: {}, }; await engine.replay(recording); expect(mockConsoleManager.stopSession).toHaveBeenCalledTimes(2); }); }); describe('replayByName', () => { it('should load and replay recording by name', async () => { // Create a test recording file const recorder = new TestRecorder(testOutputDir); recorder.startRecording({ name: 'test-by-name' }); recorder.recordCreateSession('s1', { command: 'bash' }); recorder.stopRecording(); const result = await engine.replayByName( 'test-by-name', {}, testOutputDir ); expect(result.recording).toBe('test-by-name'); expect(result.steps.length).toBeGreaterThan(0); }); it('should throw error if recording not found', async () => { await expect( engine.replayByName('nonexistent', {}, testOutputDir) ).rejects.toThrow('Recording not found'); }); }); describe('Speed control', () => { it('should replay at normal speed (1x)', async () => { const recording: TestRecording = { name: 'test-speed-1x', version: '1.0.0', createdAt: new Date().toISOString(), duration: 100, steps: [ { type: 'create_session', timestamp: 0, data: {}, sessionId: 's1' }, { type: 'send_input', timestamp: 100, data: { input: 'test' }, sessionId: 's1', }, ], metadata: {}, }; const start = Date.now(); await engine.replay(recording, { speed: 1.0 }); const duration = Date.now() - start; // Should take roughly 100ms (with some tolerance) expect(duration).toBeGreaterThanOrEqual(90); }, 10000); it('should replay at 2x speed', async () => { const recording: TestRecording = { name: 'test-speed-2x', version: '1.0.0', createdAt: new Date().toISOString(), duration: 100, steps: [ { type: 'create_session', timestamp: 0, data: {}, sessionId: 's1' }, { type: 'send_input', timestamp: 100, data: { input: 'test' }, sessionId: 's1', }, ], metadata: {}, }; const start = Date.now(); await engine.replay(recording, { speed: 2.0 }); const duration = Date.now() - start; // Should take roughly 50ms expect(duration).toBeLessThan(90); }, 10000); it('should fast-forward when speed is 0', async () => { const recording: TestRecording = { name: 'test-fast-forward', version: '1.0.0', createdAt: new Date().toISOString(), duration: 1000, steps: [ { type: 'create_session', timestamp: 0, data: {}, sessionId: 's1' }, { type: 'send_input', timestamp: 1000, data: { input: 'test' }, sessionId: 's1', }, ], metadata: {}, }; const start = Date.now(); await engine.replay(recording, { speed: 0, preserveTiming: false }); const duration = Date.now() - start; // Should complete almost immediately (no timing delays) expect(duration).toBeLessThan(100); }); }); describe('Static methods', () => { describe('getReplayStats', () => { it('should calculate replay statistics', () => { const result = { recording: 'test', status: 'success' as const, duration: 1000, steps: [ { step: {} as any, status: 'pass' as const, duration: 100 }, { step: {} as any, status: 'pass' as const, duration: 200 }, { step: {} as any, status: 'fail' as const, duration: 150 }, { step: {} as any, status: 'skip' as const, duration: 50 }, ], }; const stats = TestReplayEngine.getReplayStats(result); expect(stats.total).toBe(4); expect(stats.passed).toBe(2); expect(stats.failed).toBe(1); expect(stats.skipped).toBe(1); expect(stats.passRate).toBe(0.5); }); }); describe('formatResult', () => { it('should format result as human-readable string', () => { const result = { recording: 'test-recording', status: 'success' as const, duration: 1234, steps: [ { step: {} as any, status: 'pass' as const, duration: 100 }, { step: {} as any, status: 'pass' as const, duration: 200 }, ], }; const formatted = TestReplayEngine.formatResult(result); expect(formatted).toContain('test-recording'); expect(formatted).toContain('SUCCESS'); expect(formatted).toContain('1234ms'); expect(formatted).toContain('2/2 passed'); }); it('should include error message if present', () => { const result = { recording: 'test-error', status: 'error' as const, duration: 100, steps: [], error: new Error('Test error message'), }; const formatted = TestReplayEngine.formatResult(result); expect(formatted).toContain('Test error message'); }); }); }); });

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