import { describe, expect, it, vi } from "vitest"; vi.mock("@/lib/redis", () => ({ redis: null, })); import { cached, invalidateMemory } from "./cache"; describe("cached (memory-only, no Redis)", () => { it("computes once and serves the cached value", async () => { const fn = vi.fn(async () => 42); const key = `once-${Math.random()}`; expect(await cached(key, 10_000, fn)).toBe(42); expect(await cached(key, 10_000, fn)).toBe(42); expect(fn).toHaveBeenCalledTimes(1); }); it("dedupes concurrent misses into a single computation", async () => { let resolveFn: (value: number) => void = () => {}; const fn = vi.fn( () => new Promise((resolve) => { resolveFn = resolve; }), ); const key = `dedupe-${Math.random()}`; const p1 = cached(key, 10_000, fn); const p2 = cached(key, 10_000, fn); const p3 = cached(key, 10_000, fn); resolveFn(7); expect(await Promise.all([p1, p2, p3])).toEqual([7, 7, 7]); expect(fn).toHaveBeenCalledTimes(1); }); it("recomputes after the TTL expires", async () => { vi.useFakeTimers(); try { vi.setSystemTime(1_000); let count = 0; const fn = vi.fn(async () => ++count); const key = `ttl-${Math.random()}`; expect(await cached(key, 100, fn)).toBe(1); expect(await cached(key, 100, fn)).toBe(1); vi.advanceTimersByTime(100); expect(await cached(key, 100, fn)).toBe(2); expect(fn).toHaveBeenCalledTimes(2); } finally { vi.useRealTimers(); } }); it("invalidates a key so the next read recomputes", async () => { let count = 0; const fn = vi.fn(async () => ++count); const key = `inv-${Math.random()}`; expect(await cached(key, 10_000, fn)).toBe(1); invalidateMemory(key); expect(await cached(key, 10_000, fn)).toBe(2); expect(fn).toHaveBeenCalledTimes(2); }); it("evicts the oldest entry when the in-memory cache is full", async () => { const fn = vi.fn(async () => 1); for (let i = 0; i < 600; i++) { await cached(`bulk-${i}`, 10_000, fn); } // Key "bulk-0" was evicted (insertion order), so it must recompute. await cached("bulk-0", 10_000, fn); expect(fn).toHaveBeenCalledTimes(601); }); });