Skip to main content
Glama
command-duplication-fix.test.ts11.6 kB
/** * COMMAND DUPLICATION FIX - TDD Test Suite * * Tests to reproduce and verify fix for the fundamental architectural flaw: * Commands are executed TWICE on the same SSH session: * 1. MCP path: handleSSHExec() → sshManager.executeCommand() * 2. Browser path: handleTerminalInputMessage() → sshManager.executeCommand() * * This test suite follows TDD methodology to: * 1. Write failing tests that demonstrate duplication * 2. Implement state machine to prevent duplication * 3. Verify commands execute exactly once per invocation */ import { MCPSSHServer } from '../src/mcp-ssh-server.js'; import { WebServerManager } from '../src/web-server-manager.js'; import { SSHConnectionManager } from '../src/ssh-connection-manager.js'; import { beforeAll, afterAll, beforeEach, describe, it, expect, jest } from '@jest/globals'; import * as WebSocket from 'ws'; describe('Command Duplication Fix - TDD Test Suite', () => { let mcpServer: MCPSSHServer; let webServerManager: WebServerManager; let sshManager: SSHConnectionManager; let mockExecuteCommand: any; const TEST_SESSION_NAME = 'duplicate-test-session'; const TEST_COMMAND = 'echo "test-duplication"'; beforeAll(async () => { // Initialize components with shared SSH manager and state manager sshManager = new SSHConnectionManager(); mcpServer = new MCPSSHServer({}, sshManager); // Share the same terminal state manager between MCP and web server const sharedStateManager = mcpServer.getTerminalStateManager(); webServerManager = new WebServerManager(sshManager, {}, sharedStateManager); // Mock the executeCommand method to track invocations mockExecuteCommand = jest.spyOn(sshManager, 'executeCommand') .mockResolvedValue({ stdout: 'test-duplication\n', stderr: '', exitCode: 0 }); // Mock hasSession to return true for our test session jest.spyOn(sshManager, 'hasSession').mockReturnValue(true); // Start servers await mcpServer.start(); await webServerManager.start(); }); afterAll(async () => { await mcpServer.stop(); await webServerManager.stop(); jest.restoreAllMocks(); }); beforeEach(() => { // Clear mock invocations before each test mockExecuteCommand.mockClear(); }); describe('FAILING TESTS - Demonstrate Current Duplication Issue', () => { it('should demonstrate command duplication when MCP and browser both execute same command', async () => { // This test demonstrates concurrent command execution attempts // The state machine should prevent this duplication // Mock executeCommand to be slow so we can test concurrency mockExecuteCommand.mockImplementationOnce(() => new Promise(resolve => setTimeout(() => resolve({ stdout: 'test-duplication\n', stderr: '', exitCode: 0 }), 50)) ); // Start MCP command execution (don't await) const mcpPromise = mcpServer.callTool('ssh_exec', { sessionName: TEST_SESSION_NAME, command: TEST_COMMAND }); // Immediately try browser command execution of SAME command const mockWebSocket = { readyState: WebSocket.OPEN, send: jest.fn(), OPEN: WebSocket.OPEN } as unknown as WebSocket; const browserMessage = { type: 'terminal_input', sessionName: TEST_SESSION_NAME, command: TEST_COMMAND, commandId: 'browser-cmd-123' }; // Simulate browser message (this should be rejected by state machine) await (webServerManager as any).handleTerminalInputMessage( mockWebSocket, browserMessage, TEST_SESSION_NAME ); // Wait for MCP command to complete const mcpResult = await mcpPromise; expect(mcpResult).toHaveProperty('success', true); // Browser command should be rejected, only MCP should execute expect(mockExecuteCommand).toHaveBeenCalledTimes(1); expect(mockExecuteCommand).toHaveBeenCalledWith( TEST_SESSION_NAME, TEST_COMMAND, expect.objectContaining({ source: 'claude' }) ); // Browser should have received rejection message expect(mockWebSocket.send).toHaveBeenCalledWith( expect.stringContaining('Session is busy') ); }); it('should demonstrate concurrent command execution attempts', async () => { // This test shows what happens when MCP and browser try to execute simultaneously // Both should not execute - state machine should prevent overlap // Start MCP command execution (don't await) const mcpPromise = mcpServer.callTool('ssh_exec', { sessionName: TEST_SESSION_NAME, command: 'sleep 1 && echo "mcp-command"' }); // Immediately try browser command execution const mockWebSocket = { readyState: WebSocket.OPEN, send: jest.fn(), OPEN: WebSocket.OPEN } as unknown as WebSocket; const browserMessage = { type: 'terminal_input', sessionName: TEST_SESSION_NAME, command: 'echo "browser-command"', commandId: 'browser-cmd-456' }; // This should be rejected due to state machine (when implemented) const browserPromise = (webServerManager as any).handleTerminalInputMessage( mockWebSocket, browserMessage, TEST_SESSION_NAME ); // Wait for both to complete await Promise.all([mcpPromise, browserPromise]); // CURRENT BROKEN BEHAVIOR: Both commands execute (this will FAIL) // After fix: Only first command should execute, second should be rejected expect(mockExecuteCommand).toHaveBeenCalledTimes(1); // This will FAIL - currently called twice }); it('should demonstrate session state is not tracked between command executions', async () => { // This test shows that sessions have no state tracking mechanism // Commands can be executed without checking if another command is running // Execute first command await mcpServer.callTool('ssh_exec', { sessionName: TEST_SESSION_NAME, command: 'echo "first-command"' }); expect(mockExecuteCommand).toHaveBeenCalledTimes(1); // Execute second command immediately (should work - sequential execution) await mcpServer.callTool('ssh_exec', { sessionName: TEST_SESSION_NAME, command: 'echo "second-command"' }); expect(mockExecuteCommand).toHaveBeenCalledTimes(2); // The issue is that there's no coordination between MCP and browser paths // Both can execute simultaneously causing duplication and race conditions // This test passes but demonstrates the lack of state coordination // State machine will add proper session state tracking }); }); describe('STATE MACHINE REQUIREMENTS - Will Pass After Implementation', () => { it('should prevent browser command execution when MCP command is running', async () => { // This test defines expected behavior after state machine implementation // Currently will fail, should pass after implementation // Mock a long-running MCP command mockExecuteCommand.mockImplementationOnce(() => new Promise(resolve => setTimeout(() => resolve({ stdout: 'long-command-result\n', stderr: '', exitCode: 0 }), 100)) ); // Start MCP command (don't await) const mcpPromise = mcpServer.callTool('ssh_exec', { sessionName: TEST_SESSION_NAME, command: 'sleep 1 && echo "long-mcp-command"' }); // Try browser command while MCP is running const mockWebSocket = { readyState: WebSocket.OPEN, send: jest.fn(), OPEN: WebSocket.OPEN } as unknown as WebSocket; const browserMessage = { type: 'terminal_input', sessionName: TEST_SESSION_NAME, command: 'echo "should-be-rejected"', commandId: 'browser-cmd-789' }; await (webServerManager as any).handleTerminalInputMessage( mockWebSocket, browserMessage, TEST_SESSION_NAME ); // Browser command should be rejected via WebSocket error message expect(mockWebSocket.send).toHaveBeenCalledWith( expect.stringContaining('Session is busy') ); // Wait for MCP command to complete const mcpResult = await mcpPromise; expect(mcpResult).toHaveProperty('success', true); // Only MCP command should have executed expect(mockExecuteCommand).toHaveBeenCalledTimes(1); }); it('should prevent MCP command execution when browser command is running', async () => { // This test defines expected behavior after state machine implementation // Mock browser command execution to be long-running mockExecuteCommand.mockImplementationOnce(() => new Promise(resolve => setTimeout(() => resolve({ stdout: 'browser-command-result\n', stderr: '', exitCode: 0 }), 100)) ); const mockWebSocket = { readyState: WebSocket.OPEN, send: jest.fn(), OPEN: WebSocket.OPEN } as unknown as WebSocket; // Start browser command (don't await) const browserMessage = { type: 'terminal_input', sessionName: TEST_SESSION_NAME, command: 'sleep 1 && echo "long-browser-command"', commandId: 'browser-cmd-999' }; const browserPromise = (webServerManager as any).handleTerminalInputMessage( mockWebSocket, browserMessage, TEST_SESSION_NAME ); // Try MCP command while browser is running const mcpResult = await mcpServer.callTool('ssh_exec', { sessionName: TEST_SESSION_NAME, command: 'echo "should-be-rejected"' }); // MCP command should be rejected expect(mcpResult).toHaveProperty('success', false); expect(mcpResult).toHaveProperty('error', 'SESSION_BUSY'); // Wait for browser command to complete await browserPromise; // Only browser command should have executed expect(mockExecuteCommand).toHaveBeenCalledTimes(1); }); it('should allow sequential command execution after completion', async () => { // This test verifies proper state transitions // Execute MCP command and wait for completion await mcpServer.callTool('ssh_exec', { sessionName: TEST_SESSION_NAME, command: 'echo "first-sequential"' }); expect(mockExecuteCommand).toHaveBeenCalledTimes(1); // Execute browser command after MCP completion const mockWebSocket = { readyState: WebSocket.OPEN, send: jest.fn(), OPEN: WebSocket.OPEN } as unknown as WebSocket; const browserMessage = { type: 'terminal_input', sessionName: TEST_SESSION_NAME, command: 'echo "second-sequential"', commandId: 'sequential-cmd-123' }; await (webServerManager as any).handleTerminalInputMessage( mockWebSocket, browserMessage, TEST_SESSION_NAME ); // Both commands should execute sequentially expect(mockExecuteCommand).toHaveBeenCalledTimes(2); // Verify no error messages sent to browser expect(mockWebSocket.send).not.toHaveBeenCalledWith( expect.stringContaining('error') ); }); }); });

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