Skip to main content
Glama
2389-research

MCP Agent Social Media Server

metrics.test.ts18.5 kB
// ABOUTME: Unit tests for metrics collection system // ABOUTME: Tests operation tracking, system metrics, and performance monitoring import { jest } from '@jest/globals'; import { MetricsCollector, type OperationMetrics, metrics, withMetrics } from '../src/metrics.js'; // Helper function to sleep const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); describe('MetricsCollector', () => { let metricsCollector: MetricsCollector; beforeEach(() => { // Get fresh instance and reset it metricsCollector = MetricsCollector.getInstance(); metricsCollector.reset(); }); afterEach(() => { // Clean up metricsCollector.reset(); }); afterAll(() => { // Final cleanup metricsCollector.shutdown(); }); describe('singleton behavior', () => { it('should return the same instance', () => { const instance1 = MetricsCollector.getInstance(); const instance2 = MetricsCollector.getInstance(); expect(instance1).toBe(instance2); expect(instance1).toBe(metricsCollector); }); it('should maintain state across getInstance calls', () => { const instance1 = MetricsCollector.getInstance(); instance1.incrementSessionCount(); const instance2 = MetricsCollector.getInstance(); expect(instance2.getSessionCount()).toBe(1); }); }); describe('operation tracking', () => { it('should start and end operations successfully', () => { const operationId = metricsCollector.startOperation('test-operation'); expect(operationId).toMatch(/^test-operation_\d+_0\.\d+$/); // End the operation metricsCollector.endOperation(operationId, true); const operationMetrics = metricsCollector.getOperationMetrics('test-operation'); expect(operationMetrics).toBeDefined(); expect(operationMetrics?.count).toBe(1); expect(operationMetrics?.errors).toBe(0); }); it('should track operation duration', async () => { const operationId = metricsCollector.startOperation('timing-test'); // Wait a bit await sleep(50); metricsCollector.endOperation(operationId, true); const operationMetrics = metricsCollector.getOperationMetrics('timing-test'); expect(operationMetrics?.lastDuration).toBeGreaterThanOrEqual(40); expect(operationMetrics?.lastDuration).toBeLessThan(200); expect(operationMetrics?.minDuration).toBe(operationMetrics?.lastDuration); expect(operationMetrics?.maxDuration).toBe(operationMetrics?.lastDuration); expect(operationMetrics?.averageDuration).toBe(operationMetrics?.lastDuration); }); it('should track multiple operations of same type', async () => { // First operation const op1 = metricsCollector.startOperation('multi-test'); await sleep(30); metricsCollector.endOperation(op1, true); // Second operation const op2 = metricsCollector.startOperation('multi-test'); await sleep(70); metricsCollector.endOperation(op2, true); const metrics = metricsCollector.getOperationMetrics('multi-test'); expect(metrics?.count).toBe(2); expect(metrics?.minDuration).toBeLessThan(metrics?.maxDuration); expect(metrics?.averageDuration).toBeGreaterThan(0); expect(metrics?.totalDuration).toBe(metrics?.minDuration + metrics?.maxDuration); }); it('should track operation errors', () => { const operationId = metricsCollector.startOperation('error-test'); metricsCollector.endOperation(operationId, false); const operationMetrics = metricsCollector.getOperationMetrics('error-test'); expect(operationMetrics?.count).toBe(1); expect(operationMetrics?.errors).toBe(1); }); it('should handle non-existent operation ID gracefully', () => { expect(() => { metricsCollector.endOperation('non-existent-id', true); }).not.toThrow(); // Should not create metrics for non-existent operation expect(metricsCollector.getOperationMetrics('non-existent')).toBeUndefined(); }); it('should track concurrent operations', async () => { const ops = Array(5) .fill(0) .map((_, i) => metricsCollector.startOperation(`concurrent-test-${i}`)); // End them all after different delays await Promise.all( ops.map(async (opId, i) => { await sleep(20 + i * 10); metricsCollector.endOperation(opId, true); }), ); // Each should have its own metrics for (let i = 0; i < 5; i++) { const metrics = metricsCollector.getOperationMetrics(`concurrent-test-${i}`); expect(metrics?.count).toBe(1); } }); }); describe('session management', () => { it('should track session count', () => { expect(metricsCollector.getSessionCount()).toBe(0); metricsCollector.incrementSessionCount(); expect(metricsCollector.getSessionCount()).toBe(1); metricsCollector.incrementSessionCount(); expect(metricsCollector.getSessionCount()).toBe(2); metricsCollector.decrementSessionCount(); expect(metricsCollector.getSessionCount()).toBe(1); }); it('should not allow negative session count', () => { expect(metricsCollector.getSessionCount()).toBe(0); metricsCollector.decrementSessionCount(); expect(metricsCollector.getSessionCount()).toBe(0); metricsCollector.decrementSessionCount(); expect(metricsCollector.getSessionCount()).toBe(0); }); it('should track concurrent session changes', () => { // Simulate rapid session creation/destruction for (let i = 0; i < 10; i++) { metricsCollector.incrementSessionCount(); } expect(metricsCollector.getSessionCount()).toBe(10); for (let i = 0; i < 7; i++) { metricsCollector.decrementSessionCount(); } expect(metricsCollector.getSessionCount()).toBe(3); }); }); describe('system metrics', () => { it('should provide system metrics', () => { const systemMetrics = metricsCollector.getSystemMetrics(); expect(systemMetrics).toEqual({ uptime: expect.any(Number), memoryUsage: expect.objectContaining({ rss: expect.any(Number), heapTotal: expect.any(Number), heapUsed: expect.any(Number), external: expect.any(Number), arrayBuffers: expect.any(Number), }), sessionCount: 0, activeOperations: 0, }); expect(systemMetrics.uptime).toBeGreaterThanOrEqual(0); expect(systemMetrics.memoryUsage.rss).toBeGreaterThan(0); }); it('should update active operations count', () => { const initialMetrics = metricsCollector.getSystemMetrics(); expect(initialMetrics.activeOperations).toBe(0); const op1 = metricsCollector.startOperation('active-test-1'); const op2 = metricsCollector.startOperation('active-test-2'); const activeMetrics = metricsCollector.getSystemMetrics(); expect(activeMetrics.activeOperations).toBe(2); metricsCollector.endOperation(op1, true); const partiallyActiveMetrics = metricsCollector.getSystemMetrics(); expect(partiallyActiveMetrics.activeOperations).toBe(1); metricsCollector.endOperation(op2, true); const finalMetrics = metricsCollector.getSystemMetrics(); expect(finalMetrics.activeOperations).toBe(0); }); it('should track uptime progression', async () => { const metrics1 = metricsCollector.getSystemMetrics(); await sleep(100); const metrics2 = metricsCollector.getSystemMetrics(); expect(metrics2.uptime).toBeGreaterThanOrEqual(metrics1.uptime); }); }); describe('getAllMetrics', () => { it('should return empty object when no operations tracked', () => { const allMetrics = metricsCollector.getAllMetrics(); expect(allMetrics).toEqual({}); }); it('should return all tracked operation metrics', () => { // Track some operations const op1 = metricsCollector.startOperation('op1'); const op2 = metricsCollector.startOperation('op2'); metricsCollector.endOperation(op1, true); metricsCollector.endOperation(op2, false); const allMetrics = metricsCollector.getAllMetrics(); expect(Object.keys(allMetrics)).toEqual(['op1', 'op2']); expect(allMetrics.op1.count).toBe(1); expect(allMetrics.op1.errors).toBe(0); expect(allMetrics.op2.count).toBe(1); expect(allMetrics.op2.errors).toBe(1); }); it('should return deep copies of metrics', () => { const op = metricsCollector.startOperation('mutation-test'); metricsCollector.endOperation(op, true); const allMetrics = metricsCollector.getAllMetrics(); const originalCount = allMetrics['mutation-test'].count; // Mutate the returned object allMetrics['mutation-test'].count = 999; // Original should be unchanged const freshMetrics = metricsCollector.getAllMetrics(); expect(freshMetrics['mutation-test'].count).toBe(originalCount); }); }); describe('getSummary', () => { it('should generate readable summary', () => { // Add some sample data metricsCollector.incrementSessionCount(); metricsCollector.incrementSessionCount(); const op1 = metricsCollector.startOperation('summary-test'); metricsCollector.endOperation(op1, true); const summary = metricsCollector.getSummary(); expect(summary).toContain('=== System Metrics ==='); expect(summary).toContain('=== Operation Metrics ==='); expect(summary).toContain('Active Sessions: 2'); expect(summary).toContain('summary-test:'); expect(summary).toContain('Count: 1'); expect(summary).toContain('Error Rate:'); }); it('should handle empty metrics gracefully', () => { const summary = metricsCollector.getSummary(); expect(summary).toContain('=== System Metrics ==='); expect(summary).toContain('=== Operation Metrics ==='); expect(summary).toContain('Active Sessions: 0'); }); }); describe('reset functionality', () => { it('should clear all metrics when reset', () => { // Add some data metricsCollector.incrementSessionCount(); const op = metricsCollector.startOperation('reset-test'); metricsCollector.endOperation(op, true); expect(metricsCollector.getSessionCount()).toBe(1); expect(metricsCollector.getOperationMetrics('reset-test')).toBeDefined(); // Reset metricsCollector.reset(); expect(metricsCollector.getSessionCount()).toBe(0); expect(metricsCollector.getOperationMetrics('reset-test')).toBeUndefined(); expect(metricsCollector.getAllMetrics()).toEqual({}); }); it('should clear active operations when reset', () => { const op1 = metricsCollector.startOperation('active-reset-test'); metricsCollector.startOperation('active-reset-test-2'); expect(metricsCollector.getSystemMetrics().activeOperations).toBe(2); metricsCollector.reset(); expect(metricsCollector.getSystemMetrics().activeOperations).toBe(0); // Ending the operation after reset should not create metrics metricsCollector.endOperation(op1, true); expect(metricsCollector.getOperationMetrics('active-reset-test')).toBeUndefined(); }); }); describe('stale operation cleanup', () => { it('should clean up stale operations after timeout', async () => { // Create a collector with very short timeout for testing const testCollector = new (class extends MetricsCollector { // Override the cleanup timeout for testing public testCleanupStaleOperations() { // Use reflection to access private method (this as unknown as { OPERATION_TIMEOUT: number }).OPERATION_TIMEOUT = 50; // Very short timeout (this as unknown as { cleanupStaleOperations: () => void }).cleanupStaleOperations(); } })(); testCollector.reset(); const operationId = testCollector.startOperation('stale-test'); // Wait longer than timeout await sleep(100); // Trigger cleanup manually testCollector.testCleanupStaleOperations(); // Operation should be cleaned up and marked as error const metrics = testCollector.getOperationMetrics('stale-test'); expect(metrics?.count).toBe(1); expect(metrics?.errors).toBe(1); // Active operations should be cleared expect(testCollector.getSystemMetrics().activeOperations).toBe(0); // Ending the already cleaned operation should not affect metrics testCollector.endOperation(operationId, true); expect(metrics?.count).toBe(1); // Should still be 1 testCollector.shutdown(); }, 10000); }); describe('error cases', () => { it('should handle malformed operation IDs', () => { expect(() => { metricsCollector.endOperation('malformed-id-without-underscores', true); }).not.toThrow(); expect(() => { metricsCollector.endOperation('_', true); }).not.toThrow(); expect(() => { metricsCollector.endOperation('', true); }).not.toThrow(); }); it('should handle duplicate operation ending', () => { const operationId = metricsCollector.startOperation('duplicate-end-test'); metricsCollector.endOperation(operationId, true); // Ending again should not throw or affect metrics expect(() => { metricsCollector.endOperation(operationId, true); }).not.toThrow(); const metrics = metricsCollector.getOperationMetrics('duplicate-end-test'); expect(metrics?.count).toBe(1); }); }); }); describe('withMetrics helper', () => { beforeEach(() => { metrics.reset(); }); afterEach(() => { metrics.reset(); }); afterAll(() => { metrics.shutdown(); }); it('should track successful async operations', async () => { const result = await withMetrics('helper-test', async () => { await sleep(50); return 'success'; }); expect(result).toBe('success'); const operationMetrics = metrics.getOperationMetrics('helper-test'); expect(operationMetrics?.count).toBe(1); expect(operationMetrics?.errors).toBe(0); expect(operationMetrics?.lastDuration).toBeGreaterThanOrEqual(40); }); it('should track failed async operations', async () => { await expect( withMetrics('helper-error-test', async () => { await sleep(30); throw new Error('Test error'); }), ).rejects.toThrow('Test error'); const operationMetrics = metrics.getOperationMetrics('helper-error-test'); expect(operationMetrics?.count).toBe(1); expect(operationMetrics?.errors).toBe(1); expect(operationMetrics?.lastDuration).toBeGreaterThanOrEqual(20); }); it('should handle promise rejection correctly', async () => { await expect( withMetrics('helper-rejection-test', async () => { return Promise.reject('Rejection reason'); }), ).rejects.toBe('Rejection reason'); const operationMetrics = metrics.getOperationMetrics('helper-rejection-test'); expect(operationMetrics?.count).toBe(1); expect(operationMetrics?.errors).toBe(1); }); it('should track multiple concurrent operations', async () => { const promises = Array(3) .fill(0) .map(async (_, i) => { return withMetrics(`concurrent-helper-${i}`, async () => { await sleep(50 + i * 20); return `result-${i}`; }); }); const results = await Promise.all(promises); expect(results).toEqual(['result-0', 'result-1', 'result-2']); // Each operation should have its own metrics for (let i = 0; i < 3; i++) { const operationMetrics = metrics.getOperationMetrics(`concurrent-helper-${i}`); expect(operationMetrics?.count).toBe(1); expect(operationMetrics?.errors).toBe(0); } }); it('should handle immediately resolved promises', async () => { const result = await withMetrics('immediate-test', async () => { return 'immediate'; }); expect(result).toBe('immediate'); const operationMetrics = metrics.getOperationMetrics('immediate-test'); expect(operationMetrics?.count).toBe(1); expect(operationMetrics?.lastDuration).toBeGreaterThanOrEqual(0); expect(operationMetrics?.lastDuration).toBeLessThan(50); }); it('should handle non-async functions', async () => { const result = await withMetrics('sync-test', async () => { // Sync operation wrapped in async return 2 + 2; }); expect(result).toBe(4); const operationMetrics = metrics.getOperationMetrics('sync-test'); expect(operationMetrics?.count).toBe(1); expect(operationMetrics?.errors).toBe(0); }); }); describe('exported metrics instance', () => { afterEach(() => { metrics.reset(); }); afterAll(() => { metrics.shutdown(); }); it('should be the same as MetricsCollector.getInstance()', () => { expect(metrics).toBe(MetricsCollector.getInstance()); }); it('should maintain state across imports', () => { metrics.incrementSessionCount(); expect(MetricsCollector.getInstance().getSessionCount()).toBe(1); }); }); describe('OperationMetrics interface', () => { it('should have correct structure when created', () => { const metricsCollector = MetricsCollector.getInstance(); metricsCollector.reset(); const operationId = metricsCollector.startOperation('structure-test'); metricsCollector.endOperation(operationId, true); const operationMetrics = metricsCollector.getOperationMetrics('structure-test'); expect(operationMetrics).toEqual<OperationMetrics>({ count: expect.any(Number), totalDuration: expect.any(Number), minDuration: expect.any(Number), maxDuration: expect.any(Number), averageDuration: expect.any(Number), lastDuration: expect.any(Number), errors: expect.any(Number), }); expect(operationMetrics?.count).toBe(1); expect(operationMetrics?.errors).toBe(0); expect(operationMetrics?.totalDuration).toBe(operationMetrics?.lastDuration); expect(operationMetrics?.minDuration).toBe(operationMetrics?.lastDuration); expect(operationMetrics?.maxDuration).toBe(operationMetrics?.lastDuration); expect(operationMetrics?.averageDuration).toBe(operationMetrics?.lastDuration); }); });

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