import { mock, describe, expect, test, beforeEach } from "bun:test"; // Mock heavy deps before importing memoize mock.module("src/utils/log.ts", () => ({ logError: () => {}, logToFile: () => {}, getLogDisplayTitle: () => "", logEvent: () => {}, })); mock.module("src/utils/slowOperations.ts", () => ({ jsonStringify: JSON.stringify, jsonParse: JSON.parse, slowLogging: { enabled: false }, clone: (v: any) => structuredClone(v), cloneDeep: (v: any) => structuredClone(v), callerFrame: () => "", SLOW_OPERATION_THRESHOLD_MS: 100, writeFileSync_DEPRECATED: () => {}, })); const { memoizeWithTTL, memoizeWithTTLAsync, memoizeWithLRU } = await import( "../memoize" ); // ─── memoizeWithTTL ──────────────────────────────────────────────────── describe("memoizeWithTTL", () => { test("returns cached value on second call", () => { let calls = 0; const fn = memoizeWithTTL((x: number) => { calls++; return x * 2; }, 60_000); expect(fn(5)).toBe(10); expect(fn(5)).toBe(10); expect(calls).toBe(1); }); test("different args get separate cache entries", () => { let calls = 0; const fn = memoizeWithTTL((x: number) => { calls++; return x + 1; }, 60_000); expect(fn(1)).toBe(2); expect(fn(2)).toBe(3); expect(calls).toBe(2); }); test("cache.clear empties the cache", () => { let calls = 0; const fn = memoizeWithTTL(() => { calls++; return "val"; }, 60_000); fn(); fn.cache.clear(); fn(); expect(calls).toBe(2); }); test("returns stale value and triggers background refresh after TTL", async () => { let calls = 0; const fn = memoizeWithTTL((x: number) => { calls++; return x * calls; }, 1); // 1ms TTL const first = fn(10); expect(first).toBe(10); // calls=1, 10*1 // Wait for TTL to expire await new Promise((r) => setTimeout(r, 10)); // Should return stale value (10) and trigger background refresh const second = fn(10); expect(second).toBe(10); // stale value returned immediately // Wait for background refresh microtask await new Promise((r) => setTimeout(r, 10)); // Now cache should have refreshed value (calls=2 during refresh, 10*2=20) const third = fn(10); expect(third).toBe(20); }); }); // ─── memoizeWithTTLAsync ─────────────────────────────────────────────── describe("memoizeWithTTLAsync", () => { test("caches async result", async () => { let calls = 0; const fn = memoizeWithTTLAsync(async (x: number) => { calls++; return x * 2; }, 60_000); expect(await fn(5)).toBe(10); expect(await fn(5)).toBe(10); expect(calls).toBe(1); }); test("deduplicates concurrent cold-miss calls", async () => { let calls = 0; const fn = memoizeWithTTLAsync(async (x: number) => { calls++; await new Promise((r) => setTimeout(r, 20)); return x; }, 60_000); const [a, b, c] = await Promise.all([fn(1), fn(1), fn(1)]); expect(a).toBe(1); expect(b).toBe(1); expect(c).toBe(1); expect(calls).toBe(1); }); test("cache.clear forces re-computation", async () => { let calls = 0; const fn = memoizeWithTTLAsync(async () => { calls++; return "v"; }, 60_000); await fn(); fn.cache.clear(); await fn(); expect(calls).toBe(2); }); test("returns stale value on TTL expiry", async () => { let calls = 0; const fn = memoizeWithTTLAsync(async () => { calls++; return calls; }, 1); // 1ms TTL const first = await fn(); expect(first).toBe(1); await new Promise((r) => setTimeout(r, 10)); // Should return stale value (1) immediately const second = await fn(); expect(second).toBe(1); }); }); // ─── memoizeWithLRU ──────────────────────────────────────────────────── describe("memoizeWithLRU", () => { test("caches results by key", () => { let calls = 0; const fn = memoizeWithLRU( (x: number) => { calls++; return x * 2; }, (x) => String(x), 10 ); expect(fn(5)).toBe(10); expect(fn(5)).toBe(10); expect(calls).toBe(1); }); test("evicts least recently used when max reached", () => { let calls = 0; const fn = memoizeWithLRU( (x: number) => { calls++; return x; }, (x) => String(x), 3 ); fn(1); fn(2); fn(3); expect(calls).toBe(3); fn(4); // evicts key "1" expect(fn.cache.has("1")).toBe(false); expect(fn.cache.has("4")).toBe(true); }); test("cache.size returns current size", () => { const fn = memoizeWithLRU( (x: number) => x, (x) => String(x), 10 ); fn(1); fn(2); expect(fn.cache.size()).toBe(2); }); test("cache.delete removes entry", () => { const fn = memoizeWithLRU( (x: number) => x, (x) => String(x), 10 ); fn(1); expect(fn.cache.has("1")).toBe(true); fn.cache.delete("1"); expect(fn.cache.has("1")).toBe(false); }); test("cache.get returns value without updating recency", () => { const fn = memoizeWithLRU( (x: number) => x * 10, (x) => String(x), 10 ); fn(5); expect(fn.cache.get("5")).toBe(50); }); test("cache.clear empties everything", () => { const fn = memoizeWithLRU( (x: number) => x, (x) => String(x), 10 ); fn(1); fn(2); fn.cache.clear(); expect(fn.cache.size()).toBe(0); }); });