Skip to main content
Glama
masx200

Persistent Terminal MCP Server

by masx200
terminal-manager.test.ts12.2 kB
import { TerminalManager } from "../terminal-manager.js"; import { OutputBuffer } from "../output-buffer.js"; describe("TerminalManager", () => { let terminalManager: TerminalManager; beforeEach(() => { terminalManager = new TerminalManager({ maxBufferSize: 100, sessionTimeout: 5000, // 5 seconds for testing }); }); afterEach(async () => { await terminalManager.shutdown(); }); describe("Terminal Creation", () => { test("should create a new terminal session", async () => { const terminalId = await terminalManager.createTerminal(); expect(terminalId).toBeDefined(); expect(typeof terminalId).toBe("string"); const session = terminalManager.getTerminalInfo(terminalId); expect(session).toBeDefined(); expect(session?.status).toBe("active"); expect(session?.pid).toBeGreaterThan(0); }); test("should create terminal with custom options", async () => { const options = { cwd: process.cwd(), env: { TEST_VAR: "test_value" }, }; const terminalId = await terminalManager.createTerminal(options); const session = terminalManager.getTerminalInfo(terminalId); expect(session?.cwd).toBe(options.cwd); expect(session?.env.TEST_VAR).toBe("test_value"); }); }); describe("Terminal Operations", () => { let terminalId: string; beforeEach(async () => { terminalId = await terminalManager.createTerminal(); }); test("should write to terminal", async () => { await expect( terminalManager.writeToTerminal({ terminalId, input: "echo test\n", }), ).resolves.not.toThrow(); }); test("should support raw input without auto newline", async () => { const fakeId = "fake-terminal"; const fakeWrite = jest.fn(); const fakeSession = { id: fakeId, pid: 12345, shell: "/bin/bash", cwd: process.cwd(), env: {} as Record<string, string>, created: new Date(), lastActivity: new Date(), status: "active" as const, }; (terminalManager as any).ptyProcesses.set(fakeId, { write: fakeWrite }); (terminalManager as any).sessions.set(fakeId, fakeSession); await terminalManager.writeToTerminal({ terminalId: fakeId, input: "", appendNewline: false, }); expect(fakeWrite).toHaveBeenCalledWith(""); (terminalManager as any).ptyProcesses.delete(fakeId); (terminalManager as any).sessions.delete(fakeId); }); test("should avoid auto newline for control characters by default", async () => { const fakeId = "control-terminal"; const fakeWrite = jest.fn(); const fakeSession = { id: fakeId, pid: 12346, shell: "/bin/bash", cwd: process.cwd(), env: {} as Record<string, string>, created: new Date(), lastActivity: new Date(), status: "active" as const, }; (terminalManager as any).ptyProcesses.set(fakeId, { write: fakeWrite }); (terminalManager as any).sessions.set(fakeId, fakeSession); await terminalManager.writeToTerminal({ terminalId: fakeId, input: "", }); expect(fakeWrite).toHaveBeenCalledWith(""); (terminalManager as any).ptyProcesses.delete(fakeId); (terminalManager as any).sessions.delete(fakeId); }); test("should auto append newline for printable text by default", async () => { const fakeId = "printable-terminal"; const fakeWrite = jest.fn(); const fakeSession = { id: fakeId, pid: 12347, shell: "/bin/bash", cwd: process.cwd(), env: {} as Record<string, string>, created: new Date(), lastActivity: new Date(), status: "active" as const, }; (terminalManager as any).ptyProcesses.set(fakeId, { write: fakeWrite }); (terminalManager as any).sessions.set(fakeId, fakeSession); await terminalManager.writeToTerminal({ terminalId: fakeId, input: "npm --version", }); expect(fakeWrite).toHaveBeenCalledWith("npm --version\r"); (terminalManager as any).ptyProcesses.delete(fakeId); (terminalManager as any).sessions.delete(fakeId); }); test("should send carriage return when only newline requested", async () => { const fakeId = "enter-terminal"; const fakeWrite = jest.fn(); const fakeSession = { id: fakeId, pid: 22347, shell: "/bin/bash", cwd: process.cwd(), env: {} as Record<string, string>, created: new Date(), lastActivity: new Date(), status: "active" as const, }; (terminalManager as any).ptyProcesses.set(fakeId, { write: fakeWrite }); (terminalManager as any).sessions.set(fakeId, fakeSession); await terminalManager.writeToTerminal({ terminalId: fakeId, input: "", appendNewline: true, }); expect(fakeWrite).toHaveBeenCalledWith("\r"); (terminalManager as any).ptyProcesses.delete(fakeId); (terminalManager as any).sessions.delete(fakeId); }); test("should normalize explicit newline input to carriage return", async () => { const fakeId = "normalize-terminal"; const fakeWrite = jest.fn(); const fakeSession = { id: fakeId, pid: 22348, shell: "/bin/bash", cwd: process.cwd(), env: {} as Record<string, string>, created: new Date(), lastActivity: new Date(), status: "active" as const, }; (terminalManager as any).ptyProcesses.set(fakeId, { write: fakeWrite }); (terminalManager as any).sessions.set(fakeId, fakeSession); await terminalManager.writeToTerminal({ terminalId: fakeId, input: "\n", appendNewline: false, }); expect(fakeWrite).toHaveBeenCalledWith("\r"); (terminalManager as any).ptyProcesses.delete(fakeId); (terminalManager as any).sessions.delete(fakeId); }); test("should read from terminal", async () => { // Send a command await terminalManager.writeToTerminal({ terminalId, input: 'echo "test output"\n', }); // Wait a bit for command to execute await new Promise((resolve) => setTimeout(resolve, 500)); const result = await terminalManager.readFromTerminal({ terminalId }); expect(result).toBeDefined(); expect(typeof result.output).toBe("string"); expect(typeof result.totalLines).toBe("number"); expect(typeof result.hasMore).toBe("boolean"); expect(typeof result.cursor).toBe("number"); expect(result.status).toBeDefined(); expect(typeof result.status?.isRunning).toBe("boolean"); }); test("should list terminals", async () => { const result = await terminalManager.listTerminals(); expect(result.terminals).toBeDefined(); expect(Array.isArray(result.terminals)).toBe(true); expect(result.terminals.length).toBeGreaterThan(0); const terminal = result.terminals.find((t) => t.id === terminalId); expect(terminal).toBeDefined(); expect(terminal?.status).toBe("active"); }); test("should kill terminal", async () => { await terminalManager.killTerminal(terminalId); // Wait a bit for termination await new Promise((resolve) => setTimeout(resolve, 100)); const session = terminalManager.getTerminalInfo(terminalId); expect(session).toBeUndefined(); expect(terminalManager.isTerminalActive(terminalId)).toBe(false); }); test("should handle non-existent terminal", async () => { const fakeId = "non-existent-id"; await expect( terminalManager.writeToTerminal({ terminalId: fakeId, input: "test", }), ).rejects.toThrow(); await expect( terminalManager.readFromTerminal({ terminalId: fakeId }), ).rejects.toThrow(); await expect(terminalManager.killTerminal(fakeId)).rejects.toThrow(); }); }); describe("Manager Statistics", () => { test("should return manager stats", () => { const stats = terminalManager.getStats(); expect(stats).toBeDefined(); expect(typeof stats.activeSessions).toBe("number"); expect(typeof stats.totalSessions).toBe("number"); expect(typeof stats.totalBufferSize).toBe("number"); expect(stats.config).toBeDefined(); }); }); }); describe("OutputBuffer", () => { let buffer: OutputBuffer; beforeEach(() => { buffer = new OutputBuffer("test-terminal", 10); // Small buffer for testing }); test("should append and read content", () => { buffer.append("line 1\nline 2\nline 3"); const result = buffer.read(); expect(result.entries.length).toBe(3); expect(result.entries[0].content).toBe("line 1"); expect(result.entries[1].content).toBe("line 2"); expect(result.entries[2].content).toBe("line 3"); expect(result.nextCursor).toBeGreaterThan(0); }); test("should handle buffer overflow", () => { // Add more lines than buffer size for (let i = 0; i < 15; i++) { buffer.append(`line ${i}\n`); } const result = buffer.read(); expect(result.entries.length).toBeLessThanOrEqual(10); // Should contain the most recent lines const lastEntry = result.entries[result.entries.length - 1]; expect(lastEntry.content).toBe("line 14"); }); test("should support incremental reading", () => { buffer.append("line 1\nline 2"); const result1 = buffer.read(); buffer.append("\nline 3\nline 4\n"); const result2 = buffer.read({ since: result1.nextCursor }); expect(result2.entries.length).toBe(2); expect(result2.entries[0].content).toBe("line 3"); expect(result2.entries[1].content).toBe("line 4"); }); test("should get latest content", () => { for (let i = 0; i < 15; i++) { buffer.append(`line ${i}\n`); } const latest = buffer.getLatest(3); expect(latest.length).toBe(3); expect(latest[2].content).toBe("line 14"); }); test("should clear buffer", () => { buffer.append("test content"); buffer.clear(); const result = buffer.read(); expect(result.entries.length).toBe(0); expect(result.totalLines).toBe(0); }); test("should provide buffer statistics", () => { buffer.append("line 1\nline 2\nline 3"); const stats = buffer.getStats(); expect(stats.terminalId).toBe("test-terminal"); expect(stats.totalLines).toBe(3); expect(stats.bufferedLines).toBe(3); expect(stats.maxSize).toBe(10); }); test("should treat carriage returns as line rewrites", () => { buffer.append("⠋ Installing dependencies"); buffer.append("\r⠙ Installing dependencies"); buffer.append("\r⠹ Installing dependencies\nDone!\n"); const result = buffer.read(); expect(result.entries.map((entry) => entry.content)).toEqual([ "⠹ Installing dependencies", "Done!", ]); }); test("should collapse consecutive blank lines", () => { buffer.append("\n\n\n"); const result = buffer.read(); expect(result.entries.length).toBe(1); expect(result.entries[0].content).toBe(""); }); test("should strip ansi color sequences", () => { buffer.append("Hello World\n"); const result = buffer.read(); expect(result.entries.map((entry) => entry.content)).toEqual([ "Hello World", ]); }); test("should handle cursor movement escape sequences", () => { buffer.append("⠋ Step 1"); buffer.append("\r"); buffer.append("⠙ Step 2\n"); const result = buffer.read(); expect(result.entries.map((entry) => entry.content)).toEqual(["⠙ Step 2"]); }); test("should expose updated lines when using cursor-based reads", () => { buffer.append("bash-3.2$ "); const first = buffer.read(); buffer.append("npm install\r\n"); const second = buffer.read({ since: first.nextCursor }); const lines = second.entries.map((entry) => entry.content); expect(lines).toContain("bash-3.2$ npm install"); }); });

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/masx200/persistent-terminal-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server