Files
claude-code/src/utils/__tests__/memoize.test.ts
2026-05-01 21:39:30 +08:00

227 lines
5.2 KiB
TypeScript

import { mock, describe, expect, test, beforeEach } from 'bun:test'
import { logMock } from '../../../tests/mocks/log'
// Mock log.ts to cut the bootstrap/state dependency chain
mock.module('src/utils/log.ts', logMock)
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)
})
})