claude-code/src/utils/__tests__/memoize.test.ts
claude-code-best 21ac9e441f test: Phase 2-4 — 添加 12 个测试文件 (+321 tests, 968 total)
Phase 2 (轻 Mock): envUtils, sleep/sequential, memoize, groupToolUses, dangerousPatterns, outputLimits
Phase 3 (补全): zodToJsonSchema, PermissionMode, envValidation
Phase 4 (工具模块): mcpStringUtils, destructiveCommandWarning, commandSemantics

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-02 09:29:01 +08:00

241 lines
5.7 KiB
TypeScript

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);
});
});