Skip to main content
Glama
story2-websocket-message-enhancement.test.ts12.4 kB
import { SSHConnectionManager } from "../src/ssh-connection-manager"; import { WebServerManager } from "../src/web-server-manager"; import { SSHConnectionConfig } from "../src/types"; import WebSocket from 'ws'; describe("Story 2: WebSocket Message Enhancement - Terminal Input Handling", () => { let sshManager: SSHConnectionManager; let webServer: WebServerManager; let webPort: number; let testSessionName: string; beforeEach(async () => { sshManager = new SSHConnectionManager(); webServer = new WebServerManager(sshManager); testSessionName = `test-session-${Date.now()}`; await webServer.start(); webPort = await webServer.getPort(); }); afterEach(async () => { await webServer.stop(); sshManager.cleanup(); }); describe("AC2.1: User Command Message Handling", () => { it("should receive terminal_input messages via WebSocket", async () => { // Arrange: Create SSH session const sshConfig: SSHConnectionConfig = { name: testSessionName, host: "localhost", username: "test_user", password: "password123" }; await sshManager.createConnection(sshConfig); // Act & Assert: Connect to WebSocket and send terminal_input message return new Promise<void>((resolve, reject) => { const ws = new WebSocket(`ws://localhost:${webPort}/ws/session/${testSessionName}`); ws.on('open', () => { const testMessage = { type: 'terminal_input', sessionName: testSessionName, command: 'echo "test command"', commandId: 'cmd_123' }; ws.send(JSON.stringify(testMessage)); }); ws.on('message', (data) => { const message = JSON.parse(data.toString()); // Should receive terminal_output response indicating command was processed if (message.type === 'terminal_output' && message.data) { ws.close(); resolve(); } }); ws.on('error', (error) => { ws.close(); reject(error); }); // Timeout after 10 seconds setTimeout(() => { ws.close(); reject(new Error('Test timeout: WebSocket terminal_input handling not working')); }, 10000); }); }, 15000); it("should extract command from terminal_input message", async () => { // Arrange: Create SSH session const sshConfig: SSHConnectionConfig = { name: testSessionName, host: "localhost", username: "test_user", password: "password123" }; await sshManager.createConnection(sshConfig); // This test will fail initially because terminal_input handler doesn't exist yet const ws = new WebSocket(`ws://localhost:${webPort}/ws/session/${testSessionName}`); return new Promise<void>((resolve, reject) => { let commandExecuted = false; ws.on('open', () => { const testCommand = 'pwd'; const testMessage = { type: 'terminal_input', sessionName: testSessionName, command: testCommand, commandId: 'cmd_456' }; ws.send(JSON.stringify(testMessage)); }); ws.on('message', (data) => { const message = JSON.parse(data.toString()); if (message.type === 'terminal_output') { // Verify the command was executed (should see pwd output) if (message.data && typeof message.data === 'string' && message.data.includes('/')) { commandExecuted = true; ws.close(); resolve(); } } }); ws.on('close', () => { if (!commandExecuted) { reject(new Error('Command was not executed via terminal_input message')); } }); ws.on('error', (error) => { reject(error); }); setTimeout(() => { ws.close(); if (!commandExecuted) { reject(new Error('Test timeout: Command extraction from terminal_input not working')); } }, 8000); }); }, 15000); it("should forward command to SSH manager when receiving terminal_input", async () => { // Arrange: Create SSH session with spy on executeCommand const sshConfig: SSHConnectionConfig = { name: testSessionName, host: "localhost", username: "test_user", password: "password123" }; await sshManager.createConnection(sshConfig); // Spy on executeCommand method to verify it gets called const executeCommandSpy = jest.spyOn(sshManager, 'executeCommand'); const ws = new WebSocket(`ws://localhost:${webPort}/ws/session/${testSessionName}`); return new Promise<void>((resolve, reject) => { ws.on('open', () => { const testMessage = { type: 'terminal_input', sessionName: testSessionName, command: 'ls -la', commandId: 'cmd_789' }; ws.send(JSON.stringify(testMessage)); // Check if executeCommand was called after a short delay setTimeout(() => { if (executeCommandSpy.mock.calls.length > 0) { const [sessionName, command] = executeCommandSpy.mock.calls[0]; expect(sessionName).toBe(testSessionName); expect(command).toBe('ls -la'); ws.close(); resolve(); } else { ws.close(); reject(new Error('executeCommand was not called when terminal_input was received')); } }, 1000); }); ws.on('error', (error) => { reject(error); }); setTimeout(() => { ws.close(); reject(new Error('Test timeout: terminal_input forwarding not working')); }, 8000); }); }, 15000); it("should mark commands as user-initiated when received via terminal_input", async () => { // This test will fail initially - we need to implement source tracking const sshConfig: SSHConnectionConfig = { name: testSessionName, host: "localhost", username: "test_user", password: "password123" }; await sshManager.createConnection(sshConfig); const ws = new WebSocket(`ws://localhost:${webPort}/ws/session/${testSessionName}`); return new Promise<void>((resolve, reject) => { ws.on('open', () => { const testMessage = { type: 'terminal_input', sessionName: testSessionName, command: 'echo "user command"', commandId: 'cmd_user_1' }; ws.send(JSON.stringify(testMessage)); }); ws.on('message', (data) => { const message = JSON.parse(data.toString()); if (message.type === 'terminal_output' && message.source === 'user') { ws.close(); resolve(); } }); ws.on('error', (error) => { ws.close(); reject(error); }); setTimeout(() => { ws.close(); reject(new Error('Test timeout: user-initiated source marking not working')); }, 8000); }); }, 15000); }); describe("AC2.4: Session Validation", () => { it("should validate SSH session exists before processing terminal_input", async () => { const nonExistentSession = "non-existent-session"; return new Promise<void>((resolve, reject) => { // Try to connect to WebSocket for non-existent session const ws = new WebSocket(`ws://localhost:${webPort}/ws/session/${nonExistentSession}`); ws.on('open', () => { // This should not happen for non-existent session ws.close(); reject(new Error('WebSocket connection should have been rejected for non-existent session')); }); ws.on('error', () => { // Expected - connection should fail for non-existent session resolve(); }); ws.on('close', (code) => { // Connection closed without opening - expected behavior if (code !== 1000) { // Not a normal close resolve(); } }); setTimeout(() => { reject(new Error('Test timeout: session validation not working')); }, 5000); }); }, 10000); it("should provide clear error message when session does not exist", async () => { // Create a session, then connect WebSocket, then delete session to test error handling const sshConfig: SSHConnectionConfig = { name: testSessionName, host: "localhost", username: "test_user", password: "password123" }; await sshManager.createConnection(sshConfig); const ws = new WebSocket(`ws://localhost:${webPort}/ws/session/${testSessionName}`); return new Promise<void>((resolve, reject) => { ws.on('open', async () => { // Disconnect the SSH session to simulate session loss await sshManager.disconnectSession(testSessionName); // Try to send terminal_input after session is disconnected const testMessage = { type: 'terminal_input', sessionName: testSessionName, command: 'echo "test"', commandId: 'cmd_error_test' }; ws.send(JSON.stringify(testMessage)); }); ws.on('message', (data) => { const message = JSON.parse(data.toString()); if (message.type === 'error' && message.message) { expect(message.message).toContain('session'); ws.close(); resolve(); } }); ws.on('error', () => { // This could also be expected behavior ws.close(); resolve(); }); setTimeout(() => { ws.close(); reject(new Error('Test timeout: error message handling not working')); }, 8000); }); }, 15000); it("should maintain session state during WebSocket communication", async () => { const sshConfig: SSHConnectionConfig = { name: testSessionName, host: "localhost", username: "test_user", password: "password123" }; await sshManager.createConnection(sshConfig); const ws = new WebSocket(`ws://localhost:${webPort}/ws/session/${testSessionName}`); return new Promise<void>((resolve, reject) => { let commandCount = 0; const expectedCommands = 2; ws.on('open', () => { // Send first command ws.send(JSON.stringify({ type: 'terminal_input', sessionName: testSessionName, command: 'export TEST_VAR="hello"', commandId: 'cmd_state_1' })); }); ws.on('message', (data) => { const message = JSON.parse(data.toString()); if (message.type === 'terminal_output') { commandCount++; if (commandCount === 1) { // Send second command that depends on first ws.send(JSON.stringify({ type: 'terminal_input', sessionName: testSessionName, command: 'echo $TEST_VAR', commandId: 'cmd_state_2' })); } else if (commandCount === expectedCommands) { // Verify that session state was maintained if (message.data && message.data.includes('hello')) { ws.close(); resolve(); } else { ws.close(); reject(new Error('Session state was not maintained between commands')); } } } }); ws.on('error', (error) => { ws.close(); reject(error); }); setTimeout(() => { ws.close(); reject(new Error('Test timeout: session state maintenance not working')); }, 12000); }); }, 15000); }); });

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