Skip to main content
Glama
cacheManager.test.ts•9.12 kB
/** * CacheManager Unit Tests */ import { describe, it, expect, beforeEach } from '@jest/globals'; import { CacheManager } from '../src/cacheManager.js'; import { CacheConfig } from '../src/types.js'; describe('CacheManager', () => { let cache: CacheManager<string>; const defaultConfig: CacheConfig = { maxSize: 1000, ttl: 5000, // 5 seconds enabled: true, }; beforeEach(() => { cache = new CacheManager<string>(defaultConfig); }); describe('basic operations', () => { it('should set and get values', () => { cache.set('key1', 'value1'); expect(cache.get('key1')).toBe('value1'); }); it('should return undefined for non-existent keys', () => { expect(cache.get('nonexistent')).toBeUndefined(); }); it('should delete values', () => { cache.set('key1', 'value1'); cache.delete('key1'); expect(cache.get('key1')).toBeUndefined(); }); it('should clear all values', () => { cache.set('key1', 'value1'); cache.set('key2', 'value2'); cache.clear(); expect(cache.get('key1')).toBeUndefined(); expect(cache.get('key2')).toBeUndefined(); }); }); describe('TTL (Time To Live)', () => { it('should expire entries after TTL', async () => { const shortTTLCache = new CacheManager<string>({ ...defaultConfig, ttl: 100, // 100ms }); shortTTLCache.set('key1', 'value1'); expect(shortTTLCache.get('key1')).toBe('value1'); // Wait for TTL to expire await new Promise(resolve => setTimeout(resolve, 150)); expect(shortTTLCache.get('key1')).toBeUndefined(); }); it('should not expire entries before TTL', async () => { cache.set('key1', 'value1'); // Wait less than TTL await new Promise(resolve => setTimeout(resolve, 100)); expect(cache.get('key1')).toBe('value1'); }); }); describe('access count tracking', () => { it('should track access count', () => { cache.set('key1', 'value1'); // Access multiple times cache.get('key1'); cache.get('key1'); cache.get('key1'); const stats = cache.getStats(); expect(stats.entries).toBe(1); }); }); describe('size management', () => { it('should track current size', () => { cache.set('key1', 'small'); const stats = cache.getStats(); expect(stats.sizeBytes).toBeGreaterThan(0); expect(stats.sizeBytes).toBeLessThan(stats.maxSizeBytes); }); it('should not cache entries larger than maxSize', () => { const smallCache = new CacheManager<string>({ maxSize: 10, // Very small cache ttl: 5000, enabled: true, }); const largeValue = 'x'.repeat(1000); smallCache.set('key1', largeValue); expect(smallCache.get('key1')).toBeUndefined(); }); it('should calculate utilization percentage', () => { cache.set('key1', 'value1'); const stats = cache.getStats(); expect(stats.utilizationPercent).toBeGreaterThan(0); expect(stats.utilizationPercent).toBeLessThan(100); }); }); describe('LRU eviction', () => { it('should evict least recently used entries when full', () => { const smallCache = new CacheManager<string>({ maxSize: 100, // Small cache to trigger eviction ttl: 5000, enabled: true, }); // Fill cache smallCache.set('key1', 'value1'); smallCache.set('key2', 'value2'); smallCache.set('key3', 'value3'); // Access key1 and key2 to make them more recently used smallCache.get('key1'); smallCache.get('key2'); // Add more items to trigger eviction smallCache.set('key4', 'value4'); smallCache.set('key5', 'value5'); smallCache.set('key6', 'value6'); // Key3 should be evicted (least recently used) // Keys accessed more recently should still exist expect(smallCache.get('key1')).toBeDefined(); expect(smallCache.get('key2')).toBeDefined(); }); it('should evict oldest entries when access counts are equal', () => { const smallCache = new CacheManager<string>({ maxSize: 100, ttl: 5000, enabled: true, }); // Add entries without accessing them smallCache.set('old', 'value1'); // Small delay to ensure timestamp difference const delay = () => new Promise(resolve => setTimeout(resolve, 10)); delay().then(() => { smallCache.set('new', 'value2'); // Fill cache to trigger eviction smallCache.set('key3', 'value3'); smallCache.set('key4', 'value4'); smallCache.set('key5', 'value5'); // Older entry should be evicted first expect(smallCache.get('new')).toBeDefined(); }); }); }); describe('disabled cache', () => { it('should not cache when disabled', () => { const disabledCache = new CacheManager<string>({ ...defaultConfig, enabled: false, }); disabledCache.set('key1', 'value1'); expect(disabledCache.get('key1')).toBeUndefined(); }); it('should always return undefined when disabled', () => { const disabledCache = new CacheManager<string>({ ...defaultConfig, enabled: false, }); expect(disabledCache.get('any-key')).toBeUndefined(); }); }); describe('getStats', () => { it('should return accurate statistics', () => { cache.set('key1', 'value1'); cache.set('key2', 'value2'); const stats = cache.getStats(); expect(stats).toMatchObject({ entries: 2, sizeBytes: expect.any(Number), maxSizeBytes: defaultConfig.maxSize, utilizationPercent: expect.any(Number), }); expect(stats.sizeBytes).toBeGreaterThan(0); expect(stats.sizeBytes).toBeLessThanOrEqual(stats.maxSizeBytes); }); it('should show zero entries after clear', () => { cache.set('key1', 'value1'); cache.clear(); const stats = cache.getStats(); expect(stats.entries).toBe(0); expect(stats.sizeBytes).toBe(0); expect(stats.utilizationPercent).toBe(0); }); }); describe('complex data types', () => { it('should cache objects', () => { interface TestObject { id: number; name: string; nested: { value: string }; } const objectCache = new CacheManager<TestObject>(defaultConfig); const testObj: TestObject = { id: 1, name: 'test', nested: { value: 'nested-value' }, }; objectCache.set('obj1', testObj); const retrieved = objectCache.get('obj1'); expect(retrieved).toEqual(testObj); }); it('should cache arrays', () => { const arrayCache = new CacheManager<number[]>(defaultConfig); const testArray = [1, 2, 3, 4, 5]; arrayCache.set('arr1', testArray); expect(arrayCache.get('arr1')).toEqual(testArray); }); it('should estimate size correctly for complex objects', () => { interface ComplexObject { data: string; nested: { array: number[] }; } const complexCache = new CacheManager<ComplexObject>(defaultConfig); const largeObj: ComplexObject = { data: 'x'.repeat(100), nested: { array: Array(50).fill(1) }, }; complexCache.set('complex', largeObj); const stats = complexCache.getStats(); expect(stats.sizeBytes).toBeGreaterThan(100); // Should account for object structure }); }); describe('custom size parameter', () => { it('should accept custom size when setting', () => { const customSize = 50; cache.set('key1', 'value', customSize); const stats = cache.getStats(); expect(stats.sizeBytes).toBe(customSize); }); it('should use estimated size when custom size not provided', () => { cache.set('key1', 'value'); // No custom size const stats = cache.getStats(); expect(stats.sizeBytes).toBeGreaterThan(0); }); }); describe('edge cases', () => { it('should handle empty string values', () => { cache.set('empty', ''); expect(cache.get('empty')).toBe(''); }); it('should handle special characters in keys', () => { cache.set('key:with:colons', 'value1'); cache.set('key-with-dashes', 'value2'); cache.set('key.with.dots', 'value3'); expect(cache.get('key:with:colons')).toBe('value1'); expect(cache.get('key-with-dashes')).toBe('value2'); expect(cache.get('key.with.dots')).toBe('value3'); }); it('should handle rapid set operations', () => { for (let i = 0; i < 100; i++) { cache.set(`key${i}`, `value${i}`); } const stats = cache.getStats(); expect(stats.entries).toBeGreaterThan(0); }); it('should handle multiple deletes of same key', () => { cache.set('key1', 'value1'); cache.delete('key1'); cache.delete('key1'); // Delete again cache.delete('key1'); // And again expect(cache.get('key1')).toBeUndefined(); }); }); });

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/willianpinho/large-file-mcp'

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