import { beforeAll, describe, expect, it } from "vitest";
import type { CacheStats, ICacheLayer } from "./cache-layer.js";
describe("ICacheLayer", () => {
it("should implement basic cache operations", () => {
// Create a mock implementation to verify interface contract
const mockCache: ICacheLayer<string, number> = {
get: (key: string): number | undefined => {
if (key === "test") return 42;
return undefined;
},
set: (_key: string, _value: number): void => {
// Mock implementation
},
delete: (_key: string): void => {
// Mock implementation
},
clear: (): void => {
// Mock implementation
},
getStats: (): CacheStats => {
return {
hits: 10,
misses: 5,
size: 3,
maxSize: 100,
evictions: 2,
};
},
};
// Verify get method works with typed keys and values
expect(mockCache.get("test")).toBe(42);
expect(mockCache.get("nonexistent")).toBeUndefined();
// Verify set method accepts typed keys and values
mockCache.set("newKey", 100);
// Verify delete method accepts typed keys
mockCache.delete("test");
// Verify clear method exists and can be called
mockCache.clear();
// Verify getStats returns proper structure
const stats = mockCache.getStats();
expect(stats).toHaveProperty("hits");
expect(stats).toHaveProperty("misses");
expect(stats).toHaveProperty("size");
expect(stats).toHaveProperty("maxSize");
expect(stats).toHaveProperty("evictions");
expect(stats.hits).toBe(10);
expect(stats.misses).toBe(5);
expect(stats.size).toBe(3);
expect(stats.maxSize).toBe(100);
expect(stats.evictions).toBe(2);
});
it("should support generic key and value types", () => {
// Verify interface works with different generic types
const stringCache: ICacheLayer<string, string> = {
get: (_key: string): string | undefined => undefined,
set: (_key: string, _value: string): void => {
/* Mock implementation */
},
delete: (_key: string): void => {
/* Mock implementation */
},
clear: (): void => {
/* Mock implementation */
},
getStats: (): CacheStats => ({
hits: 0,
misses: 0,
size: 0,
maxSize: 100,
evictions: 0,
}),
};
const numberCache: ICacheLayer<number, object> = {
get: (_key: number): object | undefined => undefined,
set: (_key: number, _value: object): void => {
/* Mock implementation */
},
delete: (_key: number): void => {
/* Mock implementation */
},
clear: (): void => {
/* Mock implementation */
},
getStats: (): CacheStats => ({
hits: 0,
misses: 0,
size: 0,
maxSize: 100,
evictions: 0,
}),
};
// Type checking - these should compile without errors
expect(stringCache.get("key")).toBeUndefined();
expect(numberCache.get(123)).toBeUndefined();
});
});
describe("CacheStats", () => {
it("should have all required fields with correct types", () => {
const stats: CacheStats = {
hits: 100,
misses: 20,
size: 50,
maxSize: 1000,
evictions: 5,
};
expect(typeof stats.hits).toBe("number");
expect(typeof stats.misses).toBe("number");
expect(typeof stats.size).toBe("number");
expect(typeof stats.maxSize).toBe("number");
expect(typeof stats.evictions).toBe("number");
});
});
describe("LRUCacheLayer", () => {
// Import the class (will be implemented next)
let LRUCacheLayer: new <K, V>(maxSize?: number, ttlMs?: number) => ICacheLayer<K, V>;
beforeAll(async () => {
// Dynamic import to avoid compile-time errors during RED phase
const module = await import("./cache-layer.js");
LRUCacheLayer = module.LRUCacheLayer;
});
describe("basic operations", () => {
it("should store and retrieve values", () => {
const cache = new LRUCacheLayer<string, number>(3);
cache.set("a", 1);
cache.set("b", 2);
cache.set("c", 3);
expect(cache.get("a")).toBe(1);
expect(cache.get("b")).toBe(2);
expect(cache.get("c")).toBe(3);
});
it("should return undefined for missing keys", () => {
const cache = new LRUCacheLayer<string, number>(3);
expect(cache.get("nonexistent")).toBeUndefined();
});
it("should update existing keys", () => {
const cache = new LRUCacheLayer<string, number>(3);
cache.set("a", 1);
cache.set("a", 100);
expect(cache.get("a")).toBe(100);
});
it("should delete keys", () => {
const cache = new LRUCacheLayer<string, number>(3);
cache.set("a", 1);
cache.delete("a");
expect(cache.get("a")).toBeUndefined();
});
it("should clear all entries", () => {
const cache = new LRUCacheLayer<string, number>(3);
cache.set("a", 1);
cache.set("b", 2);
cache.clear();
expect(cache.get("a")).toBeUndefined();
expect(cache.get("b")).toBeUndefined();
expect(cache.getStats().size).toBe(0);
});
});
describe("LRU eviction", () => {
it("should evict LRU item when full", () => {
const cache = new LRUCacheLayer<string, number>(3);
cache.set("a", 1);
cache.set("b", 2);
cache.set("c", 3);
// Cache is now full [a, b, c]
// Adding 'd' should evict 'a' (least recently used)
cache.set("d", 4);
expect(cache.get("a")).toBeUndefined(); // Evicted
expect(cache.get("b")).toBe(2);
expect(cache.get("c")).toBe(3);
expect(cache.get("d")).toBe(4);
const stats = cache.getStats();
expect(stats.size).toBe(3);
expect(stats.evictions).toBe(1);
});
it("should update access order on get", () => {
const cache = new LRUCacheLayer<string, number>(3);
cache.set("a", 1);
cache.set("b", 2);
cache.set("c", 3);
// Access 'a' to make it recently used
// Order is now: [b, c, a]
cache.get("a");
// Adding 'd' should evict 'b' (now least recently used)
cache.set("d", 4);
expect(cache.get("a")).toBe(1); // Still present
expect(cache.get("b")).toBeUndefined(); // Evicted
expect(cache.get("c")).toBe(3);
expect(cache.get("d")).toBe(4);
});
it("should update access order on set for existing key", () => {
const cache = new LRUCacheLayer<string, number>(3);
cache.set("a", 1);
cache.set("b", 2);
cache.set("c", 3);
// Update 'a' to make it recently used
// Order is now: [b, c, a]
cache.set("a", 100);
// Adding 'd' should evict 'b' (now least recently used)
cache.set("d", 4);
expect(cache.get("a")).toBe(100); // Still present with updated value
expect(cache.get("b")).toBeUndefined(); // Evicted
expect(cache.get("c")).toBe(3);
expect(cache.get("d")).toBe(4);
});
});
describe("TTL expiration", () => {
it("should respect TTL on get operations", async () => {
// Create cache with 50ms TTL
const cache = new LRUCacheLayer<string, number>(3, 50);
cache.set("a", 1);
// Value should be available immediately
expect(cache.get("a")).toBe(1);
// Wait for TTL to expire
await new Promise((resolve) => setTimeout(resolve, 60));
// Value should be expired
expect(cache.get("a")).toBeUndefined();
});
it("should not count expired entries in size", async () => {
const cache = new LRUCacheLayer<string, number>(3, 50);
cache.set("a", 1);
cache.set("b", 2);
expect(cache.getStats().size).toBe(2);
// Wait for TTL to expire
await new Promise((resolve) => setTimeout(resolve, 60));
// Accessing expired items should trigger cleanup
cache.get("a");
cache.get("b");
expect(cache.getStats().size).toBe(0);
});
it("should refresh TTL on set for existing key", async () => {
const cache = new LRUCacheLayer<string, number>(3, 100);
cache.set("a", 1);
// Wait 60ms (more than half TTL)
await new Promise((resolve) => setTimeout(resolve, 60));
// Update value (should reset TTL)
cache.set("a", 2);
// Wait another 60ms (total 120ms from first set, but only 60ms from update)
await new Promise((resolve) => setTimeout(resolve, 60));
// Value should still be available because TTL was reset
expect(cache.get("a")).toBe(2);
});
});
describe("statistics tracking", () => {
it("should track hits correctly", () => {
const cache = new LRUCacheLayer<string, number>(3);
cache.set("a", 1);
cache.get("a"); // Hit
cache.get("a"); // Hit
const stats = cache.getStats();
expect(stats.hits).toBe(2);
});
it("should track misses correctly", () => {
const cache = new LRUCacheLayer<string, number>(3);
cache.get("nonexistent"); // Miss
cache.get("missing"); // Miss
const stats = cache.getStats();
expect(stats.misses).toBe(2);
});
it("should track evictions correctly", () => {
const cache = new LRUCacheLayer<string, number>(2);
cache.set("a", 1);
cache.set("b", 2);
cache.set("c", 3); // Evicts 'a'
cache.set("d", 4); // Evicts 'b'
const stats = cache.getStats();
expect(stats.evictions).toBe(2);
});
it("should track size correctly", () => {
const cache = new LRUCacheLayer<string, number>(5);
expect(cache.getStats().size).toBe(0);
cache.set("a", 1);
expect(cache.getStats().size).toBe(1);
cache.set("b", 2);
expect(cache.getStats().size).toBe(2);
cache.delete("a");
expect(cache.getStats().size).toBe(1);
cache.clear();
expect(cache.getStats().size).toBe(0);
});
it("should report maxSize correctly", () => {
const cache1 = new LRUCacheLayer<string, number>(50);
expect(cache1.getStats().maxSize).toBe(50);
const cache2 = new LRUCacheLayer<string, number>(100);
expect(cache2.getStats().maxSize).toBe(100);
});
it("should reset stats on clear", () => {
const cache = new LRUCacheLayer<string, number>(3);
cache.set("a", 1);
cache.get("a"); // Hit
cache.get("missing"); // Miss
cache.clear();
const stats = cache.getStats();
expect(stats.hits).toBe(0);
expect(stats.misses).toBe(0);
expect(stats.evictions).toBe(0);
expect(stats.size).toBe(0);
});
});
describe("configuration", () => {
it("should use default maxSize of 50", () => {
const cache = new LRUCacheLayer<string, number>();
expect(cache.getStats().maxSize).toBe(50);
});
it("should use default TTL of 24 hours", async () => {
const cache = new LRUCacheLayer<string, number>();
// We can't easily test 24 hours, but we can verify it doesn't expire quickly
cache.set("a", 1);
// Value should still be available after 100ms
await new Promise((resolve) => setTimeout(resolve, 100));
expect(cache.get("a")).toBe(1);
});
it("should accept custom maxSize", () => {
const cache = new LRUCacheLayer<string, number>(10);
expect(cache.getStats().maxSize).toBe(10);
});
it("should accept custom TTL", () => {
const cache = new LRUCacheLayer<string, number>(50, 5000);
// Implementation will store ttlMs internally - we test behavior via expiration
cache.set("a", 1);
expect(cache.get("a")).toBe(1);
});
});
});