Skip to main content
Glama
initial-history-replay-capture.test.ts12.6 kB
/** * Story 4: Initial History Replay Capture - Unit Tests * * Tests for InitialHistoryReplayCapture class that captures initial WebSocket messages * when browsers connect to terminal sessions, distinguishing between history replay * and real-time messages. * * CRITICAL: No mocks in production code. These tests verify real functionality. */ import { InitialHistoryReplayCapture } from './initial-history-replay-capture'; import { WebSocketConnectionDiscovery } from './websocket-connection-discovery'; import { EventEmitter } from 'events'; // Test helper to create a WebSocket-like interface without mocking class TestWebSocket extends EventEmitter { public readyState: number = 1; // WebSocket.OPEN // Helper method to simulate receiving a message public simulateMessage(data: string): void { this.emit('message', Buffer.from(data)); } // Helper method to simulate WebSocket closure public simulateClose(): void { this.readyState = 3; // WebSocket.CLOSED this.emit('close'); } } describe('InitialHistoryReplayCapture', () => { let capture: InitialHistoryReplayCapture; afterEach(async () => { if (capture) { await capture.cleanup(); } }); describe('Class Structure', () => { it('should exist and be constructable', () => { // This will fail until we create the class - RED phase expect(() => { capture = new InitialHistoryReplayCapture(); }).not.toThrow(); }); it('should have required public methods', () => { capture = new InitialHistoryReplayCapture(); // Core functionality methods expect(typeof capture.captureInitialHistory).toBe('function'); expect(typeof capture.waitForHistoryReplayComplete).toBe('function'); expect(typeof capture.distinguishMessageTypes).toBe('function'); expect(typeof capture.getHistoryMessages).toBe('function'); expect(typeof capture.getRealTimeMessages).toBe('function'); expect(typeof capture.isCapturing).toBe('function'); expect(typeof capture.cleanup).toBe('function'); }); }); describe('Configuration', () => { it('should accept connection discovery and configuration', () => { const realConnectionDiscovery = {} as WebSocketConnectionDiscovery; const config = { historyReplayTimeout: 10000, captureTimeout: 5000, maxHistoryMessages: 1000 }; expect(() => { capture = new InitialHistoryReplayCapture(realConnectionDiscovery, config); }).not.toThrow(); }); it('should use default configuration when none provided', () => { const realConnectionDiscovery = {} as WebSocketConnectionDiscovery; capture = new InitialHistoryReplayCapture(realConnectionDiscovery); const config = capture.getConfig(); expect(config.historyReplayTimeout).toBe(5000); // Default timeout fixed to prevent race condition expect(config.maxHistoryMessages).toBe(10000); // Default max messages }); }); describe('Message Capture', () => { let testWebSocket: TestWebSocket; beforeEach(() => { testWebSocket = new TestWebSocket(); capture = new InitialHistoryReplayCapture(); }); it('should start capturing messages when captureInitialHistory is called', async () => { expect(capture.isCapturing()).toBe(false); await capture.captureInitialHistory(testWebSocket as any); expect(capture.isCapturing()).toBe(true); }); it('should listen for WebSocket messages when capturing starts', async () => { await capture.captureInitialHistory(testWebSocket as any); // Should have attached message listener - verify by checking event listener count expect(testWebSocket.listenerCount('message')).toBe(1); }); it('should capture messages in the correct order', async () => { await capture.captureInitialHistory(testWebSocket as any); // Send some test messages testWebSocket.simulateMessage('First message'); testWebSocket.simulateMessage('Second message'); testWebSocket.simulateMessage('Third message'); const historyMessages = capture.getHistoryMessages(); expect(historyMessages).toHaveLength(3); expect(historyMessages[0].data).toBe('First message'); expect(historyMessages[1].data).toBe('Second message'); expect(historyMessages[2].data).toBe('Third message'); }); it('should assign sequence numbers to captured messages', async () => { await capture.captureInitialHistory(testWebSocket as any); testWebSocket.simulateMessage('First message'); testWebSocket.simulateMessage('Second message'); const historyMessages = capture.getHistoryMessages(); expect(historyMessages[0].sequenceNumber).toBe(1); expect(historyMessages[1].sequenceNumber).toBe(2); }); it('should include timestamps in captured messages', async () => { await capture.captureInitialHistory(testWebSocket as any); const beforeTime = Date.now(); testWebSocket.simulateMessage('Test message'); const afterTime = Date.now(); const historyMessages = capture.getHistoryMessages(); expect(historyMessages[0].timestamp).toBeGreaterThanOrEqual(beforeTime); expect(historyMessages[0].timestamp).toBeLessThanOrEqual(afterTime); }); }); describe('History Replay vs Real-Time Message Distinction', () => { let testWebSocket: TestWebSocket; beforeEach(() => { testWebSocket = new TestWebSocket(); capture = new InitialHistoryReplayCapture(); }); it('should wait for history replay completion', async () => { await capture.captureInitialHistory(testWebSocket as any); // This should wait for some indication that history replay is complete const waitPromise = capture.waitForHistoryReplayComplete(); // Should be a promise that eventually resolves expect(waitPromise).toBeInstanceOf(Promise); }); it('should distinguish between history replay and real-time messages', async () => { // Use a capture with a short timeout for this test const testCapture = new InitialHistoryReplayCapture(undefined, { historyReplayTimeout: 50 }); await testCapture.captureInitialHistory(testWebSocket as any); // Initially, messages should be marked as history replay testWebSocket.simulateMessage('History message 1'); testWebSocket.simulateMessage('History message 2'); // Simulate completion of history replay await testCapture.waitForHistoryReplayComplete(); // Now messages should be marked as real-time testWebSocket.simulateMessage('Real-time message 1'); testWebSocket.simulateMessage('Real-time message 2'); const historyMessages = testCapture.getHistoryMessages(); const realTimeMessages = testCapture.getRealTimeMessages(); expect(historyMessages).toHaveLength(2); expect(realTimeMessages).toHaveLength(2); expect(historyMessages[0].data).toBe('History message 1'); expect(historyMessages[1].data).toBe('History message 2'); expect(realTimeMessages[0].data).toBe('Real-time message 1'); expect(realTimeMessages[1].data).toBe('Real-time message 2'); expect(historyMessages[0].isHistoryReplay).toBe(true); expect(historyMessages[1].isHistoryReplay).toBe(true); expect(realTimeMessages[0].isHistoryReplay).toBe(false); expect(realTimeMessages[1].isHistoryReplay).toBe(false); await testCapture.cleanup(); }); it('should handle completion timeout for history replay', async () => { const shortTimeoutCapture = new InitialHistoryReplayCapture(undefined, { historyReplayTimeout: 100 // Very short timeout for testing }); await shortTimeoutCapture.captureInitialHistory(testWebSocket as any); // Should timeout and resolve after the specified time const startTime = Date.now(); await shortTimeoutCapture.waitForHistoryReplayComplete(); const endTime = Date.now(); expect(endTime - startTime).toBeGreaterThanOrEqual(95); await shortTimeoutCapture.cleanup(); }); it('should correctly identify message types with distinguishMessageTypes', async () => { await capture.captureInitialHistory(testWebSocket as any); const historyMessage: any = { timestamp: Date.now(), data: 'Test message', isHistoryReplay: true, sequenceNumber: 1 }; const realTimeMessage: any = { timestamp: Date.now(), data: 'Test message', isHistoryReplay: false, sequenceNumber: 2 }; expect(capture.distinguishMessageTypes(historyMessage)).toBe(true); expect(capture.distinguishMessageTypes(realTimeMessage)).toBe(false); }); }); describe('Empty History Handling', () => { let testWebSocket: TestWebSocket; beforeEach(() => { testWebSocket = new TestWebSocket(); capture = new InitialHistoryReplayCapture(); }); it('should handle empty history gracefully', async () => { await capture.captureInitialHistory(testWebSocket as any); // No messages sent during history replay period const shortTimeoutCapture = new InitialHistoryReplayCapture(undefined, { historyReplayTimeout: 50 }); await shortTimeoutCapture.captureInitialHistory(testWebSocket as any); await shortTimeoutCapture.waitForHistoryReplayComplete(); const historyMessages = shortTimeoutCapture.getHistoryMessages(); const realTimeMessages = shortTimeoutCapture.getRealTimeMessages(); expect(historyMessages).toHaveLength(0); expect(realTimeMessages).toHaveLength(0); await shortTimeoutCapture.cleanup(); }); it('should transition to real-time mode even with no history messages', async () => { const shortTimeoutCapture = new InitialHistoryReplayCapture(undefined, { historyReplayTimeout: 50 }); await shortTimeoutCapture.captureInitialHistory(testWebSocket as any); // Wait for history replay to complete without any messages await shortTimeoutCapture.waitForHistoryReplayComplete(); // Now send a real-time message testWebSocket.simulateMessage('Real-time message after empty history'); const historyMessages = shortTimeoutCapture.getHistoryMessages(); const realTimeMessages = shortTimeoutCapture.getRealTimeMessages(); expect(historyMessages).toHaveLength(0); expect(realTimeMessages).toHaveLength(1); expect(realTimeMessages[0].data).toBe('Real-time message after empty history'); expect(realTimeMessages[0].isHistoryReplay).toBe(false); await shortTimeoutCapture.cleanup(); }); it('should respect maxHistoryMessages limit', async () => { const limitedCapture = new InitialHistoryReplayCapture(undefined, { maxHistoryMessages: 2, historyReplayTimeout: 1000 // Long enough for test }); await limitedCapture.captureInitialHistory(testWebSocket as any); // Send more messages than the limit testWebSocket.simulateMessage('History message 1'); testWebSocket.simulateMessage('History message 2'); testWebSocket.simulateMessage('History message 3'); // Should be dropped testWebSocket.simulateMessage('History message 4'); // Should be dropped const historyMessages = limitedCapture.getHistoryMessages(); expect(historyMessages).toHaveLength(2); expect(historyMessages[0].data).toBe('History message 1'); expect(historyMessages[1].data).toBe('History message 2'); await limitedCapture.cleanup(); }); }); describe('Cleanup', () => { let testWebSocket: TestWebSocket; beforeEach(() => { testWebSocket = new TestWebSocket(); capture = new InitialHistoryReplayCapture(); }); it('should properly cleanup resources and cancel timers', async () => { await capture.captureInitialHistory(testWebSocket as any); // Should have message listener attached expect(testWebSocket.listenerCount('message')).toBe(1); expect(capture.isCapturing()).toBe(true); // Cleanup should remove listeners and stop capturing await capture.cleanup(); expect(testWebSocket.listenerCount('message')).toBe(0); expect(capture.isCapturing()).toBe(false); }); }); });

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/LightspeedDMS/ssh-mcp'

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