import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { MemoryCache, KVCache } from '../../lib/core/cache.js';
describe('MemoryCache', () => {
let cache: MemoryCache;
beforeEach(() => {
// Small cache with short TTL for testing
cache = new MemoryCache(3, 100); // 3 entries, 100ms TTL
});
afterEach(() => {
cache.clear();
});
describe('Basic operations', () => {
it('should store and retrieve values', () => {
cache.set('key1', 'value1');
expect(cache.get('key1')).toBe('value1');
});
it('should return null for non-existent keys', () => {
expect(cache.get('nonexistent')).toBeNull();
});
it('should handle different data types', () => {
// Use a larger cache to avoid eviction
const largeCache = new MemoryCache(10, 1000);
largeCache.set('string', 'value');
largeCache.set('number', 42);
largeCache.set('object', { foo: 'bar' });
largeCache.set('array', [1, 2, 3]);
largeCache.set('boolean', true);
expect(largeCache.get('string')).toBe('value');
expect(largeCache.get('number')).toBe(42);
expect(largeCache.get('object')).toEqual({ foo: 'bar' });
expect(largeCache.get('array')).toEqual([1, 2, 3]);
expect(largeCache.get('boolean')).toBe(true);
largeCache.clear();
});
it('should overwrite existing keys', () => {
cache.set('key1', 'value1');
cache.set('key1', 'value2');
expect(cache.get('key1')).toBe('value2');
});
it('should handle null and undefined values', () => {
cache.set('null', null);
cache.set('undefined', undefined);
expect(cache.get('null')).toBe(null);
expect(cache.get('undefined')).toBe(undefined);
});
});
describe('TTL (Time To Live)', () => {
it('should expire entries after TTL', async () => {
cache.set('key1', 'value1');
expect(cache.get('key1')).toBe('value1');
// Wait for TTL to expire
await new Promise(resolve => setTimeout(resolve, 150));
expect(cache.get('key1')).toBeNull();
});
it('should not expire entries before TTL', async () => {
cache.set('key1', 'value1');
// Wait less than TTL
await new Promise(resolve => setTimeout(resolve, 50));
expect(cache.get('key1')).toBe('value1');
});
it('should handle multiple entries with different expiration times', async () => {
cache.set('key1', 'value1');
await new Promise(resolve => setTimeout(resolve, 50));
cache.set('key2', 'value2');
await new Promise(resolve => setTimeout(resolve, 50));
cache.set('key3', 'value3');
// At this point: key1 ~100ms old, key2 ~50ms old, key3 ~0ms old
await new Promise(resolve => setTimeout(resolve, 20));
// key1 should be expired, others still valid
expect(cache.get('key1')).toBeNull();
expect(cache.get('key2')).toBe('value2');
expect(cache.get('key3')).toBe('value3');
});
it('should reset TTL when overwriting a key', async () => {
cache.set('key1', 'value1');
await new Promise(resolve => setTimeout(resolve, 80));
// Overwrite before expiration
cache.set('key1', 'value2');
await new Promise(resolve => setTimeout(resolve, 50));
// Should still be valid (new TTL)
expect(cache.get('key1')).toBe('value2');
});
});
describe('LRU (Least Recently Used) Eviction', () => {
it('should evict LRU entry when cache is full', () => {
// Fill cache to capacity (3 entries)
cache.set('key1', 'value1');
cache.set('key2', 'value2');
cache.set('key3', 'value3');
// Add 4th entry, should evict key1 (oldest)
cache.set('key4', 'value4');
expect(cache.get('key1')).toBeNull();
expect(cache.get('key2')).toBe('value2');
expect(cache.get('key3')).toBe('value3');
expect(cache.get('key4')).toBe('value4');
});
it('should not evict when overwriting existing key', () => {
cache.set('key1', 'value1');
cache.set('key2', 'value2');
cache.set('key3', 'value3');
// Overwrite existing key (shouldn't trigger eviction)
cache.set('key2', 'value2-updated');
expect(cache.get('key1')).toBe('value1');
expect(cache.get('key2')).toBe('value2-updated');
expect(cache.get('key3')).toBe('value3');
});
it('should update access order on get', () => {
cache.set('key1', 'value1');
cache.set('key2', 'value2');
cache.set('key3', 'value3');
// Access key1, making it most recently used
cache.get('key1');
// Add 4th entry, should evict key2 (now oldest)
cache.set('key4', 'value4');
expect(cache.get('key1')).toBe('value1'); // Kept (recently accessed)
expect(cache.get('key2')).toBeNull(); // Evicted
expect(cache.get('key3')).toBe('value3');
expect(cache.get('key4')).toBe('value4');
});
it('should evict correct entry with multiple accesses', () => {
cache.set('key1', 'value1');
cache.set('key2', 'value2');
cache.set('key3', 'value3');
// Access pattern: key3, key1, key2
cache.get('key3');
cache.get('key1');
cache.get('key2');
// Add 4th entry, should evict key3 (accessed first, but oldest overall)
// Wait... actually key3 was accessed most recently among the original sets
// Let me reconsider...
cache.set('keyA', 'A');
cache.set('keyB', 'B');
cache.set('keyC', 'C');
// Don't access any, just add another
cache.set('keyD', 'D');
// keyA should be evicted (least recently set)
expect(cache.get('keyA')).toBeNull();
});
it('should handle eviction with set operations updating access', () => {
cache.set('key1', 'value1');
cache.set('key2', 'value2');
cache.set('key3', 'value3');
// Update key1 (making it most recent)
cache.set('key1', 'value1-updated');
// Add new entry, should evict key2
cache.set('key4', 'value4');
expect(cache.get('key1')).toBe('value1-updated');
expect(cache.get('key2')).toBeNull();
expect(cache.get('key3')).toBe('value3');
expect(cache.get('key4')).toBe('value4');
});
});
describe('clear()', () => {
it('should remove all entries', () => {
cache.set('key1', 'value1');
cache.set('key2', 'value2');
cache.set('key3', 'value3');
cache.clear();
expect(cache.get('key1')).toBeNull();
expect(cache.get('key2')).toBeNull();
expect(cache.get('key3')).toBeNull();
});
it('should allow adding entries after clear', () => {
cache.set('key1', 'value1');
cache.clear();
cache.set('key2', 'value2');
expect(cache.get('key2')).toBe('value2');
});
it('should reset access counter', () => {
// Fill and clear cache
cache.set('key1', 'value1');
cache.set('key2', 'value2');
cache.clear();
// Add entries again, should work normally
cache.set('key1', 'new1');
cache.set('key2', 'new2');
cache.set('key3', 'new3');
expect(cache.get('key1')).toBe('new1');
expect(cache.get('key2')).toBe('new2');
expect(cache.get('key3')).toBe('new3');
});
});
describe('entries()', () => {
it('should return all valid entries', () => {
cache.set('key1', 'value1');
cache.set('key2', 'value2');
cache.set('key3', 'value3');
const entries = cache.entries();
expect(entries).toHaveLength(3);
expect(entries).toContainEqual(['key1', 'value1']);
expect(entries).toContainEqual(['key2', 'value2']);
expect(entries).toContainEqual(['key3', 'value3']);
});
it('should not return expired entries', async () => {
cache.set('key1', 'value1');
cache.set('key2', 'value2');
// Wait for expiration
await new Promise(resolve => setTimeout(resolve, 150));
const entries = cache.entries();
expect(entries).toHaveLength(0);
});
it('should return empty array for empty cache', () => {
const entries = cache.entries();
expect(entries).toEqual([]);
});
it('should cleanup expired entries when called', async () => {
cache.set('key1', 'value1');
await new Promise(resolve => setTimeout(resolve, 50));
cache.set('key2', 'value2');
await new Promise(resolve => setTimeout(resolve, 80));
// key1 expired, key2 still valid
const entries = cache.entries();
expect(entries).toHaveLength(1);
expect(entries).toContainEqual(['key2', 'value2']);
});
});
describe('Edge cases', () => {
it('should handle cache with size 0', () => {
const tinyCache = new MemoryCache(0, 100);
// Cache with size 0: first item is stored
tinyCache.set('key1', 'value1');
expect(tinyCache.get('key1')).toBe('value1');
// Adding second item triggers eviction of first (cache.size >= maxSize)
tinyCache.set('key2', 'value2');
expect(tinyCache.get('key1')).toBeNull(); // Evicted
expect(tinyCache.get('key2')).toBe('value2'); // Current item
tinyCache.clear();
});
it('should handle cache with size 1', () => {
const singleCache = new MemoryCache(1, 100);
singleCache.set('key1', 'value1');
expect(singleCache.get('key1')).toBe('value1');
singleCache.set('key2', 'value2');
expect(singleCache.get('key1')).toBeNull();
expect(singleCache.get('key2')).toBe('value2');
singleCache.clear();
});
it('should handle very short TTL', async () => {
const shortCache = new MemoryCache(3, 10); // 10ms TTL
shortCache.set('key1', 'value1');
await new Promise(resolve => setTimeout(resolve, 20));
expect(shortCache.get('key1')).toBeNull();
shortCache.clear();
});
it('should handle very long TTL', () => {
const longCache = new MemoryCache(3, 86400000); // 24 hours
longCache.set('key1', 'value1');
expect(longCache.get('key1')).toBe('value1');
longCache.clear();
});
it('should handle special characters in keys', () => {
cache.set('key/with/slashes', 'value1');
cache.set('key:with:colons', 'value2');
cache.set('key.with.dots', 'value3');
expect(cache.get('key/with/slashes')).toBe('value1');
expect(cache.get('key:with:colons')).toBe('value2');
expect(cache.get('key.with.dots')).toBe('value3');
});
it('should handle empty string keys', () => {
cache.set('', 'empty-key-value');
expect(cache.get('')).toBe('empty-key-value');
});
it('should handle very long keys', () => {
const longKey = 'a'.repeat(1000);
cache.set(longKey, 'value');
expect(cache.get(longKey)).toBe('value');
});
it('should handle very large values', () => {
const largeValue = { data: 'x'.repeat(10000) };
cache.set('large', largeValue);
expect(cache.get('large')).toEqual(largeValue);
});
});
describe('Memory management', () => {
it('should clean up expired entries on get', async () => {
cache.set('key1', 'value1');
cache.set('key2', 'value2');
await new Promise(resolve => setTimeout(resolve, 150));
// This get should trigger cleanup of key1
expect(cache.get('key1')).toBeNull();
// Verify internal map is cleaned
const entries = cache.entries();
expect(entries).toHaveLength(0);
});
});
});
describe('KVCache', () => {
let kvCache: KVCache;
let mockKV: any;
beforeEach(() => {
// Create mock KV storage
const storage = new Map<string, string>();
mockKV = {
get: vi.fn(async (key: string) => storage.get(key) || null),
put: vi.fn(async (key: string, value: string) => storage.set(key, value)),
delete: vi.fn(async (key: string) => storage.delete(key)),
};
kvCache = new KVCache(mockKV, 100); // 100ms TTL
});
describe('Basic operations', () => {
it('should store and retrieve values', async () => {
await kvCache.set('key1', 'value1');
const result = await kvCache.get('key1');
expect(result).toBe('value1');
});
it('should return null for non-existent keys', async () => {
const result = await kvCache.get('nonexistent');
expect(result).toBeNull();
});
it('should handle different data types', async () => {
await kvCache.set('string', 'value');
await kvCache.set('number', 42);
await kvCache.set('object', { foo: 'bar' });
await kvCache.set('array', [1, 2, 3]);
expect(await kvCache.get('string')).toBe('value');
expect(await kvCache.get('number')).toBe(42);
expect(await kvCache.get('object')).toEqual({ foo: 'bar' });
expect(await kvCache.get('array')).toEqual([1, 2, 3]);
});
it('should call KV methods correctly', async () => {
await kvCache.set('key1', 'value1');
expect(mockKV.put).toHaveBeenCalled();
await kvCache.get('key1');
expect(mockKV.get).toHaveBeenCalledWith('key1');
});
});
describe('TTL (Time To Live)', () => {
it('should expire entries after TTL', async () => {
await kvCache.set('key1', 'value1');
expect(await kvCache.get('key1')).toBe('value1');
// Wait for TTL to expire
await new Promise(resolve => setTimeout(resolve, 150));
expect(await kvCache.get('key1')).toBeNull();
});
it('should not expire entries before TTL', async () => {
await kvCache.set('key1', 'value1');
// Wait less than TTL
await new Promise(resolve => setTimeout(resolve, 50));
expect(await kvCache.get('key1')).toBe('value1');
});
});
describe('clear()', () => {
it('should be callable', async () => {
await kvCache.set('key1', 'value1');
await expect(kvCache.clear()).resolves.toBeUndefined();
});
});
});