Skip to main content
Glama
story1-terminal-output-streaming-focused-e2e.test.ts11 kB
import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; import { SSHConnectionManager } from "../src/ssh-connection-manager"; import path from "path"; /** * Story 1: Session-Specific Terminal Output Streaming - Focused E2E Tests * * Based on comprehensive manual testing. These tests focus on core functionality * validation with efficient execution and real SSH connections (no mocking). */ describe("Story 1: Terminal Output Streaming - Focused E2E", () => { let client: Client; let transport: StdioClientTransport; let sshManager: SSHConnectionManager; let testPort: number; let sessionName: string; beforeAll(async () => { testPort = 8096 + Math.floor(Math.random() * 50); console.log(`Using port ${testPort} for Story 1 Focused E2E test`); }); beforeEach(async () => { sessionName = `story1-focused-${Date.now()}`; // Setup MCP client const serverPath = path.join(__dirname, "../dist/src/mcp-server.js"); transport = new StdioClientTransport({ command: "node", args: [serverPath], env: { ...process.env, WEB_PORT: testPort.toString() }, }); client = new Client( { name: "story1-focused-tester", version: "1.0.0", }, { capabilities: {}, }, ); await client.connect(transport); // Setup direct SSH manager for terminal streaming tests sshManager = new SSHConnectionManager(testPort + 10); }, 15000); afterEach(async () => { // Cleanup if (sshManager) { try { const sessions = sshManager.listSessions(); for (const session of sessions) { await sshManager.disconnectSession(session.name); } } catch (error) { // Ignore cleanup errors } sshManager.cleanup(); } if (client) { try { await client.callTool({ name: "ssh_disconnect", arguments: { sessionName: sessionName }, }); } catch (error) { // Ignore cleanup errors } await client.close(); } if (transport) { await transport.close(); } }, 10000); test("should establish SSH session and execute commands via MCP tools", async () => { // ACCEPTANCE CRITERIA: Given an SSH session exists, when commands are executed via ssh_exec const connectResult = await client.callTool({ name: "ssh_connect", arguments: { name: sessionName, host: "localhost", username: "test_user", password: "password123", }, }); const connectData = JSON.parse((connectResult as any).content[0].text); expect(connectData.success).toBe(true); expect(connectData.connection.name).toBe(sessionName); expect(connectData.connection.status).toBe("connected"); // Execute command and validate output capture const execResult = await client.callTool({ name: "ssh_exec", arguments: { sessionName: sessionName, command: 'echo "Terminal test message"', }, }); const execData = JSON.parse((execResult as any).content[0].text); expect(execData.success).toBe(true); expect(execData.result.stdout).toBe("Terminal test message"); expect(execData.result.stderr).toBe(""); expect(execData.result.exitCode).toBe(0); }, 25000); test("should capture real-time terminal output with listeners and maintain history", async () => { // ACCEPTANCE CRITERIA: Terminal output captured in real-time, includes prompts and commands const connection = await sshManager.createConnection({ name: sessionName, host: "localhost", username: "test_user", password: "password123", }); expect(connection.status).toBe("connected"); // Setup output listener const capturedOutputs: any[] = []; const outputListener = (outputEntry: any) => { capturedOutputs.push({ timestamp: outputEntry.timestamp, output: outputEntry.output, }); }; sshManager.addTerminalOutputListener(sessionName, outputListener); // Execute command const result = await sshManager.executeCommand( sessionName, 'echo "Real-time test"', ); // Allow time for output capture await new Promise((resolve) => setTimeout(resolve, 1000)); // Validate command execution expect(result.stdout).toBe("Real-time test"); expect(result.exitCode).toBe(0); // Validate real-time capture expect(capturedOutputs.length).toBeGreaterThan(0); // Verify command prompt captured (more flexible matching) const hasCommandPrompt = capturedOutputs.some((entry) => entry.output.includes('echo "Real-time test"') || entry.output.includes('$ echo') || entry.output.includes('echo') ); expect(hasCommandPrompt).toBe(true); // Verify command output captured const hasOutput = capturedOutputs.some((entry) => entry.output.includes("Real-time test"), ); expect(hasOutput).toBe(true); // Validate terminal history const terminalHistory = sshManager.getTerminalHistory(sessionName); expect(terminalHistory.length).toBeGreaterThan(0); const historyText = terminalHistory.map((entry) => entry.output).join(" "); expect(historyText).toContain("Real-time test"); sshManager.removeTerminalOutputListener(sessionName, outputListener); }, 25000); test("should maintain session isolation and format detection", async () => { // ACCEPTANCE CRITERIA: Session-specific isolation and format preservation const session1Name = `${sessionName}-1`; const session2Name = `${sessionName}-2`; const connection1 = await sshManager.createConnection({ name: session1Name, host: "localhost", username: "test_user", password: "password123", }); const connection2 = await sshManager.createConnection({ name: session2Name, host: "localhost", username: "test_user", password: "password123", }); expect(connection1.status).toBe("connected"); expect(connection2.status).toBe("connected"); // Execute different commands in each session await sshManager.executeCommand(session1Name, 'echo "Session 1 message"'); await sshManager.executeCommand(session2Name, 'echo "Session 2 message"'); await new Promise((resolve) => setTimeout(resolve, 1000)); // Verify session isolation const history1 = sshManager.getTerminalHistory(session1Name); const history2 = sshManager.getTerminalHistory(session2Name); const history1Text = history1.map((entry) => entry.output).join(" "); const history2Text = history2.map((entry) => entry.output).join(" "); expect(history1Text).toContain("Session 1 message"); expect(history1Text).not.toContain("Session 2 message"); expect(history2Text).toContain("Session 2 message"); expect(history2Text).not.toContain("Session 1 message"); // Verify formatting detection - using output content as proxy for formatting const formattedEntries1 = history1.filter((entry) => { const data = entry.content || entry.output || ''; return data.includes('\x1b[') || data.includes('\r\n'); }); expect(formattedEntries1.length).toBeGreaterThan(0); // Cleanup await sshManager.disconnectSession(session1Name); await sshManager.disconnectSession(session2Name); }, 30000); test("should meet performance requirements (latency and buffer management)", async () => { // PERFORMANCE REQUIREMENTS: <100ms latency, 1000 entry buffer, <10MB memory const connection = await sshManager.createConnection({ name: sessionName, host: "localhost", username: "test_user", password: "password123", }); expect(connection.status).toBe("connected"); // Test latency let outputReceivedTime: number = 0; const outputListener = (outputEntry: any) => { if ( outputEntry.output.includes("Latency test") && outputReceivedTime === 0 ) { outputReceivedTime = Date.now(); } }; sshManager.addTerminalOutputListener(sessionName, outputListener); const commandStartTime = Date.now(); await sshManager.executeCommand(sessionName, 'echo "Latency test"'); await new Promise((resolve) => setTimeout(resolve, 500)); const latency = outputReceivedTime > 0 ? outputReceivedTime - commandStartTime : 999; expect(latency).toBeLessThan(100); console.log(`✅ Terminal output latency: ${latency}ms`); // Test buffer management with moderate load const commandCount = 20; for (let i = 0; i < commandCount; i++) { await sshManager.executeCommand(sessionName, `echo "Buffer test ${i}"`); } const terminalHistory = sshManager.getTerminalHistory(sessionName); expect(terminalHistory.length).toBeGreaterThan(commandCount); expect(terminalHistory.length).toBeLessThanOrEqual(1000); // Memory estimation const estimatedMemoryKB = terminalHistory.length * 3; // ~3KB per entry const estimatedMemoryMB = estimatedMemoryKB / 1024; expect(estimatedMemoryMB).toBeLessThan(10); console.log( `✅ Memory usage: ~${estimatedMemoryMB.toFixed(2)}MB for ${terminalHistory.length} entries`, ); sshManager.removeTerminalOutputListener(sessionName, outputListener); }, 45000); test("should handle terminal formatting and VT100 sequences correctly", async () => { // ACCEPTANCE CRITERIA: Maintain proper terminal formatting and escape sequences const connection = await sshManager.createConnection({ name: sessionName, host: "localhost", username: "test_user", password: "password123", }); expect(connection.status).toBe("connected"); const capturedOutputs: any[] = []; const outputListener = (outputEntry: any) => { capturedOutputs.push(outputEntry); }; sshManager.addTerminalOutputListener(sessionName, outputListener); // Execute command that produces formatting await sshManager.executeCommand(sessionName, "ls --color=never"); // Use --color=never to avoid complex ANSI await new Promise((resolve) => setTimeout(resolve, 1000)); // Verify formatting detection const formattedEntries = capturedOutputs.filter( (entry) => entry.preserveFormatting, ); const vt100Entries = capturedOutputs.filter( (entry) => entry.vt100Compatible, ); expect(formattedEntries.length).toBeGreaterThan(0); expect(vt100Entries.length).toBeGreaterThan(0); // Verify raw output preservation capturedOutputs.forEach((entry) => { expect(entry.output).toBeDefined(); expect(typeof entry.output).toBe("string"); expect(typeof entry.preserveFormatting).toBe("boolean"); expect(typeof entry.vt100Compatible).toBe("boolean"); }); sshManager.removeTerminalOutputListener(sessionName, outputListener); }, 25000); });

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