Skip to main content
Glama
ooples

MCP Console Automation Server

session-management.test.ts19.9 kB
import { describe, beforeAll, afterAll, beforeEach, afterEach, it, expect, jest } from '@jest/globals'; import { ConsoleManager } from '../../src/core/ConsoleManager.js'; import { ConsoleSession, SessionOptions } from '../../src/types/index.js'; import { EventEmitter } from 'events'; import { spawn } from 'child_process'; import { platform } from 'os'; // Skip hardware-intensive integration tests in CI const describeIfHardware = process.env.SKIP_HARDWARE_TESTS ? describe.skip : describe; describeIfHardware('Session Management Integration Tests', () => { let consoleManager: ConsoleManager; const maxSessions = 10; beforeAll(() => { consoleManager = new ConsoleManager(); }); afterAll(async () => { await consoleManager.destroy(); }, 15000); beforeEach(() => { jest.clearAllMocks(); }); describe('Concurrent Session Limits', () => { it('should enforce maximum session limits', async () => { const sessions: string[] = []; const command = platform() === 'win32' ? 'ping -t localhost' : 'ping localhost'; try { // Create sessions up to the limit for (let i = 0; i < maxSessions; i++) { const sessionId = await consoleManager.createSession({ command: command, timeout: 60000 }); sessions.push(sessionId); expect(consoleManager.isSessionRunning(sessionId)).toBe(true); } expect(sessions).toHaveLength(maxSessions); // Try to exceed the limit - this should succeed since we set maxSessions to 50 in ConsoleManager // Let's test with more than the default number to see actual behavior const resourceUsage = consoleManager.getResourceUsage(); expect(resourceUsage.sessions).toBeGreaterThanOrEqual(maxSessions); } finally { // Cleanup all sessions await Promise.all(sessions.map(id => consoleManager.stopSession(id))); } }, 30000); it('should handle rapid session creation and cleanup', async () => { const sessionIds: string[] = []; const command = platform() === 'win32' ? 'echo test' : 'echo test'; // Rapidly create sessions const createPromises = Array.from({ length: 5 }, async (_, i) => { const sessionId = await consoleManager.createSession({ command: `${command} ${i}`, timeout: 5000 }); sessionIds.push(sessionId); return sessionId; }); const createdSessions = await Promise.all(createPromises); expect(createdSessions).toHaveLength(5); // Wait a bit for sessions to potentially complete await new Promise(resolve => setTimeout(resolve, 2000)); // Rapidly clean up const stopPromises = sessionIds.map(id => consoleManager.stopSession(id)); await Promise.all(stopPromises); // Verify all sessions are stopped sessionIds.forEach(id => { const session = consoleManager.getSession(id); if (session) { expect(['stopped', 'crashed']).toContain(session.status); } }); }, 15000); it('should handle concurrent session creation from different threads', async () => { const command = platform() === 'win32' ? 'timeout 2' : 'sleep 2'; const concurrentSessions = 5; // Create sessions concurrently const createPromises = Array.from({ length: concurrentSessions }, async (_, i) => { return consoleManager.createSession({ command: `${command}`, timeout: 10000 }); }); const sessionIds = await Promise.all(createPromises); expect(sessionIds).toHaveLength(concurrentSessions); expect(new Set(sessionIds).size).toBe(concurrentSessions); // All should be unique // Verify all sessions are running sessionIds.forEach(id => { expect(consoleManager.isSessionRunning(id)).toBe(true); }); // Cleanup await Promise.all(sessionIds.map(id => consoleManager.stopSession(id))); }, 15000); it('should manage memory usage under concurrent session load', async () => { const initialUsage = consoleManager.getResourceUsage(); const sessionIds: string[] = []; const command = platform() === 'win32' ? 'echo memory test' : 'echo memory test'; try { // Create multiple sessions that generate output for (let i = 0; i < 8; i++) { const sessionId = await consoleManager.createSession({ command: command, timeout: 5000 }); sessionIds.push(sessionId); } await new Promise(resolve => setTimeout(resolve, 2000)); const peakUsage = consoleManager.getResourceUsage(); expect(peakUsage.sessions).toBeGreaterThan(initialUsage.sessions); // Clean up sessions await Promise.all(sessionIds.map(id => consoleManager.stopSession(id))); // Wait for cleanup await new Promise(resolve => setTimeout(resolve, 1000)); const finalUsage = consoleManager.getResourceUsage(); expect(finalUsage.sessions).toBeLessThanOrEqual(peakUsage.sessions); } finally { // Ensure cleanup await Promise.all(sessionIds.map(id => consoleManager.stopSession(id))); } }, 20000); }); describe('Session Recovery After Crash', () => { it('should detect and handle crashed sessions', async () => { const command = platform() === 'win32' ? 'invalidcommand.exe' : 'invalidcommand'; const sessionId = await consoleManager.createSession({ command: command, timeout: 5000 }); let errorReceived = false; let sessionStopped = false; consoleManager.on('console-event', (event) => { if (event.sessionId === sessionId) { if (event.type === 'error') { errorReceived = true; } else if (event.type === 'stopped') { sessionStopped = true; } } }); // Wait for process to fail await new Promise(resolve => setTimeout(resolve, 6000)); const session = consoleManager.getSession(sessionId); if (session) { expect(['crashed', 'stopped']).toContain(session.status); } expect(errorReceived || sessionStopped).toBe(true); }, 10000); it('should recover from system resource exhaustion', async () => { const sessions: string[] = []; const command = platform() === 'win32' ? 'ping -n 10 localhost' : 'ping -c 10 localhost'; try { // Create many sessions to potentially exhaust resources const createPromises = Array.from({ length: 15 }, async () => { try { const sessionId = await consoleManager.createSession({ command: command, timeout: 15000 }); return sessionId; } catch (error) { return null; // Some might fail due to resource limits } }); const results = await Promise.all(createPromises); const validSessions = results.filter((id): id is string => id !== null); sessions.push(...validSessions); expect(validSessions.length).toBeGreaterThan(0); // Wait for some to complete naturally await new Promise(resolve => setTimeout(resolve, 5000)); // System should still be responsive const resourceUsage = consoleManager.getResourceUsage(); expect(typeof resourceUsage.sessions).toBe('number'); } finally { // Cleanup await Promise.all(sessions.map(id => consoleManager.stopSession(id))); } }, 25000); it('should handle graceful restart of failed sessions', async () => { const command = platform() === 'win32' ? 'echo test & exit 1' : 'echo test && exit 1'; const sessionId = await consoleManager.createSession({ command: command, timeout: 5000 }); // Wait for session to complete with error await new Promise(resolve => setTimeout(resolve, 2000)); const session = consoleManager.getSession(sessionId); expect(session?.status).toBe('stopped'); expect(session?.exitCode).toBe(1); // Create a new session with the same command (simulating restart) const newSessionId = await consoleManager.createSession({ command: 'echo restart test', timeout: 5000 }); expect(newSessionId).toBeTruthy(); expect(newSessionId).not.toBe(sessionId); await consoleManager.stopSession(newSessionId); }, 10000); }); describe('Session State Transitions', () => { it('should properly transition through session states', async () => { const stateChanges: string[] = []; const command = platform() === 'win32' ? 'timeout 3' : 'sleep 3'; const sessionId = await consoleManager.createSession({ command: command, timeout: 10000 }); consoleManager.on('console-event', (event) => { if (event.sessionId === sessionId) { stateChanges.push(event.type); } }); // Session should start expect(consoleManager.isSessionRunning(sessionId)).toBe(true); const session = consoleManager.getSession(sessionId); expect(session?.status).toBe('running'); // Wait for completion await new Promise(resolve => setTimeout(resolve, 5000)); // Session should be stopped const finalSession = consoleManager.getSession(sessionId); expect(finalSession?.status).toBe('stopped'); // Should have received started and stopped events expect(stateChanges).toContain('started'); expect(stateChanges).toContain('stopped'); }, 15000); it('should handle forced termination state transition', async () => { const command = platform() === 'win32' ? 'ping -t localhost' : 'ping localhost'; const sessionId = await consoleManager.createSession({ command: command, timeout: 60000 }); expect(consoleManager.isSessionRunning(sessionId)).toBe(true); // Force stop the session await consoleManager.stopSession(sessionId); // Session should be stopped expect(consoleManager.isSessionRunning(sessionId)).toBe(false); const session = consoleManager.getSession(sessionId); expect(session?.status).toBe('stopped'); }, 10000); it('should maintain session metadata during transitions', async () => { const command = platform() === 'win32' ? 'echo metadata test' : 'echo metadata test'; const testEnv = { TEST_VAR: 'test_value' }; const testCwd = process.cwd(); const sessionId = await consoleManager.createSession({ command: command, env: testEnv, cwd: testCwd, timeout: 5000 }); const session = consoleManager.getSession(sessionId); expect(session?.command).toBe(command); expect(session?.env?.TEST_VAR).toBe('test_value'); expect(session?.cwd).toBe(testCwd); expect(session?.createdAt).toBeInstanceOf(Date); // Wait for completion await new Promise(resolve => setTimeout(resolve, 2000)); const finalSession = consoleManager.getSession(sessionId); expect(finalSession?.command).toBe(command); expect(finalSession?.env?.TEST_VAR).toBe('test_value'); expect(finalSession?.createdAt).toBeInstanceOf(Date); }, 10000); it('should handle rapid state transitions', async () => { const command = platform() === 'win32' ? 'echo rapid test' : 'echo rapid test'; const sessionId = await consoleManager.createSession({ command: command, timeout: 1000 }); // Immediately try to stop await consoleManager.stopSession(sessionId); const session = consoleManager.getSession(sessionId); expect(['stopped', 'crashed']).toContain(session?.status || 'stopped'); }, 5000); }); describe('Resource Cleanup', () => { it('should clean up process resources after session end', async () => { const initialUsage = consoleManager.getResourceUsage(); const command = platform() === 'win32' ? 'echo cleanup test' : 'echo cleanup test'; const sessionId = await consoleManager.createSession({ command: command, timeout: 5000 }); // Wait for session to complete await new Promise(resolve => setTimeout(resolve, 2000)); // Resource usage should return to baseline after cleanup // Allow some time for cleanup await new Promise(resolve => setTimeout(resolve, 1000)); const finalUsage = consoleManager.getResourceUsage(); // Since we clean up sessions periodically, the exact count might vary // but it shouldn't be significantly higher than initial expect(finalUsage.sessions).toBeLessThanOrEqual(initialUsage.sessions + 1); }, 10000); it('should clean up output buffers and streams', async () => { const command = platform() === 'win32' ? 'for /L %i in (1,1,10) do @echo Line %i' : 'for i in {1..10}; do echo Line $i; done'; const sessionId = await consoleManager.createSession({ command: command, streaming: true, timeout: 10000 }); // Wait for output await new Promise(resolve => setTimeout(resolve, 3000)); const output = consoleManager.getOutput(sessionId); expect(output.length).toBeGreaterThan(0); // Clear output buffer consoleManager.clearOutput(sessionId); const clearedOutput = consoleManager.getOutput(sessionId); expect(clearedOutput).toHaveLength(0); await consoleManager.stopSession(sessionId); }, 15000); it('should handle cleanup during high output volume', async () => { const command = platform() === 'win32' ? 'for /L %i in (1,1,100) do @echo High volume output line %i' : 'for i in {1..100}; do echo High volume output line $i; done'; const sessionId = await consoleManager.createSession({ command: command, timeout: 15000 }); // Let some output accumulate await new Promise(resolve => setTimeout(resolve, 2000)); const beforeCleanup = consoleManager.getResourceUsage(); // Force stop during high output await consoleManager.stopSession(sessionId); // Allow cleanup time await new Promise(resolve => setTimeout(resolve, 1000)); const afterCleanup = consoleManager.getResourceUsage(); // Memory usage should not increase significantly expect(afterCleanup.memoryMB).toBeLessThanOrEqual(beforeCleanup.memoryMB + 10); }, 20000); it('should cleanup monitoring resources', async () => { const command = platform() === 'win32' ? 'echo monitoring test' : 'echo monitoring test'; const sessionId = await consoleManager.createSession({ command: command, monitoring: { enabled: true, collectMetrics: true }, timeout: 5000 }); // Check if monitoring is active expect(consoleManager.getMonitoringSystem().isSessionBeingMonitored(sessionId)).toBe(true); await new Promise(resolve => setTimeout(resolve, 2000)); await consoleManager.stopSession(sessionId); // Monitoring should be cleaned up expect(consoleManager.getMonitoringSystem().isSessionBeingMonitored(sessionId)).toBe(false); }, 10000); }); describe('Memory Leak Prevention', () => { it('should not leak memory with many short-lived sessions', async () => { const initialUsage = consoleManager.getResourceUsage(); const command = platform() === 'win32' ? 'echo memory leak test' : 'echo memory leak test'; // Create and destroy many sessions for (let i = 0; i < 20; i++) { const sessionId = await consoleManager.createSession({ command: `${command} ${i}`, timeout: 2000 }); // Wait briefly and stop await new Promise(resolve => setTimeout(resolve, 100)); await consoleManager.stopSession(sessionId); } // Allow cleanup cycle to run await new Promise(resolve => setTimeout(resolve, 3000)); const finalUsage = consoleManager.getResourceUsage(); // Memory shouldn't have increased significantly const memoryIncrease = finalUsage.memoryMB - initialUsage.memoryMB; expect(memoryIncrease).toBeLessThan(50); // Allow some reasonable growth }, 30000); it('should handle event listener cleanup', async () => { const command = platform() === 'win32' ? 'echo event test' : 'echo event test'; let eventCount = 0; const eventHandler = (event: any) => { eventCount++; }; consoleManager.on('console-event', eventHandler); // Create and destroy sessions for (let i = 0; i < 5; i++) { const sessionId = await consoleManager.createSession({ command: `${command} ${i}`, timeout: 3000 }); await new Promise(resolve => setTimeout(resolve, 500)); await consoleManager.stopSession(sessionId); } consoleManager.removeListener('console-event', eventHandler); expect(eventCount).toBeGreaterThan(0); // Verify that removed listeners don't receive new events const previousCount = eventCount; const sessionId = await consoleManager.createSession({ command: command, timeout: 3000 }); await new Promise(resolve => setTimeout(resolve, 1000)); await consoleManager.stopSession(sessionId); expect(eventCount).toBe(previousCount); }, 20000); it('should properly handle buffer size limits', async () => { const command = platform() === 'win32' ? 'for /L %i in (1,1,1000) do @echo Buffer overflow test line %i' : 'for i in {1..1000}; do echo Buffer overflow test line $i; done'; const sessionId = await consoleManager.createSession({ command: command, timeout: 20000 }); // Wait for substantial output await new Promise(resolve => setTimeout(resolve, 5000)); const output = consoleManager.getOutput(sessionId); const resourceUsage = consoleManager.getResourceUsage(); // Buffer should be limited to prevent memory issues expect(output.length).toBeLessThan(10000); // Should respect maxBufferSize expect(resourceUsage.bufferSizes[sessionId]).toBeLessThan(10000); await consoleManager.stopSession(sessionId); }, 25000); it('should handle rapid session creation/destruction without leaks', async () => { const initialSessions = consoleManager.getAllSessions().length; const command = platform() === 'win32' ? 'echo rapid test' : 'echo rapid test'; // Rapid fire session creation and destruction const promises: Promise<void>[] = []; for (let i = 0; i < 10; i++) { promises.push((async () => { const sessionId = await consoleManager.createSession({ command: `${command} ${i}`, timeout: 1000 }); await new Promise(resolve => setTimeout(resolve, 50)); await consoleManager.stopSession(sessionId); })()); } await Promise.all(promises); // Allow cleanup time await new Promise(resolve => setTimeout(resolve, 2000)); const finalSessions = consoleManager.getAllSessions().length; const activeSessions = consoleManager.getAllSessions().filter(s => s.status === 'running').length; // Should not have accumulated sessions expect(activeSessions).toBe(0); expect(finalSessions).toBeLessThanOrEqual(initialSessions + 3); // Allow some tolerance }, 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/ooples/mcp-console-automation'

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