Skip to main content
Glama
post-websocket-command-executor-integration.test.ts13.1 kB
/** * Story 5: Post-WebSocket Real-time Command Execution - Integration Tests * * Integration tests for PostWebSocketCommandExecutor with real MCP connections * and WebSocket message capture. These tests use real components with minimal mocking. * * CRITICAL: These are integration tests - NO MOCKS for core functionality. * Real MCP server, real WebSocket connections, real message capture. */ import { PostWebSocketCommandExecutor } from './post-websocket-command-executor'; import { MCPServerManager } from './mcp-server-manager'; import { MCPClient } from './mcp-client'; import { InitialHistoryReplayCapture } from './initial-history-replay-capture'; import WebSocket, { WebSocketServer } from 'ws'; import { ChildProcess } from 'child_process'; describe('PostWebSocketCommandExecutor - Integration Tests', () => { let mcpServerManager: MCPServerManager; let mcpClient: MCPClient; let historyCapture: InitialHistoryReplayCapture; let executor: PostWebSocketCommandExecutor; let mockWebSocketServer: WebSocketServer; let mockWebSocket: WebSocket; let serverPort: number; beforeAll(async () => { // Start a mock WebSocket server for message capture testing mockWebSocketServer = new WebSocketServer({ port: 0 }); serverPort = (mockWebSocketServer.address() as any).port; }); afterAll(async () => { if (mockWebSocketServer) { mockWebSocketServer.close(); } }); beforeEach(async () => { // Initialize MCP server manager mcpServerManager = new MCPServerManager(); // Start MCP server await mcpServerManager.start(); // Get server process info and create MCP client const processInfo = mcpServerManager.getProcess(); if (!processInfo) { throw new Error('Failed to get server process'); } const serverProcess = { stdin: processInfo.stdin, stdout: processInfo.stdout } as ChildProcess; mcpClient = new MCPClient(serverProcess); // Initialize history capture historyCapture = new InitialHistoryReplayCapture(undefined, { historyReplayTimeout: 2000, captureTimeout: 30000, maxHistoryMessages: 100 }); // Create executor executor = new PostWebSocketCommandExecutor(mcpClient, historyCapture, { commandTimeout: 15000, interCommandDelay: 500, maxRetries: 2 }); // Create mock WebSocket connection for message capture mockWebSocket = new WebSocket(`ws://localhost:${serverPort}`); // Wait for WebSocket connection await new Promise<void>((resolve, reject) => { const timeout = setTimeout(() => reject(new Error('WebSocket connection timeout')), 5000); mockWebSocket.onopen = () => { clearTimeout(timeout); resolve(); }; mockWebSocket.onerror = (error) => { clearTimeout(timeout); reject(error); }; }); // Setup message echoing to simulate terminal responses mockWebSocketServer.on('connection', (ws) => { ws.on('message', (data) => { // Echo back the message to simulate terminal output setTimeout(() => { ws.send(`echo: ${data}`); ws.send('prompt> '); }, 100); }); }); }); afterEach(async () => { if (executor) { await executor.cleanup(); } if (historyCapture) { await historyCapture.cleanup(); } if (mockWebSocket && mockWebSocket.readyState === WebSocket.OPEN) { mockWebSocket.close(); } if (mcpServerManager) { await mcpServerManager.stop(); } }); describe('Real MCP Integration', () => { test('should execute ssh_exec command via real MCP server', async () => { // Start WebSocket message capture await historyCapture.captureInitialHistory(mockWebSocket); // Execute command const results = await executor.executeCommands(['ssh_exec whoami'], mockWebSocket); expect(results).toHaveLength(1); expect(results[0].command).toBe('ssh_exec whoami'); expect(results[0].success).toBeDefined(); expect(results[0].mcpResponse).toBeDefined(); expect(results[0].executionStartTime).toBeLessThanOrEqual(results[0].executionEndTime); // Verify MCP response structure if (results[0].mcpResponse) { expect(typeof results[0].mcpResponse.success).toBe('boolean'); } }); test('should execute multiple commands sequentially via real MCP server', async () => { // Start WebSocket message capture await historyCapture.captureInitialHistory(mockWebSocket); const commands = ['ssh_exec whoami', 'ssh_exec pwd']; const startTime = Date.now(); const results = await executor.executeCommands(commands, mockWebSocket); expect(results).toHaveLength(2); // Verify commands were executed sequentially expect(results[0].executionStartTime).toBeLessThanOrEqual(results[1].executionStartTime); expect(results[0].executionEndTime).toBeLessThanOrEqual(results[1].executionStartTime); // Verify inter-command delay was applied const totalTime = Date.now() - startTime; expect(totalTime).toBeGreaterThanOrEqual(400); // At least one delay period // Each result should have valid structure results.forEach((result, index) => { expect(result.command).toBe(commands[index]); expect(result.executionStartTime).toBeLessThanOrEqual(result.executionEndTime); expect(result.mcpResponse).toBeDefined(); }); }); test('should handle real MCP command failures gracefully', async () => { // Start WebSocket message capture await historyCapture.captureInitialHistory(mockWebSocket); // Execute invalid command that should fail const results = await executor.executeCommands(['ssh_exec /invalid/command/path'], mockWebSocket); expect(results).toHaveLength(1); expect(results[0].command).toBe('ssh_exec /invalid/command/path'); expect(results[0].mcpResponse).toBeDefined(); // Should handle failure without throwing if (results[0].mcpResponse && !results[0].mcpResponse.success) { expect(results[0].error).toBeDefined(); } }); }); describe('WebSocket Message Correlation', () => { test('should correlate real WebSocket messages with command execution', async () => { // Start WebSocket message capture await historyCapture.captureInitialHistory(mockWebSocket); // Wait for history replay to complete await historyCapture.waitForHistoryReplayComplete(); // Send a test message to generate WebSocket traffic mockWebSocket.send('test message before command'); // Give time for message to arrive await new Promise(resolve => setTimeout(resolve, 200)); // Execute command const results = await executor.executeCommands(['ssh_exec echo test'], mockWebSocket); expect(results).toHaveLength(1); // Should have captured some messages (at least the echoed responses) expect(results[0].capturedMessages).toBeDefined(); expect(Array.isArray(results[0].capturedMessages)).toBe(true); // Verify message structure if messages were captured if (results[0].capturedMessages.length > 0) { results[0].capturedMessages.forEach(message => { expect(message.timestamp).toBeDefined(); expect(message.data).toBeDefined(); expect(typeof message.isHistoryReplay).toBe('boolean'); expect(message.sequenceNumber).toBeDefined(); }); } }); test('should distinguish between history replay and real-time messages', async () => { // Send some messages before starting capture (these won't be captured) mockWebSocket.send('pre-capture message'); await new Promise(resolve => setTimeout(resolve, 100)); // Start WebSocket message capture await historyCapture.captureInitialHistory(mockWebSocket); // Send message during history replay phase mockWebSocket.send('during history replay'); await new Promise(resolve => setTimeout(resolve, 100)); // Wait for history replay to complete await historyCapture.waitForHistoryReplayComplete(); // Send message during real-time phase mockWebSocket.send('real-time message'); await new Promise(resolve => setTimeout(resolve, 100)); // Execute command during real-time phase const results = await executor.executeCommands(['ssh_exec echo realtime'], mockWebSocket); // Verify message correlation const historyMessages = historyCapture.getHistoryMessages(); const realTimeMessages = historyCapture.getRealTimeMessages(); // Should have some messages in each category expect(historyMessages.length + realTimeMessages.length).toBeGreaterThan(0); // History messages should be marked as history replay historyMessages.forEach(msg => { expect(msg.isHistoryReplay).toBe(true); }); // Real-time messages should not be marked as history replay realTimeMessages.forEach(msg => { expect(msg.isHistoryReplay).toBe(false); }); // Command execution should correlate with real-time messages expect(results[0].capturedMessages.every(msg => !msg.isHistoryReplay)).toBe(true); }); }); describe('Error Handling and Resource Management', () => { test('should handle WebSocket disconnection during execution', async () => { // Start WebSocket message capture await historyCapture.captureInitialHistory(mockWebSocket); // Close WebSocket during execution setTimeout(() => { mockWebSocket.close(); }, 100); // Execute command - should not fail due to WebSocket closure const results = await executor.executeCommands(['ssh_exec echo test'], mockWebSocket); expect(results).toHaveLength(1); expect(results[0].command).toBe('ssh_exec echo test'); // Should complete despite WebSocket closure }); test('should prevent concurrent command execution', async () => { // Start WebSocket message capture await historyCapture.captureInitialHistory(mockWebSocket); // Start first execution (don't await) const firstExecution = executor.executeCommands(['ssh_exec sleep 1'], mockWebSocket); // Try to start second execution await expect(executor.executeCommands(['ssh_exec whoami'], mockWebSocket)) .rejects.toThrow('Command execution already in progress'); // Wait for first execution to complete await firstExecution; // Should now allow new execution const results = await executor.executeCommands(['ssh_exec whoami'], mockWebSocket); expect(results).toHaveLength(1); }); test('should cleanup resources properly after execution', async () => { // Start WebSocket message capture await historyCapture.captureInitialHistory(mockWebSocket); // Execute commands await executor.executeCommands(['ssh_exec echo test'], mockWebSocket); // Verify cleanup expect(executor.isExecuting()).toBe(false); // Manual cleanup should not throw await expect(executor.cleanup()).resolves.not.toThrow(); }); }); describe('Configuration and Timeouts', () => { test('should respect command timeout configuration', async () => { // Create executor with short timeout const shortTimeoutExecutor = new PostWebSocketCommandExecutor(mcpClient, historyCapture, { commandTimeout: 100, // Very short timeout interCommandDelay: 0, maxRetries: 1 }); // Start WebSocket message capture await historyCapture.captureInitialHistory(mockWebSocket); try { // Execute command that might timeout (depending on server response time) const results = await shortTimeoutExecutor.executeCommands(['ssh_exec sleep 2'], mockWebSocket); // If command completes within timeout, that's also valid expect(results).toHaveLength(1); } catch (error) { // Timeout is acceptable for this test expect((error as Error).message).toContain('timeout'); } finally { await shortTimeoutExecutor.cleanup(); } }); test('should apply inter-command delays as configured', async () => { // Create executor with specific delay const delayedExecutor = new PostWebSocketCommandExecutor(mcpClient, historyCapture, { commandTimeout: 15000, interCommandDelay: 1000, // 1 second delay maxRetries: 2 }); // Start WebSocket message capture await historyCapture.captureInitialHistory(mockWebSocket); const startTime = Date.now(); await delayedExecutor.executeCommands(['ssh_exec echo 1', 'ssh_exec echo 2'], mockWebSocket); const totalTime = Date.now() - startTime; // Should take at least the delay time expect(totalTime).toBeGreaterThanOrEqual(800); // Allow some margin await delayedExecutor.cleanup(); }); }); });

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