Skip to main content
Glama
masx200

Persistent Terminal MCP Server

by masx200
spinner-detection.test.ts9.13 kB
import { OutputBuffer } from "../output-buffer.js"; describe("Spinner Detection and Compaction", () => { describe("With compactAnimations enabled", () => { let buffer: OutputBuffer; beforeEach(() => { buffer = new OutputBuffer("test-terminal", 100, { compactAnimations: true, animationThrottleMs: 50, }); }); test("should detect spinner characters", () => { // Simulate spinner animation frames buffer.append("⠋ Installing dependencies"); buffer.append("\r⠙ Installing dependencies"); buffer.append("\r⠹ Installing dependencies"); buffer.append("\r⠸ Installing dependencies"); buffer.append("\r⠼ Installing dependencies"); buffer.append("\r⠴ Installing dependencies"); buffer.append("\r⠦ Installing dependencies"); buffer.append("\r⠧ Installing dependencies\n"); // Wait for throttle to flush return new Promise<void>((resolve) => { setTimeout(() => { const result = buffer.read(); // Should have compacted the spinner updates expect(result.entries.length).toBeLessThan(8); // The last entry should contain the final spinner state or a compact representation const lastEntry = result.entries[result.entries.length - 1]; expect(lastEntry).toBeDefined(); resolve(); }, 100); }); }); test("should preserve non-spinner content", () => { buffer.append("Starting installation...\n"); buffer.append("⠋ Installing dependencies\r"); buffer.append("⠙ Installing dependencies\r"); buffer.append("⠹ Installing dependencies\n"); buffer.append("Installation complete!\n"); return new Promise<void>((resolve) => { setTimeout(() => { const result = buffer.read(); const contents = result.entries.map((e) => e.content); // Should preserve the non-spinner lines expect(contents).toContain("Starting installation..."); expect(contents).toContain("Installation complete!"); resolve(); }, 100); }); }); test("should handle mixed spinner types", () => { // Test different spinner characters buffer.append("◐ Loading\r"); buffer.append("◓ Loading\r"); buffer.append("◑ Loading\r"); buffer.append("◒ Loading\n"); return new Promise<void>((resolve) => { setTimeout(() => { const result = buffer.read(); // Should compact the spinner updates expect(result.entries.length).toBeLessThan(4); resolve(); }, 100); }); }); test("should flush spinner on non-spinner content", () => { buffer.append("⠋ Processing\r"); buffer.append("⠙ Processing\r"); buffer.append("⠹ Processing\r"); buffer.append("Done!\n"); return new Promise<void>((resolve) => { setTimeout(() => { const result = buffer.read(); const contents = result.entries.map((e) => e.content); // Should have the final output expect(contents).toContain("Done!"); resolve(); }, 100); }); }); test("should preserve npm install style summaries", () => { buffer.append("bash-3.2$ npm install\n"); buffer.append("up to date, audited 123 packages in 5s\n"); buffer.append("\u001b[1G\u001b[0K⠙"); buffer.append("\u001b[1G\u001b[0K"); buffer.append("58 packages are looking for funding\n"); buffer.append(" run `npm fund` for details\n"); buffer.append("\u001b[1G\u001b[0K"); buffer.append("found 0 vulnerabilities\n"); buffer.append("\u001b[1G\u001b[0K⠙"); return new Promise<void>((resolve) => { setTimeout(() => { const result = buffer.read(); const contents = result.entries.map((e) => e.content); expect(contents).toContain("up to date, audited 123 packages in 5s"); expect(contents).toContain("58 packages are looking for funding"); expect(contents).toContain(" run `npm fund` for details"); expect(contents).toContain("found 0 vulnerabilities"); resolve(); }, 120); }); }); test("should handle ANSI escape sequences with spinners", () => { // Simulate colored spinner output buffer.append("[33m⠋[0m Installing\r"); buffer.append("[33m⠙[0m Installing\r"); buffer.append("[33m⠹[0m Installing\n"); return new Promise<void>((resolve) => { setTimeout(() => { const result = buffer.read(); // Should still detect and compact spinners despite ANSI codes expect(result.entries.length).toBeLessThan(3); resolve(); }, 100); }); }); test("should handle rapid spinner updates", () => { // Simulate very rapid spinner updates const spinners = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]; for (let i = 0; i < 50; i++) { const spinner = spinners[i % spinners.length]; buffer.append(`${spinner} Processing\r`); } buffer.append("Complete!\n"); return new Promise<void>((resolve) => { setTimeout(() => { const result = buffer.read(); // Should have significantly compacted the output expect(result.entries.length).toBeLessThan(20); // Should preserve the final message const contents = result.entries.map((e) => e.content); expect(contents).toContain("Complete!"); resolve(); }, 150); }); }); }); describe("With compactAnimations disabled", () => { let buffer: OutputBuffer; beforeEach(() => { buffer = new OutputBuffer("test-terminal", 100, { compactAnimations: false, animationThrottleMs: 50, }); }); test("should not compact spinner updates when disabled", () => { buffer.append("⠋ Installing\r"); buffer.append("⠙ Installing\r"); buffer.append("⠹ Installing\r"); buffer.append("⠸ Installing\n"); return new Promise<void>((resolve) => { setTimeout(() => { const result = buffer.read(); // Without compaction, should have the final line expect(result.entries.length).toBeGreaterThan(0); resolve(); }, 100); }); }); }); describe("Dynamic configuration", () => { let buffer: OutputBuffer; beforeEach(() => { buffer = new OutputBuffer("test-terminal", 100, { compactAnimations: true, animationThrottleMs: 50, }); }); test("should allow toggling compactAnimations", () => { expect(buffer.getCompactAnimations()).toBe(true); buffer.setCompactAnimations(false); expect(buffer.getCompactAnimations()).toBe(false); buffer.setCompactAnimations(true); expect(buffer.getCompactAnimations()).toBe(true); }); test("should flush pending spinners when disabling compaction", () => { buffer.append("⠋ Processing\r"); buffer.append("⠙ Processing\r"); // Disable compaction - should flush pending spinners buffer.setCompactAnimations(false); return new Promise<void>((resolve) => { setTimeout(() => { const result = buffer.read(); expect(result.entries.length).toBeGreaterThan(0); resolve(); }, 100); }); }); }); describe("Edge cases", () => { let buffer: OutputBuffer; beforeEach(() => { buffer = new OutputBuffer("test-terminal", 100, { compactAnimations: true, animationThrottleMs: 50, }); }); test("should handle empty spinner lines", () => { buffer.append("\r\r\r\n"); const result = buffer.read(); expect(result.entries.length).toBeGreaterThanOrEqual(0); }); test("should handle spinner-like text that is not a spinner", () => { buffer.append("The file is named spinner.txt\n"); const result = buffer.read(); const contents = result.entries.map((e) => e.content); // Should preserve normal text expect(contents).toContain("The file is named spinner.txt"); }); test("should handle mixed content on same line", () => { buffer.append("⠋ Step 1: Installing packages\r"); buffer.append("⠙ Step 1: Installing packages\r"); buffer.append("✓ Step 1: Installing packages\n"); buffer.append("⠋ Step 2: Building project\r"); buffer.append("⠙ Step 2: Building project\r"); buffer.append("✓ Step 2: Building project\n"); return new Promise<void>((resolve) => { setTimeout(() => { const result = buffer.read(); const contents = result.entries.map((e) => e.content); // Should preserve the completion messages expect(contents.some((c) => c.includes("Step 1"))).toBe(true); expect(contents.some((c) => c.includes("Step 2"))).toBe(true); resolve(); }, 150); }); }); }); });

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