opencode/packages/app/src/utils/scoped-cache.test.ts
LukeParkerDev 33f5b80235 fix: defer reactive root disposal in cache cleanups
Same nested-dispose-in-onCleanup bug as 7f36ac2481 but in three more
places: TerminalProvider.disposeAll, PromptProvider.disposeAll, and
scoped-cache.clear() (covers viewCache.clear and comments cache.clear).
All of them synchronously call createRoot dispose() on cached entries
inside onCleanup, which during a server switch nests into the outer
cleanNode cascade and throws TypeError at chunk-*.js:992.

Snapshot the pending disposers, clear the cache synchronously, and
fire the disposers on a microtask so the outer cleanup finishes first.
2026-04-19 13:19:12 +10:00

72 lines
2 KiB
TypeScript

import { describe, expect, test } from "bun:test"
import { createScopedCache } from "./scoped-cache"
describe("createScopedCache", () => {
test("evicts least-recently-used entry when max is reached", () => {
const disposed: string[] = []
const cache = createScopedCache((key) => ({ key }), {
maxEntries: 2,
dispose: (value) => disposed.push(value.key),
})
const a = cache.get("a")
const b = cache.get("b")
expect(a.key).toBe("a")
expect(b.key).toBe("b")
cache.get("a")
const c = cache.get("c")
expect(c.key).toBe("c")
expect(cache.peek("a")?.key).toBe("a")
expect(cache.peek("b")).toBeUndefined()
expect(cache.peek("c")?.key).toBe("c")
expect(disposed).toEqual(["b"])
})
test("disposes entries on delete and clear", async () => {
const disposed: string[] = []
const cache = createScopedCache((key) => ({ key }), {
dispose: (value) => disposed.push(value.key),
})
cache.get("a")
cache.get("b")
const removed = cache.delete("a")
expect(removed?.key).toBe("a")
expect(cache.peek("a")).toBeUndefined()
cache.clear()
expect(cache.peek("b")).toBeUndefined()
// clear() defers dispose to a microtask to avoid nested cleanNode cascades
// when called from inside an onCleanup; flush the queue before asserting.
await Promise.resolve()
expect(disposed).toEqual(["a", "b"])
})
test("expires stale entries with ttl and recreates on get", () => {
let clock = 0
let count = 0
const disposed: string[] = []
const cache = createScopedCache((key) => ({ key, count: ++count }), {
ttlMs: 10,
now: () => clock,
dispose: (value) => disposed.push(`${value.key}:${value.count}`),
})
const first = cache.get("a")
expect(first.count).toBe(1)
clock = 9
expect(cache.peek("a")?.count).toBe(1)
clock = 11
expect(cache.peek("a")).toBeUndefined()
expect(disposed).toEqual(["a:1"])
const second = cache.get("a")
expect(second.count).toBe(2)
expect(disposed).toEqual(["a:1"])
})
})