Skip to main content
Glama

SFCC Development MCP Server

by taurgis
cache.test.ts•14.4 kB
import { InMemoryCache, CacheManager } from '../src/utils/cache'; describe('InMemoryCache', () => { let cache: InMemoryCache<string>; beforeEach(() => { cache = new InMemoryCache<string>({ maxSize: 3, ttlMs: 1000, // 1 second for testing cleanupIntervalMs: 100, // 100ms for faster testing }); }); afterEach(() => { cache.destroy(); }); describe('constructor', () => { it('should use default options when none provided', () => { const defaultCache = new InMemoryCache(); const stats = defaultCache.getStats(); expect(stats.maxSize).toBe(1000); defaultCache.destroy(); }); it('should accept custom options', () => { const customCache = new InMemoryCache({ maxSize: 50, ttlMs: 5000, cleanupIntervalMs: 1000, }); const stats = customCache.getStats(); expect(stats.maxSize).toBe(50); customCache.destroy(); }); }); describe('set and get', () => { it('should store and retrieve 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 update access statistics on get', () => { cache.set('key1', 'value1'); cache.get('key1'); // First access cache.get('key1'); // Second access const stats = cache.getStats(); const entry = stats.entries.find(e => e.key === 'key1'); expect(entry?.accessCount).toBe(2); }); it('should update lastAccessed timestamp on get', async () => { cache.set('key1', 'value1'); const initialGet = cache.get('key1'); // Wait a bit and access again await new Promise(resolve => setTimeout(resolve, 10)); const secondGet = cache.get('key1'); expect(initialGet).toBe('value1'); expect(secondGet).toBe('value1'); const stats = cache.getStats(); const entry = stats.entries.find(e => e.key === 'key1'); expect(entry?.accessCount).toBe(2); }); }); describe('has', () => { it('should return true for existing keys', () => { cache.set('key1', 'value1'); expect(cache.has('key1')).toBe(true); }); it('should return false for non-existent keys', () => { expect(cache.has('nonexistent')).toBe(false); }); it('should not update access statistics', () => { cache.set('key1', 'value1'); cache.has('key1'); const stats = cache.getStats(); const entry = stats.entries.find(e => e.key === 'key1'); expect(entry?.accessCount).toBe(0); }); }); describe('delete', () => { it('should remove existing keys', () => { cache.set('key1', 'value1'); expect(cache.has('key1')).toBe(true); const deleted = cache.delete('key1'); expect(deleted).toBe(true); expect(cache.has('key1')).toBe(false); }); it('should return false for non-existent keys', () => { const deleted = cache.delete('nonexistent'); expect(deleted).toBe(false); }); }); describe('clear', () => { it('should remove all entries', () => { cache.set('key1', 'value1'); cache.set('key2', 'value2'); expect(cache.getStats().size).toBe(2); cache.clear(); expect(cache.getStats().size).toBe(0); }); }); describe('TTL (Time To Live)', () => { it('should expire entries after TTL', async () => { const shortTtlCache = new InMemoryCache<string>({ ttlMs: 50, // 50ms cleanupIntervalMs: 1000, // Don't auto-cleanup for this test }); shortTtlCache.set('key1', 'value1'); expect(shortTtlCache.get('key1')).toBe('value1'); // Wait for expiration await new Promise(resolve => setTimeout(resolve, 60)); expect(shortTtlCache.get('key1')).toBeUndefined(); shortTtlCache.destroy(); }); it('should remove expired entries when checking has()', async () => { const shortTtlCache = new InMemoryCache<string>({ ttlMs: 50, cleanupIntervalMs: 1000, }); shortTtlCache.set('key1', 'value1'); expect(shortTtlCache.has('key1')).toBe(true); await new Promise(resolve => setTimeout(resolve, 60)); expect(shortTtlCache.has('key1')).toBe(false); shortTtlCache.destroy(); }); }); describe('LRU (Least Recently Used) eviction', () => { it('should evict LRU item when max size is reached', async () => { // Fill cache to max capacity cache.set('key1', 'value1'); await new Promise(resolve => setTimeout(resolve, 1)); // Small delay cache.set('key2', 'value2'); await new Promise(resolve => setTimeout(resolve, 1)); // Small delay cache.set('key3', 'value3'); expect(cache.getStats().size).toBe(3); // Wait a bit then access key1 and key2 to make key3 the least recently used await new Promise(resolve => setTimeout(resolve, 5)); cache.get('key1'); cache.get('key2'); // Add new item, should evict key3 cache.set('key4', 'value4'); expect(cache.getStats().size).toBe(3); expect(cache.has('key3')).toBe(false); expect(cache.has('key1')).toBe(true); expect(cache.has('key2')).toBe(true); expect(cache.has('key4')).toBe(true); }); it('should not evict when updating existing key', () => { cache.set('key1', 'value1'); cache.set('key2', 'value2'); cache.set('key3', 'value3'); // Update existing key cache.set('key1', 'newvalue1'); expect(cache.getStats().size).toBe(3); expect(cache.get('key1')).toBe('newvalue1'); }); }); describe('getStats', () => { it('should return correct cache statistics', () => { cache.set('key1', 'value1'); cache.set('key2', 'value2'); cache.get('key1'); // Access key1 const stats = cache.getStats(); expect(stats.size).toBe(2); expect(stats.maxSize).toBe(3); expect(stats.entries).toHaveLength(2); const key1Entry = stats.entries.find(e => e.key === 'key1'); const key2Entry = stats.entries.find(e => e.key === 'key2'); expect(key1Entry?.accessCount).toBe(1); expect(key2Entry?.accessCount).toBe(0); }); it('should calculate hit rate correctly', () => { cache.set('key1', 'value1'); cache.set('key2', 'value2'); cache.get('key1'); // Hit cache.get('key1'); // Hit // key2 never accessed const stats = cache.getStats(); // Total accesses: 2, hits: 1 (key1 was accessed, key2 wasn't) expect(stats.hitRate).toBe(0.5); }); }); describe('cleanup', () => { it('should automatically cleanup expired entries', async () => { const autoCleanupCache = new InMemoryCache<string>({ ttlMs: 50, cleanupIntervalMs: 60, // Very frequent cleanup }); autoCleanupCache.set('key1', 'value1'); expect(autoCleanupCache.getStats().size).toBe(1); // Wait for TTL + cleanup interval await new Promise(resolve => setTimeout(resolve, 120)); const stats = autoCleanupCache.getStats(); expect(stats.size).toBe(0); autoCleanupCache.destroy(); }); }); describe('destroy', () => { it('should clear cache and stop cleanup timer', () => { cache.set('key1', 'value1'); expect(cache.getStats().size).toBe(1); cache.destroy(); expect(cache.getStats().size).toBe(0); // Cache should still be usable but without automatic cleanup cache.set('key2', 'value2'); expect(cache.get('key2')).toBe('value2'); }); }); }); describe('CacheManager', () => { let cacheManager: CacheManager; beforeEach(() => { cacheManager = new CacheManager(); }); afterEach(() => { cacheManager.destroy(); }); describe('file content cache', () => { it('should store and retrieve file content', () => { const content = 'file content'; cacheManager.setFileContent('file1.ts', content); expect(cacheManager.getFileContent('file1.ts')).toBe(content); expect(cacheManager.getFileContent('nonexistent.ts')).toBeUndefined(); }); }); describe('class details cache', () => { it('should store and retrieve class details', () => { const details = { name: 'TestClass', methods: ['method1', 'method2'] }; cacheManager.setClassDetails('TestClass', details); expect(cacheManager.getClassDetails('TestClass')).toEqual(details); expect(cacheManager.getClassDetails('NonExistentClass')).toBeUndefined(); }); }); describe('search results cache', () => { it('should store and retrieve search results', () => { const results = [{ name: 'result1' }, { name: 'result2' }]; cacheManager.setSearchResults('query1', results); expect(cacheManager.getSearchResults('query1')).toEqual(results); expect(cacheManager.getSearchResults('query2')).toBeUndefined(); }); }); describe('method search cache', () => { it('should store and retrieve method search results', () => { const results = [{ method: 'getValue', class: 'TestClass' }]; cacheManager.setMethodSearch('getValue', results); expect(cacheManager.getMethodSearch('getValue')).toEqual(results); expect(cacheManager.getMethodSearch('nonexistent')).toBeUndefined(); }); }); describe('getAllStats', () => { it('should return statistics for all caches', () => { cacheManager.setFileContent('file1.ts', 'content'); cacheManager.setClassDetails('Class1', { name: 'Class1' }); cacheManager.setSearchResults('query1', []); cacheManager.setMethodSearch('method1', []); const allStats = cacheManager.getAllStats(); expect(allStats.fileContent.size).toBe(1); expect(allStats.classDetails.size).toBe(1); expect(allStats.searchResults.size).toBe(1); expect(allStats.methodSearch.size).toBe(1); expect(allStats.fileContent.maxSize).toBe(500); expect(allStats.classDetails.maxSize).toBe(300); expect(allStats.searchResults.maxSize).toBe(200); expect(allStats.methodSearch.maxSize).toBe(100); }); }); describe('clearAll', () => { it('should clear all caches', () => { cacheManager.setFileContent('file1.ts', 'content'); cacheManager.setClassDetails('Class1', { name: 'Class1' }); cacheManager.setSearchResults('query1', []); cacheManager.setMethodSearch('method1', []); let allStats = cacheManager.getAllStats(); expect(allStats.fileContent.size).toBe(1); expect(allStats.classDetails.size).toBe(1); expect(allStats.searchResults.size).toBe(1); expect(allStats.methodSearch.size).toBe(1); cacheManager.clearAll(); allStats = cacheManager.getAllStats(); expect(allStats.fileContent.size).toBe(0); expect(allStats.classDetails.size).toBe(0); expect(allStats.searchResults.size).toBe(0); expect(allStats.methodSearch.size).toBe(0); }); }); describe('destroy', () => { it('should destroy all underlying caches', () => { cacheManager.setFileContent('file1.ts', 'content'); let allStats = cacheManager.getAllStats(); expect(allStats.fileContent.size).toBe(1); cacheManager.destroy(); // After destroy, the caches should be cleared allStats = cacheManager.getAllStats(); expect(allStats.fileContent.size).toBe(0); }); }); describe('different TTL configurations', () => { it('should have different TTL settings for different cache types', () => { // We can't directly test TTL values, but we can verify the caches work independently cacheManager.setFileContent('file1.ts', 'content'); cacheManager.setClassDetails('Class1', { name: 'Class1' }); cacheManager.setSearchResults('query1', ['result1']); cacheManager.setMethodSearch('method1', ['method1']); // All should be accessible expect(cacheManager.getFileContent('file1.ts')).toBe('content'); expect(cacheManager.getClassDetails('Class1')).toEqual({ name: 'Class1' }); expect(cacheManager.getSearchResults('query1')).toEqual(['result1']); expect(cacheManager.getMethodSearch('method1')).toEqual(['method1']); }); }); }); describe('Edge cases and error handling', () => { describe('InMemoryCache edge cases', () => { it('should handle zero max size gracefully', () => { const zeroSizeCache = new InMemoryCache<string>({ maxSize: 0, ttlMs: 1000, }); zeroSizeCache.set('key1', 'value1'); // With maxSize 0, nothing should be stored expect(zeroSizeCache.get('key1')).toBeUndefined(); expect(zeroSizeCache.getStats().size).toBe(0); zeroSizeCache.destroy(); }); it('should handle very short TTL', async () => { const shortTtlCache = new InMemoryCache<string>({ ttlMs: 1, // 1ms cleanupIntervalMs: 1000, }); shortTtlCache.set('key1', 'value1'); // Wait longer than TTL await new Promise(resolve => setTimeout(resolve, 5)); expect(shortTtlCache.get('key1')).toBeUndefined(); shortTtlCache.destroy(); }); it('should handle multiple destroy calls', () => { const cache = new InMemoryCache<string>(); cache.set('key1', 'value1'); cache.destroy(); cache.destroy(); // Second destroy should not throw expect(cache.getStats().size).toBe(0); }); }); describe('Type safety', () => { it('should maintain type safety for different value types', () => { const stringCache = new InMemoryCache<string>(); const numberCache = new InMemoryCache<number>(); const objectCache = new InMemoryCache<{ id: number; name: string }>(); stringCache.set('key1', 'string value'); numberCache.set('key1', 42); objectCache.set('key1', { id: 1, name: 'test' }); const stringValue: string | undefined = stringCache.get('key1'); const numberValue: number | undefined = numberCache.get('key1'); const objectValue: { id: number; name: string } | undefined = objectCache.get('key1'); expect(typeof stringValue).toBe('string'); expect(typeof numberValue).toBe('number'); expect(typeof objectValue).toBe('object'); stringCache.destroy(); numberCache.destroy(); objectCache.destroy(); }); }); });

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/taurgis/sfcc-dev-mcp'

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