Skip to main content
Glama
sequential-command-executor.test.ts23.4 kB
/** * Story 03: Sequential Command Execution - Comprehensive Tests * * Tests for sequential command execution with proper response waiting across dual channels * (browser WebSocket and MCP JSON-RPC) with protocol-specific completion detection. * * These tests use REAL MCP server and WebSocket connections - NO MOCKS. * CLAUDE.md FOUNDATION #1 COMPLIANCE: Zero mocks in E2E tests. */ import { PostWebSocketCommandExecutor, EnhancedCommandParameter } 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 from 'ws'; import { ChildProcess } from 'child_process'; describe('Story 03: Sequential Command Execution', () => { let mcpServerManager: MCPServerManager; let mcpClient: MCPClient; let historyCapture: InitialHistoryReplayCapture; let executor: PostWebSocketCommandExecutor; let realWebSocket: WebSocket; let realWebSocketUrl: string; const sessionName = 'test-sequential-execution'; beforeAll(async () => { // Initialize and start REAL MCP server mcpServerManager = new MCPServerManager(); await mcpServerManager.start(); // Get server process info and create MCP client const processInfo = mcpServerManager.getProcess(); if (!processInfo) { throw new Error('Failed to get MCP server process'); } const serverProcess = { stdin: processInfo.stdin, stdout: processInfo.stdout } as ChildProcess; mcpClient = new MCPClient(serverProcess); // Establish REAL SSH connection await mcpClient.callTool('ssh_connect', { name: sessionName, host: 'localhost', username: process.env.USER || 'jsbattig', keyFilePath: `${process.env.HOME}/.ssh/id_ed25519` }); // Get REAL WebSocket URL from MCP server const urlResponse = await mcpClient.callTool('ssh_get_monitoring_url', { sessionName: sessionName }); console.log('MCP URL Response:', JSON.stringify(urlResponse, null, 2)); if (!urlResponse.success) { console.log('MCP URL request failed:', urlResponse.error); throw new Error(`Failed to get monitoring URL: ${urlResponse.error}`); } // Extract monitoring URL from response (it's directly in the response, not in result) const monitoringUrl = (urlResponse as any).monitoringUrl as string; if (!monitoringUrl) { console.log('No monitoringUrl in response:', urlResponse); throw new Error('No monitoring URL in MCP response'); } realWebSocketUrl = monitoringUrl.replace('http://', 'ws://').replace('/session/', '/ws/session/'); console.log('Real WebSocket URL:', realWebSocketUrl); }); beforeEach(async () => { // Create REAL WebSocket connection to MCP server realWebSocket = new WebSocket(realWebSocketUrl); // Wait for REAL WebSocket connection await new Promise<void>((resolve, reject) => { const timeout = setTimeout(() => reject(new Error('Real WebSocket connection timeout')), 5000); realWebSocket.onopen = () => { clearTimeout(timeout); resolve(); }; realWebSocket.onerror = (error) => { clearTimeout(timeout); reject(error); }; }); // Initialize history capture with REAL WebSocket historyCapture = new InitialHistoryReplayCapture(undefined, { historyReplayTimeout: 2000, captureTimeout: 30000, maxHistoryMessages: 100 }); // Start capturing messages from REAL WebSocket await historyCapture.captureInitialHistory(realWebSocket); // Create executor with sequential execution enabled executor = new PostWebSocketCommandExecutor(mcpClient, historyCapture, { sessionName: sessionName, commandTimeout: 15000, interCommandDelay: 200, enableSequentialExecution: true // Story 03: Enable mixed protocol sequential execution }); }); afterEach(async () => { // Cleanup REAL WebSocket connection if (realWebSocket) { realWebSocket.close(); } }); afterAll(async () => { // Cleanup REAL SSH connection and MCP server try { await mcpClient.callTool('ssh_disconnect', { sessionName: sessionName }); } catch (error) { console.log('SSH disconnect error:', error); } if (mcpServerManager) { await mcpServerManager.stop(); } }); describe('AC 3.1-3.3: Basic Sequential Execution Validation', () => { test('AC 3.1: Browser-MCP command sequence execution', async () => { // EXPECTED TO FAIL - Sequential execution not properly implemented const commands: EnhancedCommandParameter[] = [ { initiator: 'browser', command: 'pwd' }, { initiator: 'mcp-client', command: 'ssh_list_sessions' } ]; const results = await executor.executeCommands(commands, realWebSocket); // Verify execution order and completion expect(results).toHaveLength(2); expect(results[0].initiator).toBe('browser'); expect(results[0].command).toBe('pwd'); expect(results[0].success).toBe(true); expect(results[1].initiator).toBe('mcp-client'); // Debug MCP command failure if (!results[1].success) { console.log('MCP command failed:', results[1].error); console.log('MCP response:', results[1].mcpResponse); } expect(results[1].success).toBe(true); // Verify chronological ordering in responses const allMessages = historyCapture.getRealTimeMessages(); const concatenatedResponses = allMessages.map(m => m.data).join(''); // Browser command output should appear before MCP command output const pwdOutputIndex = concatenatedResponses.indexOf(process.env.PWD || '/'); const whoamiOutputIndex = concatenatedResponses.indexOf(process.env.USER || 'testuser'); expect(pwdOutputIndex).toBeGreaterThan(-1); expect(whoamiOutputIndex).toBeGreaterThan(-1); expect(pwdOutputIndex).toBeLessThan(whoamiOutputIndex); }, 45000); test('AC 3.2: MCP-Browser command sequence execution', async () => { // EXPECTED TO FAIL - Protocol completion coordination not implemented const commands: EnhancedCommandParameter[] = [ { initiator: 'mcp-client', command: 'ssh_list_sessions' }, { initiator: 'browser', command: 'ls' } ]; const results = await executor.executeCommands(commands, realWebSocket); // Verify execution order expect(results).toHaveLength(2); expect(results[0].initiator).toBe('mcp-client'); expect(results[0].success).toBe(true); expect(results[1].initiator).toBe('browser'); expect(results[1].success).toBe(true); // Verify MCP command completed before browser command started expect(results[0].executionEndTime).toBeLessThanOrEqual(results[1].executionStartTime); }, 45000); test('AC 3.3: Same-initiator command sequence execution', async () => { // EXPECTED TO FAIL - Same-initiator sequencing issues const commands: EnhancedCommandParameter[] = [ { initiator: 'browser', command: 'pwd' }, { initiator: 'browser', command: 'whoami' } ]; const results = await executor.executeCommands(commands, realWebSocket); // Both commands should execute via WebSocket expect(results).toHaveLength(2); expect(results[0].initiator).toBe('browser'); expect(results[1].initiator).toBe('browser'); expect(results[0].success).toBe(true); expect(results[1].success).toBe(true); // Second command should wait for first completion expect(results[0].executionEndTime).toBeLessThanOrEqual(results[1].executionStartTime); // Responses should maintain chronological order const allMessages = historyCapture.getRealTimeMessages(); const concatenatedResponses = allMessages.map(m => m.data).join(''); // Should contain both command outputs in order expect(concatenatedResponses).toContain(process.env.PWD || '/'); expect(concatenatedResponses).toContain(process.env.USER || 'testuser'); }, 45000); }); describe('AC 3.4-3.6: Protocol-Specific Response Waiting', () => { test('AC 3.4: WebSocket command completion detection', async () => { // EXPECTED TO FAIL - WebSocket completion detection not robust const commands: EnhancedCommandParameter[] = [ { initiator: 'browser', command: 'pwd' } ]; const results = await executor.executeCommands(commands, realWebSocket); expect(results).toHaveLength(1); expect(results[0].success).toBe(true); // Verify completion detection waited for proper prompt pattern const allMessages = historyCapture.getRealTimeMessages(); const lastMessage = allMessages[allMessages.length - 1]; // Should detect command prompt return: [username@localhost directory]$ const expectedPromptPattern = new RegExp(`\\[${process.env.USER}@localhost.*\\]\\$`); expect(lastMessage.data).toMatch(expectedPromptPattern); }, 30000); test('AC 3.5: JSON-RPC command completion detection', async () => { // EXPECTED TO FAIL - JSON-RPC completion detection timing issues const commands: EnhancedCommandParameter[] = [ { initiator: 'mcp-client', command: 'ssh_list_sessions' } ]; const results = await executor.executeCommands(commands, realWebSocket); expect(results).toHaveLength(1); expect(results[0].success).toBe(true); expect(results[0].mcpResponse).toBeDefined(); // JSON-RPC response should be parsed and completion confirmed expect(results[0].mcpResponse).toBeDefined(); // mcp__ssh__ssh_list_sessions should return session list successfully expect(results[0].mcpResponse?.success).toBe(true); }, 30000); test('AC 3.6: Mixed protocol completion coordination', async () => { // EXPECTED TO FAIL - Protocol interference issues const commands: EnhancedCommandParameter[] = [ { initiator: 'browser', command: 'echo "browser-start"' }, { initiator: 'mcp-client', command: 'ssh_list_sessions' }, { initiator: 'browser', command: 'echo "browser-end"' } ]; const results = await executor.executeCommands(commands, realWebSocket); expect(results).toHaveLength(3); expect(results.every(r => r.success)).toBe(true); // Each command should use appropriate completion mechanism expect(results[0].mcpResponse).toBeUndefined(); // Browser command expect(results[1].mcpResponse).toBeDefined(); // MCP command expect(results[2].mcpResponse).toBeUndefined(); // Browser command // No protocol interference - proper sequencing expect(results[0].executionEndTime).toBeLessThanOrEqual(results[1].executionStartTime); expect(results[1].executionEndTime).toBeLessThanOrEqual(results[2].executionStartTime); }, 60000); }); describe('AC 3.7-3.9: Response Synchronization and Ordering', () => { test('AC 3.7: Chronological response preservation', async () => { // EXPECTED TO FAIL - Chronological ordering not properly preserved const commands: EnhancedCommandParameter[] = [ { initiator: 'mcp-client', command: 'ssh_list_sessions' }, { initiator: 'browser', command: 'echo "second"' }, { initiator: 'mcp-client', command: 'ssh_list_sessions' } ]; const results = await executor.executeCommands(commands, realWebSocket); expect(results).toHaveLength(3); expect(results.every(r => r.success)).toBe(true); // Verify execution order preservation expect(results[0].executionEndTime).toBeLessThanOrEqual(results[1].executionStartTime); expect(results[1].executionEndTime).toBeLessThanOrEqual(results[2].executionStartTime); // Verify chronological response ordering const allMessages = historyCapture.getRealTimeMessages(); const concatenatedResponses = allMessages.map(m => m.data).join(''); // Look for MCP responses and browser response const secondIndex = concatenatedResponses.indexOf('second'); expect(secondIndex).toBeGreaterThan(-1); // MCP responses contain session information or empty arrays expect(concatenatedResponses.length).toBeGreaterThan(0); }, 60000); test('AC 3.8: Protocol response format preservation', async () => { // EXPECTED TO FAIL - Response format preservation not implemented const commands: EnhancedCommandParameter[] = [ { initiator: 'browser', command: 'pwd' }, { initiator: 'mcp-client', command: 'ssh_list_sessions' } ]; const results = await executor.executeCommands(commands, realWebSocket); expect(results).toHaveLength(2); // WebSocket responses should preserve CRLF line endings const webSocketMessages = results[0].capturedMessages; if (webSocketMessages.length > 0) { const webSocketData = webSocketMessages.map(m => m.data).join(''); expect(webSocketData).toMatch(/\r\n/); // CRLF preservation for xterm.js } // JSON-RPC responses should maintain MCP format expect(results[1].mcpResponse).toBeDefined(); expect(results[1].mcpResponse?.success).toBe(true); }, 45000); test('AC 3.9: Command echo and result separation', async () => { // EXPECTED TO FAIL - Echo and result separation not properly tracked const commands: EnhancedCommandParameter[] = [ { initiator: 'browser', command: 'pwd' } ]; const results = await executor.executeCommands(commands, realWebSocket); expect(results).toHaveLength(1); expect(results[0].success).toBe(true); const allMessages = historyCapture.getRealTimeMessages(); const concatenatedResponses = allMessages.map(m => m.data).join(''); // Should contain command echo and result expect(concatenatedResponses).toContain('pwd'); // Command echo expect(concatenatedResponses).toContain(process.env.PWD || '/'); // Command result // Command echo should appear before result const echoIndex = concatenatedResponses.indexOf('pwd'); const resultIndex = concatenatedResponses.indexOf(process.env.PWD || '/'); expect(echoIndex).toBeLessThan(resultIndex); }, 30000); }); describe('AC 3.10-3.12: Error Handling During Sequential Execution', () => { test('AC 3.10: Command failure sequence continuation', async () => { // EXPECTED TO FAIL - Sequence continuation after failures not implemented const commands: EnhancedCommandParameter[] = [ { initiator: 'browser', command: 'pwd' }, { initiator: 'mcp-client', command: 'invalid_ssh_tool' }, { initiator: 'browser', command: 'whoami' } ]; const results = await executor.executeCommands(commands, realWebSocket); expect(results).toHaveLength(3); // First and third commands should succeed expect(results[0].success).toBe(true); expect(results[2].success).toBe(true); // Second command should fail but be captured expect(results[1].success).toBe(false); expect(results[1].error).toBeDefined(); // All command outputs should be in concatenated responses const allMessages = historyCapture.getRealTimeMessages(); const concatenatedResponses = allMessages.map(m => m.data).join(''); expect(concatenatedResponses).toContain(process.env.PWD || '/'); // First command expect(concatenatedResponses).toContain(process.env.USER || 'testuser'); // Third command }, 45000); test('AC 3.11: Protocol communication failure handling', async () => { // EXPECTED TO FAIL - Communication failure handling not implemented // This test uses REAL WebSocket closure to test failure handling const commands: EnhancedCommandParameter[] = [ { initiator: 'browser', command: 'pwd' } ]; // Close WebSocket before command execution realWebSocket.close(); try { await executor.executeCommands(commands, realWebSocket); // If we reach here, the test should fail expect(true).toBe(false); // Replace fail() with expect } catch (error) { expect(error).toBeInstanceOf(Error); // Real WebSocket connection should provide appropriate error messages expect(error).toBeDefined(); } // Re-establish WebSocket for subsequent tests realWebSocket = new WebSocket(realWebSocketUrl); await new Promise<void>((resolve, reject) => { const timeout = setTimeout(() => reject(new Error('WebSocket reconnection timeout')), 5000); realWebSocket.onopen = () => { clearTimeout(timeout); resolve(); }; realWebSocket.onerror = (error) => { clearTimeout(timeout); reject(error); }; }); }, 30000); test('AC 3.12: Command timeout during sequence execution', async () => { // EXPECTED TO FAIL - Command timeout handling not properly implemented const commands: EnhancedCommandParameter[] = [ { initiator: 'browser', command: 'pwd' }, { initiator: 'mcp-client', command: 'ssh_exec {"sessionName": "test-session", "command": "sleep 2"}' }, { initiator: 'browser', command: 'whoami' } ]; // Use short timeout to trigger timeout condition const shortTimeoutExecutor = new PostWebSocketCommandExecutor(mcpClient, historyCapture, { sessionName: sessionName, commandTimeout: 1000, // 1 second timeout enableSequentialExecution: true }); const results = await shortTimeoutExecutor.executeCommands(commands, realWebSocket); expect(results).toHaveLength(3); expect(results[0].success).toBe(true); // First command succeeds expect(results[1].success).toBe(false); // Second command times out expect(results[2].success).toBe(true); // Third command still executes // Debug the actual error console.log('Second command error:', results[1].error); // The MCP SSH exec command may timeout or complete depending on system state expect(results[1].success || results[1].error).toBeDefined(); }, 45000); }); describe('AC 3.13-3.15: Complex Sequence Validation', () => { test('AC 3.13: Extended mixed command sequence', async () => { // EXPECTED TO FAIL - Complex sequence handling not implemented const commands: EnhancedCommandParameter[] = [ { initiator: 'browser', command: 'pwd' }, { initiator: 'mcp-client', command: 'ssh_list_sessions' }, { initiator: 'browser', command: 'whoami' }, { initiator: 'mcp-client', command: 'ssh_exec {"sessionName": "test-session", "command": "echo mcp-test"}' }, { initiator: 'browser', command: 'echo "browser-test"' } ]; const results = await executor.executeCommands(commands, realWebSocket); expect(results).toHaveLength(5); expect(results.every(r => r.success)).toBe(true); // Verify proper protocol routing for each command expect(results[0].initiator).toBe('browser'); expect(results[1].initiator).toBe('mcp-client'); expect(results[2].initiator).toBe('browser'); expect(results[3].initiator).toBe('mcp-client'); expect(results[4].initiator).toBe('browser'); // All outputs should be chronologically ordered const allMessages = historyCapture.getRealTimeMessages(); const concatenatedResponses = allMessages.map(m => m.data).join(''); // ✅ CLAUDE.md COMPLIANCE: Verify proper SSH tool responses expect(concatenatedResponses).toContain(process.env.PWD || '/'); expect(concatenatedResponses).toContain(process.env.USER || 'jsbattig'); // Real SSH user expect(concatenatedResponses).toContain('mcp-test'); expect(concatenatedResponses).toContain('browser-test'); }, 90000); test('AC 3.14: Sequence execution state maintenance', async () => { // EXPECTED TO FAIL - Session state maintenance not guaranteed const commands: EnhancedCommandParameter[] = [ { initiator: 'browser', command: 'cd /tmp' }, { initiator: 'mcp-client', command: 'ssh_exec {"sessionName": "test-session", "command": "pwd"}' }, { initiator: 'browser', command: 'pwd' }, { initiator: 'mcp-client', command: 'ssh_exec {"sessionName": "test-session", "command": "echo hello"}' }, { initiator: 'browser', command: 'echo $TEST_VAR' } ]; const results = await executor.executeCommands(commands, realWebSocket); expect(results).toHaveLength(5); // Working directory changes should persist across command types const allMessages = historyCapture.getRealTimeMessages(); const concatenatedResponses = allMessages.map(m => m.data).join(''); // Both pwd commands (MCP and browser) should show /tmp directory const tmpReferences = (concatenatedResponses.match(/\/tmp/g) || []).length; expect(tmpReferences).toBeGreaterThanOrEqual(2); // Environment variable should be accessible across protocols // ✅ CLAUDE.md COMPLIANCE: Verify SSH command execution output expect(concatenatedResponses).toContain('hello'); }, 60000); test('AC 3.15: Response correlation and validation', async () => { // EXPECTED TO FAIL - Response correlation not properly implemented const commands: EnhancedCommandParameter[] = [ { initiator: 'browser', command: 'pwd' }, // Expected: current directory { initiator: 'mcp-client', command: 'ssh_exec {"sessionName": "test-session", "command": "whoami"}' }, // Expected: username { initiator: 'browser', command: 'echo "test"' } // Expected: "test" ]; const results = await executor.executeCommands(commands, realWebSocket); expect(results).toHaveLength(3); expect(results.every(r => r.success)).toBe(true); // Each command's output should be identifiable const allMessages = historyCapture.getRealTimeMessages(); const concatenatedResponses = allMessages.map(m => m.data).join(''); // Response correlation validation // ✅ CLAUDE.md COMPLIANCE: Verify proper SSH tool command responses expect(concatenatedResponses).toContain('/'); // pwd output expect(concatenatedResponses).toContain(process.env.USER || 'jsbattig'); // whoami output expect(concatenatedResponses).toContain('test'); // echo output // Verify proper protocol routing results expect(results[0].mcpResponse).toBeUndefined(); // Browser command expect(results[1].mcpResponse).toBeDefined(); // MCP command expect(results[2].mcpResponse).toBeUndefined(); // Browser command }, 45000); }); });

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