Skip to main content
Glama
2389-research

MCP Agent Social Media Server

timeout.test.ts18.2 kB
// ABOUTME: Unit tests for TimeoutManager middleware // ABOUTME: Tests timeout handling, async lock safety, and configuration management import { jest } from '@jest/globals'; // Mock logger jest.mock('../src/logger.js', () => ({ logger: { info: jest.fn(), debug: jest.fn(), warn: jest.fn(), error: jest.fn(), }, })); import { McpTimeoutError } from '../src/middleware/error-handler.js'; import { type TimeoutConfig, TimeoutManager } from '../src/middleware/timeout.js'; // Helper function to sleep const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); describe('TimeoutManager', () => { let timeoutManager: TimeoutManager; beforeEach(() => { jest.clearAllMocks(); timeoutManager = new TimeoutManager(); }); afterEach(async () => { // Clean up any active timeouts await timeoutManager.clearAllTimeouts(); }); describe('constructor', () => { it('should initialize with default configuration', () => { const manager = new TimeoutManager(); const stats = manager.getStats(); expect(stats.config.defaultTimeout).toBe(30000); expect(stats.config.maxTimeout).toBe(120000); expect(stats.config.methodTimeoutCount).toBeGreaterThan(0); expect(stats.totalTimeouts).toBe(0); expect(stats.activeTimeouts).toBe(0); }); it('should accept custom configuration', () => { const customConfig: Partial<TimeoutConfig> = { defaultTimeout: 15000, maxTimeout: 60000, methodTimeouts: { 'custom/method': 5000, }, }; const manager = new TimeoutManager(customConfig); const stats = manager.getStats(); expect(stats.config.defaultTimeout).toBe(15000); expect(stats.config.maxTimeout).toBe(60000); }); it('should merge custom method timeouts with defaults', () => { const customConfig: Partial<TimeoutConfig> = { methodTimeouts: { 'custom/method': 8000, 'tools/call': 45000, // Override default }, }; const manager = new TimeoutManager(customConfig); const stats = manager.getStats(); // Should have both default methods and custom ones expect(stats.config.methodTimeoutCount).toBeGreaterThanOrEqual(2); }); }); describe('createTimeout', () => { it('should create a timeout promise that rejects after specified time', async () => { const manager = new TimeoutManager({ defaultTimeout: 100, methodTimeouts: {}, }); const start = Date.now(); await expect(manager.createTimeout('test/method')).rejects.toThrow(McpTimeoutError); const elapsed = Date.now() - start; expect(elapsed).toBeGreaterThanOrEqual(90); // Allow some timing variance expect(elapsed).toBeLessThan(200); }); it('should use method-specific timeout when available', async () => { const manager = new TimeoutManager({ defaultTimeout: 1000, methodTimeouts: { 'fast/method': 50, }, }); const start = Date.now(); await expect(manager.createTimeout('fast/method')).rejects.toThrow(McpTimeoutError); const elapsed = Date.now() - start; expect(elapsed).toBeGreaterThanOrEqual(40); expect(elapsed).toBeLessThan(150); }); it('should use default timeout for unknown methods', async () => { const manager = new TimeoutManager({ defaultTimeout: 50, methodTimeouts: { 'known/method': 1000, }, }); const start = Date.now(); await expect(manager.createTimeout('unknown/method')).rejects.toThrow(McpTimeoutError); const elapsed = Date.now() - start; expect(elapsed).toBeGreaterThanOrEqual(40); expect(elapsed).toBeLessThan(150); }); it('should respect maximum timeout limit', async () => { const manager = new TimeoutManager({ defaultTimeout: 30000, maxTimeout: 80, methodTimeouts: { 'long/method': 10000, // This should be capped at maxTimeout }, }); const start = Date.now(); await expect(manager.createTimeout('long/method')).rejects.toThrow(McpTimeoutError); const elapsed = Date.now() - start; expect(elapsed).toBeGreaterThanOrEqual(70); expect(elapsed).toBeLessThan(150); }); it('should track timeout statistics', async () => { const manager = new TimeoutManager({ defaultTimeout: 50, methodTimeouts: {}, }); const initialStats = manager.getStats(); expect(initialStats.totalTimeouts).toBe(0); try { await manager.createTimeout('test/method'); } catch { // Expected timeout } const finalStats = manager.getStats(); expect(finalStats.totalTimeouts).toBe(1); }); }); describe('createClearableTimeout', () => { it('should create timeout that can be cleared', async () => { const manager = new TimeoutManager({ defaultTimeout: 100, methodTimeouts: {}, }); const { promise, clear } = manager.createClearableTimeout('test/method'); // Clear immediately clear(); // Promise should still be pending, but won't reject const raceResult = await Promise.race([ promise.catch(() => 'timeout'), sleep(150).then(() => 'success'), ]); expect(raceResult).toBe('success'); }); it('should timeout if not cleared', async () => { const manager = new TimeoutManager({ defaultTimeout: 50, methodTimeouts: {}, }); const { promise } = manager.createClearableTimeout('test/method'); await expect(promise).rejects.toThrow(McpTimeoutError); }); it('should handle multiple clear calls safely', async () => { const manager = new TimeoutManager({ defaultTimeout: 100, methodTimeouts: {}, }); const { clear } = manager.createClearableTimeout('test/method'); // Multiple clears should not throw expect(() => { clear(); clear(); clear(); }).not.toThrow(); }); }); describe('withTimeout', () => { it('should resolve with promise result when promise completes first', async () => { const manager = new TimeoutManager({ defaultTimeout: 200, methodTimeouts: {}, }); const promise = sleep(50).then(() => 'success'); const result = await manager.withTimeout(promise, 'test/method'); expect(result).toBe('success'); }); it('should reject with timeout error when timeout occurs first', async () => { const manager = new TimeoutManager({ defaultTimeout: 50, methodTimeouts: {}, }); const promise = sleep(200).then(() => 'success'); await expect(manager.withTimeout(promise, 'test/method')).rejects.toThrow(McpTimeoutError); }); it('should propagate promise errors', async () => { const manager = new TimeoutManager({ defaultTimeout: 200, methodTimeouts: {}, }); const promise = sleep(50).then(() => { throw new Error('Test error'); }); await expect(manager.withTimeout(promise, 'test/method')).rejects.toThrow('Test error'); }); it('should clean up timeout when promise resolves', async () => { const manager = new TimeoutManager({ defaultTimeout: 200, methodTimeouts: {}, }); const promise = sleep(50).then(() => 'success'); await manager.withTimeout(promise, 'test/method'); // Give a moment for cleanup await sleep(10); const stats = manager.getStats(); expect(stats.activeTimeouts).toBe(0); }); it('should clean up timeout when promise rejects', async () => { const manager = new TimeoutManager({ defaultTimeout: 200, methodTimeouts: {}, }); const promise = sleep(50).then(() => { throw new Error('Test error'); }); try { await manager.withTimeout(promise, 'test/method'); } catch { // Expected error } // Give a moment for cleanup await sleep(10); const stats = manager.getStats(); expect(stats.activeTimeouts).toBe(0); }); }); describe('clearAllTimeouts', () => { it('should clear all active timeouts', async () => { const manager = new TimeoutManager({ defaultTimeout: 1000, methodTimeouts: {}, }); // Create multiple timeouts const timeout1 = manager.createClearableTimeout('method1'); const timeout2 = manager.createClearableTimeout('method2'); const timeout3 = manager.createClearableTimeout('method3'); const initialStats = manager.getStats(); expect(initialStats.activeTimeouts).toBe(3); await manager.clearAllTimeouts(); const finalStats = manager.getStats(); expect(finalStats.activeTimeouts).toBe(0); // Promises should not timeout anymore const results = await Promise.allSettled([ Promise.race([timeout1.promise.catch(() => 'timeout'), sleep(50).then(() => 'cleared')]), Promise.race([timeout2.promise.catch(() => 'timeout'), sleep(50).then(() => 'cleared')]), Promise.race([timeout3.promise.catch(() => 'timeout'), sleep(50).then(() => 'cleared')]), ]); for (const result of results) { expect(result.status).toBe('fulfilled'); expect((result as PromiseFulfilledResult<string>).value).toBe('cleared'); } }); it('should handle clearing when no timeouts are active', async () => { const manager = new TimeoutManager(); // Should not throw await expect(manager.clearAllTimeouts()).resolves.toBeUndefined(); }); }); describe('getStats', () => { it('should return current statistics', async () => { const manager = new TimeoutManager({ defaultTimeout: 100, methodTimeouts: { 'custom/method': 200, }, }); const initialStats = manager.getStats(); expect(initialStats).toEqual({ totalTimeouts: 0, activeTimeouts: 0, config: { defaultTimeout: 100, maxTimeout: 120000, methodTimeoutCount: expect.any(Number), }, }); // Create timeout const { clear } = manager.createClearableTimeout('test/method'); const statsWithActive = manager.getStats(); expect(statsWithActive.activeTimeouts).toBe(1); // Clean up the clearable timeout first clear(); await sleep(10); // Let timeout occur with a fast timeout const fastManager = new TimeoutManager({ defaultTimeout: 50, methodTimeouts: {}, }); try { await fastManager.createTimeout('fast/method'); } catch { // Expected timeout } const finalStats = fastManager.getStats(); expect(finalStats.totalTimeouts).toBe(1); }); }); describe('updateConfig', () => { it('should update timeout configuration', () => { const manager = new TimeoutManager(); const newConfig: Partial<TimeoutConfig> = { defaultTimeout: 5000, maxTimeout: 15000, methodTimeouts: { 'new/method': 3000, }, }; manager.updateConfig(newConfig); const stats = manager.getStats(); expect(stats.config.defaultTimeout).toBe(5000); expect(stats.config.maxTimeout).toBe(15000); }); it('should merge new method timeouts with existing ones', () => { const manager = new TimeoutManager({ methodTimeouts: { 'existing/method': 1000, }, }); manager.updateConfig({ methodTimeouts: { 'new/method': 2000, }, }); // Both methods should exist - the update should merge with existing defaults const stats = manager.getStats(); expect(stats.config.methodTimeoutCount).toBeGreaterThanOrEqual(1); }); }); describe('Concurrent Operations', () => { it('should handle multiple concurrent timeouts safely', async () => { const manager = new TimeoutManager({ defaultTimeout: 100, methodTimeouts: {}, }); // Create multiple concurrent timeouts const promises = Array(10) .fill(0) .map((_, i) => manager.createTimeout(`method${i}`).catch(() => `timeout-${i}`)); const results = await Promise.all(promises); // All should timeout for (const [i, result] of results.entries()) { expect(result).toBe(`timeout-${i}`); } const stats = manager.getStats(); expect(stats.totalTimeouts).toBe(10); expect(stats.activeTimeouts).toBe(0); }); it('should handle concurrent clear operations safely', async () => { const manager = new TimeoutManager({ defaultTimeout: 500, methodTimeouts: {}, }); // Create multiple clearable timeouts const clearableTimeouts = Array(5) .fill(0) .map((_, i) => manager.createClearableTimeout(`method${i}`)); // Clear them all concurrently await Promise.all(clearableTimeouts.map(({ clear }) => clear())); const stats = manager.getStats(); expect(stats.activeTimeouts).toBe(0); }); it('should handle mix of timeouts and clears safely', async () => { const manager = new TimeoutManager({ defaultTimeout: 100, methodTimeouts: {}, }); // Create some that will timeout const timeoutPromises = Array(3) .fill(0) .map((_, i) => manager.createTimeout(`timeout-method${i}`).catch(() => `timeout-${i}`)); // Create some that will be cleared const clearableTimeouts = Array(3) .fill(0) .map((_, i) => manager.createClearableTimeout(`clear-method${i}`)); // Clear the clearable ones immediately for (const { clear } of clearableTimeouts) { clear(); } // Wait for timeouts to complete const timeoutResults = await Promise.all(timeoutPromises); for (const [i, result] of timeoutResults.entries()) { expect(result).toBe(`timeout-${i}`); } const stats = manager.getStats(); expect(stats.totalTimeouts).toBe(3); expect(stats.activeTimeouts).toBe(0); }); }); describe('Error Handling', () => { it('should create proper McpTimeoutError instances', async () => { const manager = new TimeoutManager({ defaultTimeout: 50, methodTimeouts: {}, }); try { await manager.createTimeout('test/method'); expect(true).toBe(false); // Should not reach here } catch (error) { expect(error).toBeInstanceOf(McpTimeoutError); expect((error as McpTimeoutError).message).toContain('timed out after 50ms'); } }); it('should handle timeout edge cases', async () => { const manager = new TimeoutManager({ defaultTimeout: 0, // Immediate timeout methodTimeouts: {}, }); const start = Date.now(); await expect(manager.createTimeout('immediate/method')).rejects.toThrow(McpTimeoutError); const elapsed = Date.now() - start; expect(elapsed).toBeLessThan(50); // Should be very fast }); }); describe('Resource Management', () => { it('should properly track active timeouts count', async () => { const manager = new TimeoutManager({ defaultTimeout: 200, methodTimeouts: {}, }); const stats1 = manager.getStats(); expect(stats1.activeTimeouts).toBe(0); // Create clearable timeout const { clear } = manager.createClearableTimeout('test1'); const stats2 = manager.getStats(); expect(stats2.activeTimeouts).toBe(1); // Create another manager.createClearableTimeout('test2'); const stats3 = manager.getStats(); expect(stats3.activeTimeouts).toBe(2); // Clear one clear(); await sleep(10); // Allow async cleanup const stats4 = manager.getStats(); expect(stats4.activeTimeouts).toBe(1); }); it('should not leak timeouts on rapid creation and clearing', async () => { const manager = new TimeoutManager({ defaultTimeout: 1000, methodTimeouts: {}, }); // Rapidly create and clear timeouts for (let i = 0; i < 20; i++) { const { clear } = manager.createClearableTimeout(`rapid-${i}`); clear(); } await sleep(50); // Allow cleanup const stats = manager.getStats(); expect(stats.activeTimeouts).toBe(0); }); }); describe('Method-Specific Timeouts', () => { it('should use correct timeouts for predefined methods', async () => { const manager = new TimeoutManager(); // Use defaults // Test that tools/call gets longer timeout than resources/read const toolCallTimeout = manager.createClearableTimeout('tools/call'); const resourceReadTimeout = manager.createClearableTimeout('resources/read'); // Both should be created without immediate timeout expect(toolCallTimeout.promise).toBeInstanceOf(Promise); expect(resourceReadTimeout.promise).toBeInstanceOf(Promise); // Clean up toolCallTimeout.clear(); resourceReadTimeout.clear(); }); it('should handle method names with special characters', async () => { const manager = new TimeoutManager({ defaultTimeout: 100, methodTimeouts: { 'method/with/slashes': 50, 'method-with-dashes': 50, method_with_underscores: 50, 'method.with.dots': 50, }, }); // All should timeout quickly const promises = [ 'method/with/slashes', 'method-with-dashes', 'method_with_underscores', 'method.with.dots', ].map((method) => manager.createTimeout(method).catch(() => method)); const results = await Promise.all(promises); expect(results).toEqual([ 'method/with/slashes', 'method-with-dashes', 'method_with_underscores', 'method.with.dots', ]); }); }); });

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/2389-research/mcp-socialmedia'

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