Skip to main content
Glama
story05-command-interruption-emulation.test.ts38 kB
/** * Story 05: Command Interruption Emulation - Acceptance Criteria Tests * * Tests all 19 acceptance criteria for command interruption emulation in the Villenele framework. * Focuses on timeout-based cancellation, protocol-specific cancellation mechanisms, and response capture. * * CRITICAL: Following TDD methodology - these are failing tests that drive implementation. * IMPORTANT: No mocks in production code per CLAUDE.md - uses real WebSocket connections and MCP client. */ import { PostWebSocketCommandExecutor, EnhancedCommandParameter } from './post-websocket-command-executor'; import { MCPClient } from './mcp-client'; import { InitialHistoryReplayCapture } from './initial-history-replay-capture'; import { MCPServerManager } from './mcp-server-manager'; import { WebSocketConnectionDiscovery } from './websocket-connection-discovery'; // import { JestTestUtilities } from './jest-test-utilities'; // Will be used for integration later import WebSocket from 'ws'; describe('Story 05: Command Interruption Emulation', () => { let serverManager: MCPServerManager; let mcpClient: MCPClient; let executor: PostWebSocketCommandExecutor; let historyCapture: InitialHistoryReplayCapture; let webSocketDiscovery: WebSocketConnectionDiscovery; let webSocket: WebSocket; // let testUtils: JestTestUtilities; // Unused for now, will be used in integration beforeEach(async () => { // Start fresh MCP server for each test serverManager = new MCPServerManager(); await serverManager.start(); const processInfo = serverManager.getProcess(); if (!processInfo || !processInfo.stdin || !processInfo.stdout) { throw new Error('Failed to start MCP server for testing'); } mcpClient = new MCPClient(processInfo as any); // Set up WebSocket discovery webSocketDiscovery = new WebSocketConnectionDiscovery(mcpClient); // Create SSH session for testing await mcpClient.callTool('ssh_connect', { name: 'story05-test-session', host: 'localhost', username: 'jsbattig', keyFilePath: '/home/jsbattig/.ssh/id_ed25519' }); // Establish WebSocket connection const webSocketUrl = await webSocketDiscovery.discoverWebSocketUrl('story05-test-session'); webSocket = await webSocketDiscovery.establishConnection(webSocketUrl); // Initialize history capture with the WebSocket historyCapture = new InitialHistoryReplayCapture(); await historyCapture.captureInitialHistory(webSocket); executor = new PostWebSocketCommandExecutor(mcpClient, historyCapture, { sessionName: 'story05-test-session', enableSequentialExecution: true }); // Initialize test utilities - will be used for integration // testUtils = JestTestUtilities.setupJestEnvironment('story05-command-interruption'); }); afterEach(async () => { if (historyCapture) { await historyCapture.cleanup(); } if (webSocket) { webSocket.close(); } if (mcpClient) { await mcpClient.disconnect(); } if (serverManager) { await serverManager.stop(); } }); describe('Basic Timeout-Based Cancellation', () => { describe('AC 5.1: Browser command cancellation after timeout with sleep', () => { it('should cancel browser sleep command after timeout via WebSocket SIGINT', async () => { // Given: Browser command with cancellation: sleep 30, cancel after 3000ms const commands: EnhancedCommandParameter[] = [ { initiator: 'browser', command: 'sleep 30', cancel: true, waitToCancelMs: 3000 } ]; const startTime = performance.now(); // When: Villenele executes the command const results = await executor.executeCommands(commands, webSocket); const executionTime = performance.now() - startTime; // Then: Command should be sent via WebSocket terminal_input message expect(results).toHaveLength(1); expect(results[0].initiator).toBe('browser'); expect(results[0].command).toBe('sleep 30'); expect(results[0].cancelRequested).toBe(true); expect(results[0].waitToCancelMs).toBe(3000); // And: After 3000ms, cancellation should be triggered via WebSocket SIGINT expect(executionTime).toBeLessThan(30000); // Should not wait full 30 seconds expect(executionTime).toBeGreaterThanOrEqual(2500); // Should wait roughly 3 seconds (±500ms tolerance) expect(executionTime).toBeLessThanOrEqual(3500); // And: Sleep command should be interrupted with ^C before 30 seconds complete // And: Cancellation should be captured in concatenatedResponses showing interrupted sleep const capturedMessages = results[0].capturedMessages; const messageContent = capturedMessages.map(msg => JSON.stringify(msg)).join(' '); // This test will fail initially until WebSocket SIGINT cancellation is implemented expect(messageContent).toContain('^C'); // Should show interrupt signal expect(messageContent).toContain('sleep'); // Should show the command was started }); }); describe('AC 5.2: MCP command cancellation after timeout', () => { it('should cancel MCP sleep command after timeout via ssh_cancel_command', async () => { // Given: MCP command with cancellation: sleep 15, cancel after 5000ms const commands: EnhancedCommandParameter[] = [ { initiator: 'mcp-client', command: 'ssh_exec {"sessionName": "story05-test-session", "command": "sleep 15"}', cancel: true, waitToCancelMs: 5000 } ]; const startTime = performance.now(); // When: Villenele executes the command const results = await executor.executeCommands(commands, webSocket); const executionTime = performance.now() - startTime; // Then: Command should be sent via JSON-RPC stdin expect(results).toHaveLength(1); expect(results[0].initiator).toBe('mcp-client'); expect(results[0].command).toBe('ssh_exec {"sessionName": "story05-test-session", "command": "sleep 15"}'); expect(results[0].cancelRequested).toBe(true); expect(results[0].waitToCancelMs).toBe(5000); // And: After 5000ms, cancellation should be triggered via ssh_cancel_command tool expect(executionTime).toBeLessThan(15000); // Should not wait full 15 seconds expect(executionTime).toBeGreaterThanOrEqual(4500); // Should wait roughly 5 seconds (±500ms tolerance) expect(executionTime).toBeLessThanOrEqual(5500); // And: Command should be interrupted using MCP cancellation mechanism expect(results[0].mcpResponse).toBeDefined(); // And: Cancellation result should be captured in response // This test will fail initially until MCP ssh_cancel_command cancellation is implemented expect(results[0].success).toBe(false); // Command should be cancelled, not successful completion }); }); describe('AC 5.3: Default cancellation timeout behavior', () => { it('should use default 10 second timeout when waitToCancelMs not specified', async () => { // Given: Command with cancel enabled but no timeout specified const commands: EnhancedCommandParameter[] = [ { initiator: 'browser', command: 'sleep 20', cancel: true // waitToCancelMs not specified - should default to 10000 } ]; const startTime = performance.now(); // When: Villenele processes the configuration const results = await executor.executeCommands(commands, webSocket); const executionTime = performance.now() - startTime; // Then: Default waitToCancelMs should be set to 10000 (10 seconds) expect(results[0].waitToCancelMs).toBe(10000); // And: Cancellation should trigger after 10 seconds expect(executionTime).toBeLessThan(20000); // Should not wait full 20 seconds expect(executionTime).toBeGreaterThanOrEqual(9500); // Should wait roughly 10 seconds (±500ms tolerance) expect(executionTime).toBeLessThanOrEqual(10500); // And: Command should be interrupted appropriately // This test will fail initially until default timeout behavior is implemented expect(results[0].cancelRequested).toBe(true); }); }); }); describe('Protocol-Specific Cancellation Mechanisms', () => { describe('AC 5.4: WebSocket SIGINT signal cancellation', () => { it('should send WebSocket SIGINT signal for browser command cancellation', async () => { // Given: Browser command with cancellation const commands: EnhancedCommandParameter[] = [ { initiator: 'browser', command: 'sleep 10', cancel: true, waitToCancelMs: 2000 } ]; // When: Timeout period expires const results = await executor.executeCommands(commands, webSocket); // Then: Villenele should send WebSocket message with SIGINT signal const capturedMessages = results[0].capturedMessages; const messageContent = capturedMessages.map(msg => JSON.stringify(msg)).join(' '); // Expected WebSocket message: {type: 'terminal_signal', sessionName: 'story05-test-session', signal: 'SIGINT'} // This test will fail initially until WebSocket SIGINT implementation is added expect(messageContent).toContain('terminal_signal'); expect(messageContent).toContain('SIGINT'); expect(messageContent).toContain('story05-test-session'); // And: Terminal should receive interrupt signal // And: Command should terminate with interrupt indication: ^C expect(messageContent).toContain('^C'); // And: Command prompt should return after interruption expect(messageContent).toMatch(/\[jsbattig@localhost.*\]\$/); }); }); describe('AC 5.5: MCP ssh_cancel_command cancellation', () => { it('should call ssh_cancel_command tool for MCP command cancellation', async () => { // Given: MCP command with cancellation const commands: EnhancedCommandParameter[] = [ { initiator: 'mcp-client', command: 'find . -name "*.ts" -exec grep -l "export" {} \\;', cancel: true, waitToCancelMs: 4000 } ]; // When: Timeout period expires const results = await executor.executeCommands(commands, webSocket); // Then: Villenele should call ssh_cancel_command tool with session name // This test will fail initially until MCP ssh_cancel_command implementation is added expect(results[0].mcpResponse).toBeDefined(); expect(results[0].success).toBe(false); // Command cancelled, not successful // And: MCP server should cancel the running command // And: Cancellation result should indicate successful interruption // Implementation should track cancellation attempts in response // And: Session should remain active for subsequent commands // Follow-up command should still work after cancellation const followUpCommands: EnhancedCommandParameter[] = [ { initiator: 'mcp-client', command: 'ssh_exec {"sessionName": "story05-test-session", "command": "echo \"session still active\""}' } ]; const followUpResults = await executor.executeCommands(followUpCommands, webSocket); expect(followUpResults[0].success).toBe(true); }); }); describe('AC 5.6: Mixed cancellation scenario execution', () => { it('should handle mixed browser and MCP cancellations in sequence', async () => { // Given: Sequence with mixed cancellations const commands: EnhancedCommandParameter[] = [ { initiator: 'browser', command: 'pwd' }, // No cancel { initiator: 'browser', command: 'sleep 10', cancel: true, waitToCancelMs: 2000 }, // Browser cancel { initiator: 'mcp-client', command: 'ssh_exec {"sessionName": "story05-test-session", "command": "sleep 10"}', cancel: true, waitToCancelMs: 3000 }, // MCP cancel { initiator: 'browser', command: 'whoami' } // No cancel ]; const startTime = performance.now(); // When: Villenele executes the complete sequence const results = await executor.executeCommands(commands, webSocket); const executionTime = performance.now() - startTime; // Then: First command should complete normally expect(results[0].success).toBe(true); expect(results[0].cancelRequested).toBe(false); // And: Second command should be cancelled via WebSocket after 2 seconds expect(results[1].cancelRequested).toBe(true); expect(results[1].waitToCancelMs).toBe(2000); // And: Third command should be cancelled via MCP after 3 seconds expect(results[2].cancelRequested).toBe(true); expect(results[2].waitToCancelMs).toBe(3000); // And: Fourth command should execute normally after cancellations expect(results[3].success).toBe(true); expect(results[3].cancelRequested).toBe(false); // Total execution should be much less than 20 seconds (10+10 without cancellation) expect(executionTime).toBeLessThan(15000); // Should complete in under 15 seconds due to cancellations }); }); }); describe('Cancellation Timing and Precision', () => { describe('AC 5.7: Cancellation timing accuracy', () => { it('should cancel within timing tolerance of ±500ms', async () => { // Given: Command with waitToCancelMs: 5000 (5 seconds) const commands: EnhancedCommandParameter[] = [ { initiator: 'browser', command: 'sleep 15', cancel: true, waitToCancelMs: 5000 } ]; const startTime = performance.now(); // When: Villenele executes command with cancellation const results = await executor.executeCommands(commands, webSocket); const executionTime = performance.now() - startTime; // Then: Cancellation should occur within 5000ms ± 500ms tolerance expect(executionTime).toBeGreaterThanOrEqual(4500); // 5000ms - 500ms expect(executionTime).toBeLessThanOrEqual(5500); // 5000ms + 500ms // And: Timing should be measured from command start, not configuration time expect(results[0].executionStartTime).toBeDefined(); expect(results[0].executionEndTime).toBeDefined(); const commandExecutionTime = results[0].executionEndTime - results[0].executionStartTime; expect(commandExecutionTime).toBeGreaterThanOrEqual(4500); expect(commandExecutionTime).toBeLessThanOrEqual(5500); }); it('should maintain cancellation precision across multiple executions', async () => { // Test consistency across 3 executions const timings: number[] = []; for (let i = 0; i < 3; i++) { const commands: EnhancedCommandParameter[] = [ { initiator: 'browser', command: 'sleep 15', cancel: true, waitToCancelMs: 3000 } ]; const startTime = performance.now(); await executor.executeCommands(commands, webSocket); const executionTime = performance.now() - startTime; timings.push(executionTime); } // And: Cancellation precision should be consistent across multiple executions for (const timing of timings) { expect(timing).toBeGreaterThanOrEqual(2500); // 3000ms - 500ms expect(timing).toBeLessThanOrEqual(3500); // 3000ms + 500ms } // Variance should be reasonable (less than 1 second between fastest and slowest) const maxTiming = Math.max(...timings); const minTiming = Math.min(...timings); expect(maxTiming - minTiming).toBeLessThan(1000); }); }); describe('AC 5.8: Command completion before timeout', () => { it('should clear cancellation timer for commands that complete naturally', async () => { // Given: Fast command with long cancellation timeout const commands: EnhancedCommandParameter[] = [ { initiator: 'browser', command: 'echo "quick"', cancel: true, waitToCancelMs: 10000 // Long timeout - command should complete first } ]; const startTime = performance.now(); // When: Command completes naturally before timeout const results = await executor.executeCommands(commands, webSocket); const executionTime = performance.now() - startTime; // Then: Cancellation timer should be cleared expect(executionTime).toBeLessThan(2000); // Should complete quickly, not wait 10 seconds // And: No cancellation signal should be sent const capturedMessages = results[0].capturedMessages; const messageContent = capturedMessages.map(msg => JSON.stringify(msg)).join(' '); expect(messageContent).not.toContain('^C'); // Should NOT contain interrupt signal // And: Command should complete normally with expected output expect(messageContent).toContain('quick'); expect(results[0].success).toBe(true); }); }); describe('AC 5.9: Concurrent cancellation handling', () => { it('should handle multiple commands with different cancellation timers independently', async () => { // Given: Multiple commands with different cancellation timers // Note: This test simulates concurrent execution by running them in separate executor calls const shortCancelCommand: EnhancedCommandParameter[] = [ { initiator: 'browser', command: 'sleep 15', cancel: true, waitToCancelMs: 2000 } ]; const longCancelCommand: EnhancedCommandParameter[] = [ { initiator: 'mcp-client', command: 'ssh_exec {"sessionName": "story05-test-session", "command": "sleep 15"}', cancel: true, waitToCancelMs: 5000 } ]; // When: Different cancellation timeouts expire at overlapping times const shortCancelPromise = executor.executeCommands(shortCancelCommand, webSocket); // Start second command slightly after first (simulate overlap) await new Promise(resolve => setTimeout(resolve, 1000)); const longCancelPromise = executor.executeCommands(longCancelCommand, webSocket); const [shortResults, longResults] = await Promise.all([shortCancelPromise, longCancelPromise]); // Then: Each command should be cancelled independently expect(shortResults[0].waitToCancelMs).toBe(2000); expect(longResults[0].waitToCancelMs).toBe(5000); // And: Cancellation signals should not interfere with each other expect(shortResults[0].cancelRequested).toBe(true); expect(longResults[0].cancelRequested).toBe(true); // And: Session state should remain stable across concurrent cancellations // Verify with a follow-up command const followUpCommands: EnhancedCommandParameter[] = [ { initiator: 'browser', command: 'echo "session stable"' } ]; const followUpResults = await executor.executeCommands(followUpCommands, webSocket); expect(followUpResults[0].success).toBe(true); }); }); }); describe('Cancellation Response Validation', () => { describe('AC 5.10: Browser cancellation response capture', () => { it('should capture complete response for cancelled browser commands', async () => { // Given: Browser command cancelled after timeout const commands: EnhancedCommandParameter[] = [ { initiator: 'browser', command: 'sleep 30', cancel: true, waitToCancelMs: 2000 } ]; // When: WebSocket SIGINT signal is sent const results = await executor.executeCommands(commands, webSocket); // Then: concatenatedResponses should contain command echo const capturedMessages = results[0].capturedMessages; const messageContent = capturedMessages.map(msg => JSON.stringify(msg)).join(' '); expect(messageContent).toContain('sleep 30'); // Command echo // And: Contain partial command output (if any produced before cancellation) // Sleep typically doesn't produce output, but should capture any that exists // And: Contain interrupt indicator: ^C expect(messageContent).toContain('^C'); // And: Contain returned command prompt expect(messageContent).toMatch(/\[jsbattig@localhost.*\]\$/); }); }); describe('AC 5.11: MCP cancellation response capture', () => { it('should capture MCP cancellation response and confirmation', async () => { // Given: MCP command cancelled via ssh_cancel_command const commands: EnhancedCommandParameter[] = [ { initiator: 'mcp-client', command: 'sleep 15', cancel: true, waitToCancelMs: 3000 } ]; // When: Cancellation is executed const results = await executor.executeCommands(commands, webSocket); // Then: Response should contain command execution start expect(results[0].mcpResponse).toBeDefined(); expect(results[0].command).toBe('ssh_exec {"sessionName": "story05-test-session", "command": "sleep 15"}'); // And: Contain cancellation confirmation from MCP server // Implementation should track cancellation attempts expect(results[0].success).toBe(false); // Command was cancelled // And: Contain any partial output produced before cancellation // And: Maintain response format consistency for validation expect(results[0].executionStartTime).toBeDefined(); expect(results[0].executionEndTime).toBeDefined(); }); }); describe('AC 5.12: Post-cancellation session validation', () => { it('should maintain session integrity after command cancellations', async () => { // Given: Commands that have been cancelled via timeout mechanisms const cancelledCommands: EnhancedCommandParameter[] = [ { initiator: 'browser', command: 'sleep 20', cancel: true, waitToCancelMs: 2000 }, { initiator: 'mcp-client', command: 'ssh_exec {"sessionName": "story05-test-session", "command": "sleep 20"}', cancel: true, waitToCancelMs: 3000 } ]; await executor.executeCommands(cancelledCommands, webSocket); // When: Subsequent commands are executed in same session const subsequentCommands: EnhancedCommandParameter[] = [ { initiator: 'browser', command: 'pwd' }, { initiator: 'mcp-client', command: 'ssh_exec {"sessionName": "story05-test-session", "command": "whoami"}' }, { initiator: 'browser', command: 'echo "test after cancellation"' } ]; const results = await executor.executeCommands(subsequentCommands, webSocket); // Then: SSH session should remain active and functional expect(results).toHaveLength(3); expect(results[0].success).toBe(true); expect(results[1].success).toBe(true); expect(results[2].success).toBe(true); // And: Working directory should be preserved correctly const pwdMessages = results[0].capturedMessages.map(msg => JSON.stringify(msg)).join(' '); expect(pwdMessages).toContain('/home/jsbattig'); // Should show expected working directory // And: Environment state should be maintained across cancellations // Whoami command should work expect(results[1].mcpResponse?.success).toBe(true); // And: No session corruption should occur from interruptions const echoMessages = results[2].capturedMessages.map(msg => JSON.stringify(msg)).join(' '); expect(echoMessages).toContain('test after cancellation'); }); }); }); describe('Error Handling and Edge Cases', () => { describe('AC 5.13: Cancellation failure handling', () => { it('should handle cancellation mechanism failures gracefully', async () => { // This test requires mocking WebSocket disconnection or MCP error // For now, we'll test the error handling path by using invalid session // Given: Command with cancellation timeout const commands: EnhancedCommandParameter[] = [ { initiator: 'browser', command: 'sleep 10', cancel: true, waitToCancelMs: 2000 } ]; // When: Cancellation mechanism fails (simulated by closing WebSocket) webSocket.close(); try { const results = await executor.executeCommands(commands, webSocket); // Then: Villenele should detect cancellation failure expect(results[0].error).toBeDefined(); // And: Log specific error with reason expect(results[0].error).toContain('WebSocket'); // Should mention WebSocket connection issue } catch (error) { // And: Continue with next command or fail gracefully based on configuration expect(error).toBeDefined(); // And: Provide diagnostic information for troubleshooting cancellation issues expect((error as Error).message).toContain('WebSocket'); // Should provide diagnostic info } }); }); describe('AC 5.14: Invalid timeout value handling', () => { it('should reject zero or negative timeout values', async () => { // Given: Command with invalid timeout: waitToCancelMs: 0 const invalidCommands: EnhancedCommandParameter[] = [ { initiator: 'browser', command: 'pwd', cancel: true, waitToCancelMs: 0 // Invalid: zero timeout } ]; // When: Villenele validates the configuration // Then: Validation should reject zero or negative timeout values await expect(executor.executeCommands(invalidCommands, webSocket)).rejects.toThrow(); // Test negative value const negativeCommands: EnhancedCommandParameter[] = [ { initiator: 'browser', command: 'pwd', cancel: true, waitToCancelMs: -1000 // Invalid: negative timeout } ]; await expect(executor.executeCommands(negativeCommands, webSocket)).rejects.toThrow(); }); it('should provide helpful error messages for invalid timeouts', async () => { // Given: Command with invalid timeout const invalidCommands: EnhancedCommandParameter[] = [ { initiator: 'browser', command: 'pwd', cancel: true, waitToCancelMs: 500 // Invalid: too small (less than minimum 1000ms) } ]; try { await executor.executeCommands(invalidCommands, webSocket); fail('Should have thrown validation error'); } catch (error) { // And: Provide error: "waitToCancelMs must be positive value >= 1000" expect((error as Error).message).toContain('waitToCancelMs must be positive'); // And: Suggest minimum reasonable timeout value expect((error as Error).message).toContain('1000'); } }); }); describe('AC 5.15: Command already completed during cancellation attempt', () => { it('should handle race condition where command completes just as cancellation triggers', async () => { // Given: Command that completes just as cancellation timeout expires // Use a command that might complete around the cancellation time const commands: EnhancedCommandParameter[] = [ { initiator: 'browser', command: 'sleep 2', // 2 second sleep cancel: true, waitToCancelMs: 2000 // Cancel after 2 seconds - race condition } ]; // When: Cancellation signal is sent after command natural completion const results = await executor.executeCommands(commands, webSocket); // Then: Cancellation should be safely ignored // And: No error should be generated for attempting to cancel completed command expect(results[0].error).toBeUndefined(); // And: Final response should reflect natural completion, not interruption // const capturedMessages = results[0].capturedMessages; // Unused for now // const messageContent = capturedMessages.map(msg => JSON.stringify(msg)).join(' '); // Unused for now // Should not contain ^C if command completed naturally // Note: Due to race conditions, this might contain ^C, so we test for no error instead expect(results[0].error).toBeUndefined(); // And: Subsequent commands should execute normally const followUpCommands: EnhancedCommandParameter[] = [ { initiator: 'browser', command: 'echo "after race condition"' } ]; const followUpResults = await executor.executeCommands(followUpCommands, webSocket); expect(followUpResults[0].success).toBe(true); }); }); }); describe('Complex Cancellation Scenarios', () => { describe('AC 5.16: Nested command cancellation testing', () => { it('should interrupt entire command hierarchy including nested processes', async () => { // Given: Script execution with cancellation const commands: EnhancedCommandParameter[] = [ { initiator: 'browser', command: 'bash -c "sleep 30 && echo done"', cancel: true, waitToCancelMs: 5000 } ]; const startTime = performance.now(); // When: Cancellation timeout expires during nested script execution const results = await executor.executeCommands(commands, webSocket); const executionTime = performance.now() - startTime; // Then: Entire command hierarchy should be interrupted expect(executionTime).toBeLessThan(30000); // Should not wait for full sleep expect(executionTime).toBeGreaterThanOrEqual(4500); // Should wait roughly 5 seconds expect(executionTime).toBeLessThanOrEqual(5500); // And: Bash script and sleep command should both be terminated const capturedMessages = results[0].capturedMessages; const messageContent = capturedMessages.map(msg => JSON.stringify(msg)).join(' '); expect(messageContent).toContain('^C'); // Should show interruption expect(messageContent).not.toContain('done'); // Should not reach the echo // And: No orphaned processes should remain after cancellation // This is tested by ensuring session remains functional const cleanupCommands: EnhancedCommandParameter[] = [ { initiator: 'browser', command: 'echo "cleanup successful"' } ]; const cleanupResults = await executor.executeCommands(cleanupCommands, webSocket); expect(cleanupResults[0].success).toBe(true); }); }); describe('AC 5.17: Interactive nano editor cancellation', () => { it('should forcefully cancel nano editor and return to normal prompt', async () => { // Given: Browser command with nano editor cancellation const commands: EnhancedCommandParameter[] = [ { initiator: 'browser', command: 'nano /tmp/test.txt', cancel: true, waitToCancelMs: 4000 } ]; const startTime = performance.now(); // When: Nano editor is launched and timeout expires const results = await executor.executeCommands(commands, webSocket); const executionTime = performance.now() - startTime; // Then: Nano editor should be forcefully cancelled via WebSocket SIGINT expect(executionTime).toBeGreaterThanOrEqual(3500); // Should wait roughly 4 seconds expect(executionTime).toBeLessThanOrEqual(4500); // And: Terminal should show ^C interruption and exit nano cleanly const capturedMessages = results[0].capturedMessages; const messageContent = capturedMessages.map(msg => JSON.stringify(msg)).join(' '); expect(messageContent).toContain('^C'); // And: Terminal should return to normal command prompt without hanging in editor mode expect(messageContent).toMatch(/\[jsbattig@localhost.*\]\$/); // And: No orphaned nano processes should remain after cancellation const followUpCommands: EnhancedCommandParameter[] = [ { initiator: 'browser', command: 'echo "nano cancelled successfully"' } ]; const followUpResults = await executor.executeCommands(followUpCommands, webSocket); expect(followUpResults[0].success).toBe(true); }); }); describe('AC 5.18: Interactive command with user input cancellation', () => { it('should cancel interactive read command waiting for user input', async () => { // Given: Interactive command with cancellation const commands: EnhancedCommandParameter[] = [ { initiator: 'browser', command: 'read -p "Enter input: " userInput', cancel: true, waitToCancelMs: 3000 } ]; const startTime = performance.now(); // When: Command waits for user input and timeout expires const results = await executor.executeCommands(commands, webSocket); const executionTime = performance.now() - startTime; // Then: Interactive command should be cancelled via SIGINT expect(executionTime).toBeGreaterThanOrEqual(2500); // Should wait roughly 3 seconds expect(executionTime).toBeLessThanOrEqual(3500); // And: Input prompt should be interrupted const capturedMessages = results[0].capturedMessages; const messageContent = capturedMessages.map(msg => JSON.stringify(msg)).join(' '); expect(messageContent).toContain('^C'); // And: Terminal should return to normal command prompt expect(messageContent).toMatch(/\[jsbattig@localhost.*\]\$/); // And: No hanging input state should remain const followUpCommands: EnhancedCommandParameter[] = [ { initiator: 'browser', command: 'echo "input cancelled successfully"' } ]; const followUpResults = await executor.executeCommands(followUpCommands, webSocket); expect(followUpResults[0].success).toBe(true); }); }); describe('AC 5.19: Long sequence with multiple cancellations', () => { it('should handle extended sequence with multiple cancellation points', async () => { // Given: Extended sequence with multiple cancellation points const commands: EnhancedCommandParameter[] = [ { initiator: 'browser', command: 'echo "start"' }, { initiator: 'browser', command: 'sleep 20', cancel: true, waitToCancelMs: 2000 }, { initiator: 'browser', command: 'nano /tmp/test.txt', cancel: true, waitToCancelMs: 1500 }, { initiator: 'mcp-client', command: 'ssh_exec {"sessionName": "story05-test-session", "command": "sleep 15"}', cancel: true, waitToCancelMs: 3000 }, { initiator: 'mcp-client', command: 'ssh_exec {"sessionName": "story05-test-session", "command": "echo \"end\""}' } ]; const startTime = performance.now(); // When: Villenele executes the complete sequence with multiple cancellations const results = await executor.executeCommands(commands, webSocket); const executionTime = performance.now() - startTime; // Then: All cancellation timeouts should be respected independently expect(results).toHaveLength(5); // First command - normal execution expect(results[0].success).toBe(true); expect(results[0].cancelRequested).toBe(false); // Second command - cancelled after 2 seconds expect(results[1].cancelRequested).toBe(true); expect(results[1].waitToCancelMs).toBe(2000); // Third command - cancelled after 1.5 seconds expect(results[2].cancelRequested).toBe(true); expect(results[2].waitToCancelMs).toBe(1500); // Fourth command - cancelled after 3 seconds expect(results[3].cancelRequested).toBe(true); expect(results[3].waitToCancelMs).toBe(3000); // Fifth command - normal execution expect(results[4].success).toBe(true); expect(results[4].cancelRequested).toBe(false); // And: Sequence should continue after each cancellation // Total execution should be much less than 55 seconds (20+20+15 without cancellation) expect(executionTime).toBeLessThan(20000); // Should complete in under 20 seconds due to cancellations // And: Final command should execute normally after all interruptions const finalMessages = results[4].capturedMessages.map(msg => JSON.stringify(msg)).join(' '); expect(finalMessages).toContain('end'); // And: Session integrity should be maintained throughout expect(results[4].success).toBe(true); }); }); }); });

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