forked from bai/curriculum-project-hub
4479f88f4f
- SenderNameCache: TTL 10min, LRU max 2048 entries, getOrFetch pattern - resolveSenderName: API call with cache integration - Trigger uses cache for approval card resolutions and audit metadata - Add unit tests (9) for cache lifecycle, LRU, expiry, and null handling
108 lines
3.0 KiB
TypeScript
108 lines
3.0 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import { SenderNameCache } from "../../src/feishu/senderCache.js";
|
|
|
|
describe("SenderNameCache", () => {
|
|
afterEach(() => {
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
it("get returns null for missing key", () => {
|
|
const cache = new SenderNameCache();
|
|
|
|
expect(cache.get("ou_user")).toBeNull();
|
|
});
|
|
|
|
it("set then get returns the name", () => {
|
|
const cache = new SenderNameCache();
|
|
|
|
cache.set("ou_user", "Alice");
|
|
|
|
expect(cache.get("ou_user")).toBe("Alice");
|
|
});
|
|
|
|
it("expired entry returns null", () => {
|
|
vi.useFakeTimers();
|
|
vi.setSystemTime(new Date("2026-01-01T00:00:00Z"));
|
|
const cache = new SenderNameCache({ ttlMs: 100 });
|
|
|
|
cache.set("ou_user", "Alice");
|
|
vi.advanceTimersByTime(101);
|
|
|
|
expect(cache.get("ou_user")).toBeNull();
|
|
expect(cache.size()).toBe(0);
|
|
});
|
|
|
|
it("getOrFetch uses cache on hit", async () => {
|
|
const cache = new SenderNameCache();
|
|
const fetchFn = vi.fn(async () => "Fetched Alice");
|
|
cache.set("ou_user", "Alice");
|
|
|
|
await expect(cache.getOrFetch("ou_user", fetchFn)).resolves.toBe("Alice");
|
|
|
|
expect(fetchFn).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("getOrFetch calls fetchFn on miss", async () => {
|
|
const cache = new SenderNameCache();
|
|
const fetchFn = vi.fn(async () => "Alice");
|
|
|
|
await expect(cache.getOrFetch("ou_user", fetchFn)).resolves.toBe("Alice");
|
|
|
|
expect(fetchFn).toHaveBeenCalledTimes(1);
|
|
expect(fetchFn).toHaveBeenCalledWith("ou_user");
|
|
});
|
|
|
|
it("getOrFetch caches the result of fetchFn", async () => {
|
|
const cache = new SenderNameCache();
|
|
const fetchFn = vi.fn(async () => "Alice");
|
|
|
|
await expect(cache.getOrFetch("ou_user", fetchFn)).resolves.toBe("Alice");
|
|
await expect(cache.getOrFetch("ou_user", fetchFn)).resolves.toBe("Alice");
|
|
|
|
expect(fetchFn).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("evicts the least recently used entry when full", () => {
|
|
const cache = new SenderNameCache({ maxSize: 2 });
|
|
|
|
cache.set("ou_1", "Alice");
|
|
cache.set("ou_2", "Bob");
|
|
expect(cache.get("ou_1")).toBe("Alice");
|
|
cache.set("ou_3", "Carol");
|
|
|
|
expect(cache.get("ou_1")).toBe("Alice");
|
|
expect(cache.get("ou_2")).toBeNull();
|
|
expect(cache.get("ou_3")).toBe("Carol");
|
|
});
|
|
|
|
it("clear empties the cache", () => {
|
|
const cache = new SenderNameCache();
|
|
|
|
cache.set("ou_user", "Alice");
|
|
cache.clear();
|
|
|
|
expect(cache.get("ou_user")).toBeNull();
|
|
expect(cache.size()).toBe(0);
|
|
});
|
|
|
|
it("size returns correct count", () => {
|
|
const cache = new SenderNameCache();
|
|
|
|
cache.set("ou_1", "Alice");
|
|
cache.set("ou_2", "Bob");
|
|
|
|
expect(cache.size()).toBe(2);
|
|
});
|
|
|
|
it("getOrFetch with null fetchFn result does not cache null", async () => {
|
|
const cache = new SenderNameCache();
|
|
const fetchFn = vi.fn(async () => null);
|
|
|
|
await expect(cache.getOrFetch("ou_user", fetchFn)).resolves.toBeNull();
|
|
await expect(cache.getOrFetch("ou_user", fetchFn)).resolves.toBeNull();
|
|
|
|
expect(fetchFn).toHaveBeenCalledTimes(2);
|
|
expect(cache.size()).toBe(0);
|
|
});
|
|
});
|