/**
* 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);
});
});