Skip to main content
Glama
story5-acceptance-criteria.test.ts15.6 kB
/** * Story 5: Post-WebSocket Real-time Command Execution - Acceptance Criteria Validation * * Direct validation of all Story 5 acceptance criteria using the Gherkin format: * * Given I have established WebSocket connection * And I have specified post-WebSocket commands ["ssh_exec whoami"] * When the testing framework executes the post-phase * Then it should send each command via MCP stdin/stdout * And capture corresponding WebSocket messages for each command * And wait for WebSocket responses before sending next command * And preserve message ordering between commands * And handle command execution failures appropriately * * CRITICAL: These are acceptance 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('Story 5: Post-WebSocket Real-time Command Execution - Acceptance Criteria', () => { 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.removeAllListeners('connection'); mockWebSocketServer.on('connection', (ws) => { ws.on('message', (data) => { // Echo back the message to simulate terminal output setTimeout(() => { ws.send(`server_response: ${data}`); ws.send('prompt> '); }, 50); }); }); }); afterEach(async () => { if (executor) { await executor.cleanup(); } if (historyCapture) { await historyCapture.cleanup(); } if (mockWebSocket && mockWebSocket.readyState === WebSocket.OPEN) { mockWebSocket.close(); } if (mcpClient) { await mcpClient.disconnect(); } if (mcpServerManager) { await mcpServerManager.stop(); } }); describe('Acceptance Criteria: Given I have established WebSocket connection', () => { test('WebSocket connection should be established before command execution', async () => { // Given: WebSocket connection is established (already done in beforeEach) expect(mockWebSocket.readyState).toBe(WebSocket.OPEN); // And: History capture is initialized await historyCapture.captureInitialHistory(mockWebSocket); expect(historyCapture.isCapturing()).toBe(true); }); }); describe('Acceptance Criteria: And I have specified post-WebSocket commands ["ssh_exec whoami"]', () => { test('should accept and validate post-WebSocket commands specification', async () => { const commands = ['ssh_exec whoami']; // Given: WebSocket connection established await historyCapture.captureInitialHistory(mockWebSocket); // When: Commands are specified // Then: Executor should accept the command specification without error expect(() => { // This validates command parsing in the executor const testCommands = commands; expect(testCommands).toEqual(['ssh_exec whoami']); }).not.toThrow(); }); }); describe('Acceptance Criteria: When the testing framework executes the post-phase', () => { test('should execute post-phase after WebSocket connection is established', async () => { const commands = ['ssh_exec whoami']; // Given: WebSocket connection established await historyCapture.captureInitialHistory(mockWebSocket); await historyCapture.waitForHistoryReplayComplete(); // When: Testing framework executes the post-phase const results = await executor.executeCommands(commands, mockWebSocket); // Then: Post-phase execution should complete successfully expect(results).toHaveLength(1); expect(results[0].command).toBe('ssh_exec whoami'); expect(results[0].executionStartTime).toBeLessThanOrEqual(results[0].executionEndTime); }); }); describe('Acceptance Criteria: Then it should send each command via MCP stdin/stdout', () => { test('should send commands via real MCP stdin/stdout communication', async () => { const commands = ['ssh_exec whoami', 'ssh_exec pwd']; // Given: WebSocket connection and commands specified await historyCapture.captureInitialHistory(mockWebSocket); await historyCapture.waitForHistoryReplayComplete(); // When: Commands are executed const results = await executor.executeCommands(commands, mockWebSocket); // Then: Each command should be sent via MCP stdin/stdout expect(results).toHaveLength(2); results.forEach((result, index) => { expect(result.command).toBe(commands[index]); expect(result.mcpResponse).toBeDefined(); // MCP response indicates real stdin/stdout communication occurred expect(typeof result.mcpResponse?.success).toBe('boolean'); }); }); }); describe('Acceptance Criteria: And capture corresponding WebSocket messages for each command', () => { test('should capture WebSocket messages corresponding to each command execution', async () => { const commands = ['ssh_exec echo test1', 'ssh_exec echo test2']; // Given: WebSocket connection and message capture setup await historyCapture.captureInitialHistory(mockWebSocket); await historyCapture.waitForHistoryReplayComplete(); // Send some messages to generate WebSocket traffic mockWebSocket.send('pre-command message'); await new Promise(resolve => setTimeout(resolve, 100)); // When: Commands are executed const results = await executor.executeCommands(commands, mockWebSocket); // Then: WebSocket messages should be captured for each command expect(results).toHaveLength(2); results.forEach(result => { expect(result.capturedMessages).toBeDefined(); expect(Array.isArray(result.capturedMessages)).toBe(true); // Each command execution should correlate with captured messages }); }); }); describe('Acceptance Criteria: And wait for WebSocket responses before sending next command', () => { test('should wait for WebSocket responses before proceeding to next command', async () => { const commands = ['ssh_exec echo first', 'ssh_exec echo second']; // Given: WebSocket connection established await historyCapture.captureInitialHistory(mockWebSocket); await historyCapture.waitForHistoryReplayComplete(); // Track execution timing let firstCommandComplete = 0; let secondCommandStart = 0; // Monitor MCP client calls to track timing const originalCallTool = mcpClient.callTool; let callCount = 0; mcpClient.callTool = async (name: string, args: Record<string, unknown>) => { callCount++; if (callCount === 1) { const result = await originalCallTool.call(mcpClient, name, args); firstCommandComplete = Date.now(); return result; } else if (callCount === 2) { secondCommandStart = Date.now(); return await originalCallTool.call(mcpClient, name, args); } return await originalCallTool.call(mcpClient, name, args); }; // When: Commands are executed const results = await executor.executeCommands(commands, mockWebSocket); // Then: Second command should start after first command completes expect(results).toHaveLength(2); expect(firstCommandComplete).toBeLessThanOrEqual(secondCommandStart); expect(results[0].executionEndTime).toBeLessThanOrEqual(results[1].executionStartTime); // Restore original method mcpClient.callTool = originalCallTool; }); }); describe('Acceptance Criteria: And preserve message ordering between commands', () => { test('should preserve message ordering between sequential command executions', async () => { const commands = ['ssh_exec echo cmd1', 'ssh_exec echo cmd2', 'ssh_exec echo cmd3']; // Given: WebSocket connection established await historyCapture.captureInitialHistory(mockWebSocket); await historyCapture.waitForHistoryReplayComplete(); // When: Multiple commands are executed sequentially const results = await executor.executeCommands(commands, mockWebSocket); // Then: Message ordering should be preserved expect(results).toHaveLength(3); // Verify sequential execution timing expect(results[0].executionEndTime).toBeLessThanOrEqual(results[1].executionStartTime); expect(results[1].executionEndTime).toBeLessThanOrEqual(results[2].executionStartTime); // Verify each command has its own captured messages results.forEach((result, index) => { expect(result.command).toBe(commands[index]); expect(result.capturedMessages).toBeDefined(); }); }); }); describe('Acceptance Criteria: And handle command execution failures appropriately', () => { test('should handle command execution failures without stopping subsequent commands', async () => { const commands = ['ssh_exec /invalid/command', 'ssh_exec whoami']; // Given: WebSocket connection established await historyCapture.captureInitialHistory(mockWebSocket); await historyCapture.waitForHistoryReplayComplete(); // When: Commands include a failing command const results = await executor.executeCommands(commands, mockWebSocket); // Then: Failures should be handled appropriately expect(results).toHaveLength(2); // First command should fail but not throw expect(results[0].command).toBe('ssh_exec /invalid/command'); expect(results[0].mcpResponse).toBeDefined(); // Second command should still execute despite first command failure expect(results[1].command).toBe('ssh_exec whoami'); expect(results[1].mcpResponse).toBeDefined(); // Both commands should have execution times recorded expect(results[0].executionStartTime).toBeLessThanOrEqual(results[0].executionEndTime); expect(results[1].executionStartTime).toBeLessThanOrEqual(results[1].executionEndTime); }); test('should capture failure details in command execution results', async () => { const commands = ['ssh_exec /definitely/invalid/command/path']; // Given: WebSocket connection established await historyCapture.captureInitialHistory(mockWebSocket); await historyCapture.waitForHistoryReplayComplete(); // When: A command that will fail is executed const results = await executor.executeCommands(commands, mockWebSocket); // Then: Failure details should be captured appropriately expect(results).toHaveLength(1); expect(results[0].command).toBe(commands[0]); expect(results[0].mcpResponse).toBeDefined(); // Should still capture messages even for failed commands expect(results[0].capturedMessages).toBeDefined(); expect(Array.isArray(results[0].capturedMessages)).toBe(true); }); }); describe('Complete Acceptance Criteria Integration', () => { test('should satisfy all acceptance criteria in a single comprehensive scenario', async () => { // Given I have established WebSocket connection expect(mockWebSocket.readyState).toBe(WebSocket.OPEN); await historyCapture.captureInitialHistory(mockWebSocket); // And I have specified post-WebSocket commands const postWebSocketCommands = ['ssh_exec whoami', 'ssh_exec pwd', 'ssh_exec /invalid/cmd']; // When the testing framework executes the post-phase const startTime = Date.now(); const results = await executor.executeCommands(postWebSocketCommands, mockWebSocket); const totalTime = Date.now() - startTime; // Then it should send each command via MCP stdin/stdout expect(results).toHaveLength(3); results.forEach(result => { expect(result.mcpResponse).toBeDefined(); expect(typeof result.mcpResponse?.success).toBe('boolean'); }); // And capture corresponding WebSocket messages for each command results.forEach(result => { expect(result.capturedMessages).toBeDefined(); expect(Array.isArray(result.capturedMessages)).toBe(true); }); // And wait for WebSocket responses before sending next command expect(results[0].executionEndTime).toBeLessThanOrEqual(results[1].executionStartTime); expect(results[1].executionEndTime).toBeLessThanOrEqual(results[2].executionStartTime); // And preserve message ordering between commands expect(totalTime).toBeGreaterThanOrEqual(1000); // Inter-command delays applied // And handle command execution failures appropriately expect(results[0].command).toBe('ssh_exec whoami'); expect(results[1].command).toBe('ssh_exec pwd'); expect(results[2].command).toBe('ssh_exec /invalid/cmd'); // All commands should complete execution regardless of success/failure results.forEach(result => { expect(result.executionStartTime).toBeLessThanOrEqual(result.executionEndTime); }); }); }); });

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