111 lines
3.6 KiB
TypeScript
111 lines
3.6 KiB
TypeScript
import { describe, expect, test } from "bun:test"
|
|||
|
|
|
||
|
|
import { pickDirectoriesToEvict } from "./eviction"
|
||
|
|
import { DIR_IDLE_TTL_MS, EVICTION_GRACE_MS, MAX_DIR_STORES } from "./types"
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Regression coverage for the sidebar cache-thrash loop (issue #1472).
|
||
|
|
*
|
||
|
|
* Expanding a project with many worktrees mounts a sidebar row per directory.
|
||
|
|
* Each row calls `ensureChild` during render, while the pin that protects it is
|
||
|
|
* only taken in an effect after commit. With more live directories than the
|
||
|
|
* store limit, overflow eviction therefore disposed directories that were
|
||
|
|
* actively being rendered; the next render recreated them with a `loading`
|
||
|
|
* status, which issued another bootstrap request, and the cycle repeated
|
||
|
|
* indefinitely.
|
||
|
|
*
|
||
|
|
* The fix treats the limit as a soft target: a directory touched within the
|
||
|
|
* grace window is never an overflow victim, so a burst of live directories
|
||
|
|
* overflows the cache briefly instead of thrashing. Idle directories stay
|
||
|
|
* evictable, which is what keeps the cache bounded.
|
||
|
|
*/
|
||
|
|
|
||
|
|
const buildState = (directories: string[], lastAccessAt: number) =>
|
||
|
|
new Map(directories.map((directory) => [directory, { lastAccessAt }]))
|
||
|
|
|
||
|
|
const directories = (count: number, prefix = "/repo/worktree-") =>
|
||
|
|
Array.from({ length: count }, (_, index) => `${prefix}${index}`)
|
||
|
|
|
||
|
|
describe("directory eviction under sidebar expansion", () => {
|
||
|
|
test("does not evict directories that are being accessed right now", () => {
|
||
|
|
const now = 1_000_000
|
||
|
|
const stores = directories(MAX_DIR_STORES + 25)
|
||
|
|
|
||
|
|
const evicted = pickDirectoriesToEvict({
|
||
|
|
stores,
|
||
|
|
state: buildState(stores, now),
|
||
|
|
pins: new Set(),
|
||
|
|
max: MAX_DIR_STORES,
|
||
|
|
ttl: DIR_IDLE_TTL_MS,
|
||
|
|
graceMs: EVICTION_GRACE_MS,
|
||
|
|
now,
|
||
|
|
})
|
||
|
|
|
||
|
|
expect(evicted).toEqual([])
|
||
|
|
})
|
||
|
|
|
||
|
|
test("still evicts overflow once directories fall outside the grace window", () => {
|
||
|
|
const now = 1_000_000
|
||
|
|
const live = directories(MAX_DIR_STORES, "/repo/live-")
|
||
|
|
const stale = directories(5, "/repo/stale-")
|
||
|
|
const state = new Map([
|
||
|
|
...buildState(live, now),
|
||
|
|
...buildState(stale, now - EVICTION_GRACE_MS - 1),
|
||
|
|
])
|
||
|
|
|
||
|
|
const evicted = pickDirectoriesToEvict({
|
||
|
|
stores: [...live, ...stale],
|
||
|
|
state,
|
||
|
|
pins: new Set(),
|
||
|
|
max: MAX_DIR_STORES,
|
||
|
|
ttl: DIR_IDLE_TTL_MS,
|
||
|
|
graceMs: EVICTION_GRACE_MS,
|
||
|
|
now,
|
||
|
|
})
|
||
|
|
|
||
|
|
expect([...evicted].sort()).toEqual([...stale].sort())
|
||
|
|
})
|
||
|
|
|
||
|
|
test("still evicts directories idle past the TTL even inside the limit", () => {
|
||
|
|
const now = 1_000_000
|
||
|
|
const active = directories(3, "/repo/active-")
|
||
|
|
const abandoned = directories(2, "/repo/abandoned-")
|
||
|
|
const state = new Map([
|
||
|
|
...buildState(active, now),
|
||
|
|
...buildState(abandoned, now - DIR_IDLE_TTL_MS - 1),
|
||
|
|
])
|
||
|
|
|
||
|
|
const evicted = pickDirectoriesToEvict({
|
||
|
|
stores: [...active, ...abandoned],
|
||
|
|
state,
|
||
|
|
pins: new Set(),
|
||
|
|
max: MAX_DIR_STORES,
|
||
|
|
ttl: DIR_IDLE_TTL_MS,
|
||
|
|
graceMs: EVICTION_GRACE_MS,
|
||
|
|
now,
|
||
|
|
})
|
||
|
|
|
||
|
|
expect([...evicted].sort()).toEqual([...abandoned].sort())
|
||
|
|
})
|
||
|
|
|
||
|
|
test("never evicts pinned or blocked directories regardless of overflow", () => {
|
||
|
|
const now = 1_000_000
|
||
|
|
const stores = directories(MAX_DIR_STORES + 10)
|
||
|
|
const stale = now - EVICTION_GRACE_MS - 1
|
||
|
|
|
||
|
|
const evicted = pickDirectoriesToEvict({
|
||
|
|
stores,
|
||
|
|
state: buildState(stores, stale),
|
||
|
|
pins: new Set([stores[0]]),
|
||
|
|
max: MAX_DIR_STORES,
|
||
|
|
ttl: DIR_IDLE_TTL_MS,
|
||
|
|
graceMs: EVICTION_GRACE_MS,
|
||
|
|
now,
|
||
|
|
hasPendingBlockingRequests: (directory) => directory === stores[1],
|
||
|
|
})
|
||
|
|
|
||
|
|
expect(evicted).not.toContain(stores[0])
|
||
|
|
expect(evicted).not.toContain(stores[1])
|
||
|
|
})
|
||
|
|
})
|