codeburn/tests/session-cache.test.ts
ozymandiashh a67bd279a6 test(cache): make the round-2 review findings fail when broken
Round 2 of the independent review proved five things by mutation: it broke the
behavior and the tests stayed green. Every one is now pinned.

The most important invariant in this change was the least guarded. Copilot must
have NO entry in PROVIDER_ENV_VARS - declaring any of its nine reads moves its
fingerprint and re-opens the durable history-loss path - but only one of the
nine was covered, so declaring any of the other eight passed the whole suite.
Now the absence of the entry is asserted directly, and all nine vars are
table-tested for fingerprint stability.

Doctor stops blaming parse-only overrides for a failed discovery.
CODEBURN_CURSOR_MAX_BUBBLES caps how many bubbles Cursor parses and
KIMI_MODEL_NAME renames an attributed model; neither relocates anything, so
"NOTHING FOUND (override CODEBURN_CURSOR_MAX_BUBBLES set...)" pointed the user
at the wrong thing. Both join NON_DISCOVERY_ENV_VARS, which exists for exactly
this, and both still appear in Details - only the verdict's blame line changes.

The secret-redaction and ambient-suppression tests are table-driven over both
names each covers, since removing either second name (VERCEL_OIDC_TOKEN,
LOCALAPPDATA) previously leaked or surfaced it with every test still passing.

The changelog no longer claims a one-time re-parse for the Vercel gateway: it
is a network provider re-fetched on every writable run, so its declaration is a
read-only-path correction, not a migration. Fourteen file-backed providers
migrate once.
2026-08-05 05:58:17 +03:00

884 lines
34 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it } from 'vitest'
import { readFile, rm, utimes, writeFile, mkdir } from 'fs/promises'
import { existsSync } from 'fs'
import { tmpdir } from 'os'
import { basename, join } from 'path'
import {
CACHE_VERSION,
PROVIDER_ENV_VARS,
type CachedCall,
type CachedFile,
type CachedTurn,
type FileFingerprint,
type SessionCache,
cleanupOrphanedTempFiles,
computeEnvFingerprint,
emptyCache,
fingerprintFile,
loadCache,
mergeCallByDedupKey,
reconcileFile,
saveCache,
sessionCachePath,
} from '../src/session-cache.js'
// Version-suffixed filename (e.g. session-cache.v5.json) the cache now writes to.
const CACHE_FILE = () => basename(sessionCachePath())
const TMP_DIR = join(tmpdir(), `codeburn-scache-test-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`)
beforeEach(() => {
process.env['CODEBURN_CACHE_DIR'] = TMP_DIR
})
afterEach(async () => {
if (existsSync(TMP_DIR)) await rm(TMP_DIR, { recursive: true })
})
function makeCall(overrides: Partial<CachedCall> = {}): CachedCall {
return {
provider: 'claude',
model: 'claude-sonnet-4-20250514',
usage: {
inputTokens: 1000,
outputTokens: 500,
cacheCreationInputTokens: 0,
cacheReadInputTokens: 0,
cachedInputTokens: 0,
reasoningTokens: 0,
webSearchRequests: 0,
cacheCreationOneHourTokens: 0,
},
speed: 'standard',
timestamp: '2026-05-15T10:00:00Z',
tools: ['Read', 'Edit'],
bashCommands: [],
skills: [],
deduplicationKey: 'msg-abc123',
...overrides,
}
}
function makeTurn(overrides: Partial<CachedTurn> = {}): CachedTurn {
return {
timestamp: '2026-05-15T10:00:00Z',
sessionId: 'sess-1',
userMessage: 'fix the bug',
calls: [makeCall()],
...overrides,
}
}
function makeCachedFile(overrides: Partial<CachedFile> = {}): CachedFile {
return {
fingerprint: { dev: 1, ino: 100, mtimeMs: 1000, sizeBytes: 5000 },
mcpInventory: [],
turns: [makeTurn()],
...overrides,
}
}
// ── emptyCache ─────────────────────────────────────────────────────────
describe('emptyCache', () => {
it('returns a valid empty cache', () => {
const cache = emptyCache()
expect(cache.version).toBe(CACHE_VERSION)
expect(cache.providers).toEqual({})
})
})
// ── loadCache / saveCache ──────────────────────────────────────────────
describe('loadCache / saveCache', () => {
it('returns empty cache when no file exists', async () => {
const cache = await loadCache()
expect(cache.version).toBe(CACHE_VERSION)
expect(cache.providers).toEqual({})
})
it('round-trips a cache through save and load', async () => {
const cache: SessionCache = {
version: CACHE_VERSION,
providers: {
claude: {
envFingerprint: 'abc123',
files: {
'/path/to/session.jsonl': makeCachedFile(),
},
},
},
}
await saveCache(cache)
const loaded = await loadCache()
expect(loaded).toEqual(cache)
})
it('persists a failed-parse marker across save/load (negative-result cache)', async () => {
const cache: SessionCache = {
version: CACHE_VERSION,
providers: {
pi: {
envFingerprint: 'abc123',
files: {
'/path/to/bad.jsonl': makeCachedFile({ turns: [], failed: true }),
},
},
},
}
await saveCache(cache)
const loaded = await loadCache()
// The `failed` flag and empty turns survive validation + load, so the file
// stays skipped on the next run instead of being re-read and re-thrown.
expect(loaded.providers['pi']?.files['/path/to/bad.jsonl']?.failed).toBe(true)
expect(loaded.providers['pi']?.files['/path/to/bad.jsonl']?.turns).toEqual([])
})
it('preserves the estimated-cost flag through save/load; a measured call stays unflagged', async () => {
const cache: SessionCache = {
version: CACHE_VERSION,
providers: {
warp: {
envFingerprint: 'abc123',
files: {
'/path/to/warp.sqlite': makeCachedFile({
turns: [makeTurn({ calls: [
makeCall({ deduplicationKey: 'est', costUSD: 0.5, isEstimated: true }),
makeCall({ deduplicationKey: 'measured', costUSD: 0.5 }),
] })],
}),
},
},
},
}
await saveCache(cache)
const loaded = await loadCache()
const calls = loaded.providers['warp']?.files['/path/to/warp.sqlite']?.turns[0]?.calls
expect(calls?.[0]?.isEstimated).toBe(true)
// A call with no flag round-trips as undefined, not silently coerced to true.
expect(calls?.[1]?.isEstimated).toBeUndefined()
})
it('returns empty cache on version mismatch', async () => {
const bad: SessionCache = { version: 999, providers: { claude: { envFingerprint: 'x', files: {} } } }
await mkdir(TMP_DIR, { recursive: true })
await writeFile(join(TMP_DIR, 'session-cache.json'), JSON.stringify(bad))
const loaded = await loadCache()
expect(loaded.version).toBe(CACHE_VERSION)
expect(loaded.providers).toEqual({})
})
it('returns empty cache on corrupt JSON', async () => {
await mkdir(TMP_DIR, { recursive: true })
await writeFile(join(TMP_DIR, 'session-cache.json'), '{broken')
const loaded = await loadCache()
expect(loaded.version).toBe(CACHE_VERSION)
expect(loaded.providers).toEqual({})
})
it('atomic write does not leave partial file on error', async () => {
await saveCache(emptyCache())
const raw = await readFile(sessionCachePath(), 'utf-8')
expect(JSON.parse(raw)).toEqual(emptyCache())
})
})
// ── versioned filename + legacy adoption ───────────────────────────────
describe('versioned cache file + legacy adoption', () => {
function validCache(): SessionCache {
return {
version: CACHE_VERSION,
providers: { claude: { envFingerprint: 'abc123', files: { '/path/to/session.jsonl': makeCachedFile() } } },
}
}
it('writes and reads the version-suffixed file, never the legacy name', async () => {
expect(basename(sessionCachePath())).toBe(`session-cache.v${CACHE_VERSION}.json`)
await saveCache(validCache())
expect(existsSync(sessionCachePath())).toBe(true)
expect(existsSync(join(TMP_DIR, 'session-cache.json'))).toBe(false)
expect(await loadCache()).toEqual(validCache())
})
it('adopts a matching-version legacy file once, without deleting or rewriting it', async () => {
await mkdir(TMP_DIR, { recursive: true })
const legacy = join(TMP_DIR, 'session-cache.json')
await writeFile(legacy, JSON.stringify(validCache()))
// Versioned file absent → adopt-copy from legacy on first load.
expect(await loadCache()).toEqual(validCache())
expect(existsSync(sessionCachePath())).toBe(true)
// Legacy left intact (not deleted, not rewritten).
expect(existsSync(legacy)).toBe(true)
expect(JSON.parse(await readFile(legacy, 'utf-8'))).toEqual(validCache())
// Adoption is one-time: a later legacy edit is ignored once the versioned
// file exists.
const mutated: SessionCache = { version: CACHE_VERSION, providers: { codex: { envFingerprint: 'zzz', files: {} } } }
await writeFile(legacy, JSON.stringify(mutated))
expect(await loadCache()).toEqual(validCache())
})
it('ignores a different-version legacy file and never touches it', async () => {
await mkdir(TMP_DIR, { recursive: true })
const legacy = join(TMP_DIR, 'session-cache.json')
const stale = { version: 999, providers: { claude: { envFingerprint: 'x', files: {} } } }
await writeFile(legacy, JSON.stringify(stale))
expect((await loadCache()).providers).toEqual({})
// No versioned file adopted; legacy left byte-intact.
expect(existsSync(sessionCachePath())).toBe(false)
expect(JSON.parse(await readFile(legacy, 'utf-8'))).toEqual(stale)
})
it('saveCache never creates or overwrites a pre-existing legacy file', async () => {
await mkdir(TMP_DIR, { recursive: true })
const legacy = join(TMP_DIR, 'session-cache.json')
const legacyContent = JSON.stringify({ version: CACHE_VERSION, providers: {} })
await writeFile(legacy, legacyContent)
await saveCache(validCache())
// The versioned file holds the new data; the legacy file is byte-untouched.
expect(await readFile(legacy, 'utf-8')).toBe(legacyContent)
expect(JSON.parse(await readFile(sessionCachePath(), 'utf-8'))).toEqual(validCache())
})
})
// ── computeEnvFingerprint ──────────────────────────────────────────────
describe('computeEnvFingerprint', () => {
it('returns stable hash for same env', () => {
const a = computeEnvFingerprint('claude')
const b = computeEnvFingerprint('claude')
expect(a).toBe(b)
expect(a).toHaveLength(16)
})
it('changes when env var changes', () => {
const before = computeEnvFingerprint('claude')
process.env['CLAUDE_CONFIG_DIR'] = '/tmp/different'
const after = computeEnvFingerprint('claude')
expect(before).not.toBe(after)
})
it('returns stable hash for unknown provider (no env vars)', () => {
const a = computeEnvFingerprint('unknown-provider')
const b = computeEnvFingerprint('unknown-provider')
expect(a).toBe(b)
})
it('includes parser versions in provider fingerprints', () => {
expect(computeEnvFingerprint('claude')).not.toBe(computeEnvFingerprint('unknown-provider'))
expect(computeEnvFingerprint('copilot')).not.toBe(computeEnvFingerprint('unknown-provider'))
expect(computeEnvFingerprint('kiro')).not.toBe(computeEnvFingerprint('unknown-provider'))
expect(computeEnvFingerprint('warp')).not.toBe(computeEnvFingerprint('unknown-provider'))
})
})
// ── provider env overrides invalidate the fingerprint (#920) ─────────────
describe('provider env overrides invalidate the fingerprint (#920)', () => {
// Nine providers honored an env var that relocates where discovery looks
// without the var being declared in PROVIDER_ENV_VARS, so
// computeEnvFingerprint did not hash it and the cache section survived the
// change: sessions parsed from the old root kept being reported and the new
// root was never read. Each pair below must change the fingerprint when the
// var is set. codex/CODEX_HOME is the control — it already worked and must
// keep working.
const CASES: Array<[provider: string, varName: string]> = [
['kiro', 'KIRO_HOME'],
['grok', 'GROK_HOME'],
['kimi', 'KIMI_SHARE_DIR'],
['mux', 'MUX_ROOT'],
['mistral-vibe', 'VIBE_HOME'],
['zerostack', 'ZS_DATA_DIR'],
['codebuff', 'CODEBUFF_DATA_DIR'],
['goose', 'GOOSE_PATH_ROOT'],
['crush', 'CRUSH_GLOBAL_DATA'],
['codex', 'CODEX_HOME'],
]
const VARS = CASES.map(([, varName]) => varName)
// Save and restore every var we touch (beforeEach/afterEach), so a leaked
// env var never breaks unrelated tests in the same worker — and an ambient
// value never makes the "unset" case a lie.
const saved = new Map<string, string | undefined>()
beforeEach(() => {
for (const varName of VARS) {
saved.set(varName, process.env[varName])
delete process.env[varName]
}
})
afterEach(() => {
for (const varName of VARS) {
const original = saved.get(varName)
if (original === undefined) delete process.env[varName]
else process.env[varName] = original
}
})
for (const [provider, varName] of CASES) {
it(`changes the ${provider} fingerprint when ${varName} is set`, () => {
const unset = computeEnvFingerprint(provider)
process.env[varName] = '/tmp/codeburn-920-override'
const set = computeEnvFingerprint(provider)
expect(set).not.toBe(unset)
// Round trip: restoring the variable to its original state restores the
// original fingerprint, so the hash is a pure function of the
// environment.
delete process.env[varName]
expect(computeEnvFingerprint(provider)).toBe(unset)
})
}
it('changes the vercel-gateway fingerprint when AI_GATEWAY_API_KEY is set', () => {
const prev = process.env['AI_GATEWAY_API_KEY']
try {
const unset = computeEnvFingerprint('vercel-gateway')
process.env['AI_GATEWAY_API_KEY'] = 'sk-live-secret-abc'
const set = computeEnvFingerprint('vercel-gateway')
expect(set).not.toBe(unset)
delete process.env['AI_GATEWAY_API_KEY']
expect(computeEnvFingerprint('vercel-gateway')).toBe(unset)
} finally {
if (prev === undefined) delete process.env['AI_GATEWAY_API_KEY']
else process.env['AI_GATEWAY_API_KEY'] = prev
}
})
// Copilot is deliberately NOT declared in PROVIDER_ENV_VARS (Ruling 1 of
// lane 04): its OTel discovery returns one source per DB file
// ({ path: dbPath }, src/providers/copilot.ts:1935), and the durable
// carry-forward in getOrCreateProviderSection (src/parser.ts:2650) drops
// every cached entry whose source still exists on a fingerprint change — so
// declaring any CODEBURN_COPILOT_* var would force a re-parse that destroys
// conversations Copilot has since pruned from the DB, which only the cache
// still holds. The fingerprint must therefore NOT move when one is set.
// This reads as intent, not as an oversight — and the assertions below pin
// the WHOLE invariant (no entry at all, plus every one of the nine deferred
// reads), so a future "completing" edit fails a test instead of silently
// re-opening the durable history-loss path.
describe('copilot is deliberately undeclared in PROVIDER_ENV_VARS', () => {
it('has no PROVIDER_ENV_VARS entry at all', () => {
expect(PROVIDER_ENV_VARS['copilot']).toBeUndefined()
})
// The nine reads copilot.ts performs whose declaration is deferred (each
// is allowlisted in tests/provider-env-declarations.test.ts): setting any
// of them must leave the copilot fingerprint untouched.
const DEFERRED_COPILOT_VARS = [
'CODEBURN_COPILOT_SESSION_STATE_DIR',
'CODEBURN_COPILOT_OTEL_DB',
'CODEBURN_COPILOT_JETBRAINS_DIR',
'CODEBURN_COPILOT_WS_STORAGE_DIR',
'CODEBURN_COPILOT_GLOBAL_STORAGE_DIR',
'CODEBURN_COPILOT_DISABLE_OTEL',
'APPDATA',
'LOCALAPPDATA',
'XDG_CONFIG_HOME',
]
for (const varName of DEFERRED_COPILOT_VARS) {
it(`does not move the copilot fingerprint when ${varName} is set (deliberately undeclared)`, () => {
const prev = process.env[varName]
try {
const before = computeEnvFingerprint('copilot')
process.env[varName] = `/tmp/codeburn-copilot-920/${varName}`
expect(computeEnvFingerprint('copilot')).toBe(before)
delete process.env[varName]
expect(computeEnvFingerprint('copilot')).toBe(before)
} finally {
if (prev === undefined) delete process.env[varName]
else process.env[varName] = prev
}
})
}
})
})
// ── fingerprintFile ────────────────────────────────────────────────────
describe('fingerprintFile', () => {
it('returns fingerprint for existing file', async () => {
await mkdir(TMP_DIR, { recursive: true })
const filePath = join(TMP_DIR, 'test.jsonl')
await writeFile(filePath, 'line1\nline2\n')
const fp = await fingerprintFile(filePath)
expect(fp).not.toBeNull()
expect(fp!.sizeBytes).toBe(12)
expect(fp!.dev).toBeGreaterThan(0)
expect(fp!.ino).toBeGreaterThan(0)
expect(fp!.mtimeMs).toBeGreaterThan(0)
})
it('returns null for non-existent file', async () => {
const fp = await fingerprintFile('/no/such/file')
expect(fp).toBeNull()
})
it('resolves compound path with # separator (Cursor workspace)', async () => {
await mkdir(TMP_DIR, { recursive: true })
const filePath = join(TMP_DIR, 'state.vscdb')
await writeFile(filePath, 'cursor-data')
const fp = await fingerprintFile(`${filePath}#cursor-ws=__orphan__`)
expect(fp).not.toBeNull()
expect(fp!.sizeBytes).toBe(11)
})
it('resolves compound path with : separator (OpenCode session)', async () => {
await mkdir(TMP_DIR, { recursive: true })
const filePath = join(TMP_DIR, 'opencode.db')
await writeFile(filePath, 'opencode-data')
const fp = await fingerprintFile(`${filePath}:ses_abc123`)
expect(fp).not.toBeNull()
expect(fp!.sizeBytes).toBe(13)
})
it('returns null when base file does not exist for compound path', async () => {
const fp = await fingerprintFile('/no/such/file.db#cursor-ws=workspace')
expect(fp).toBeNull()
})
it('prefers # separator over : when both present', async () => {
await mkdir(TMP_DIR, { recursive: true })
const filePath = join(TMP_DIR, 'state.vscdb')
await writeFile(filePath, 'both-seps')
// Path has both # and : — should strip at # first and find the base file
const fp = await fingerprintFile(`${filePath}#cursor-ws=ws:extra-colon`)
expect(fp).not.toBeNull()
expect(fp!.sizeBytes).toBe(9)
})
// SQLite WAL mode parks committed writes in `<db>-wal`; the main file's
// stat only moves on checkpoint, which a long-lived writer defers for
// hours. A fingerprint from the main file alone reports data older than
// what is really committed (issue #913). The WAL sibling must be folded in.
it('folds -wal sibling into a # compound fingerprint (Hermes session)', async () => {
await mkdir(TMP_DIR, { recursive: true })
const dbPath = join(TMP_DIR, 'state.db')
await writeFile(dbPath, 'main-db')
const past = new Date(Date.now() - 48 * 3600 * 1000)
await utimes(dbPath, past, past)
await writeFile(`${dbPath}-wal`, 'wal-frames')
const fp = await fingerprintFile(`${dbPath}#hermes-session=abc`)
expect(fp).not.toBeNull()
// mtime: the fresh WAL wins over the checkpoint-stale main file.
expect(fp!.mtimeMs).toBeGreaterThan(past.getTime() + 3600 * 1000)
// size: main + wal, so WAL growth alone changes the fingerprint.
expect(fp!.sizeBytes).toBe('main-db'.length + 'wal-frames'.length)
})
it('folds -wal sibling into a : compound fingerprint (OpenCode session)', async () => {
await mkdir(TMP_DIR, { recursive: true })
const dbPath = join(TMP_DIR, 'opencode.db')
await writeFile(dbPath, 'oc-db')
const past = new Date(Date.now() - 48 * 3600 * 1000)
await utimes(dbPath, past, past)
await writeFile(`${dbPath}-wal`, 'oc-wal')
const fp = await fingerprintFile(`${dbPath}:ses_abc123`)
expect(fp).not.toBeNull()
expect(fp!.mtimeMs).toBeGreaterThan(past.getTime() + 3600 * 1000)
expect(fp!.sizeBytes).toBe('oc-db'.length + 'oc-wal'.length)
})
it('folds -wal sibling into a bare SQLite path (copilot agent-traces.db)', async () => {
await mkdir(TMP_DIR, { recursive: true })
const dbPath = join(TMP_DIR, 'agent-traces.db')
await writeFile(dbPath, 'traces')
const past = new Date(Date.now() - 48 * 3600 * 1000)
await utimes(dbPath, past, past)
await writeFile(`${dbPath}-wal`, 'traces-wal')
const fp = await fingerprintFile(dbPath)
expect(fp).not.toBeNull()
expect(fp!.mtimeMs).toBeGreaterThan(past.getTime() + 3600 * 1000)
expect(fp!.sizeBytes).toBe('traces'.length + 'traces-wal'.length)
})
it('keeps compound fingerprints working when no -wal sibling exists', async () => {
await mkdir(TMP_DIR, { recursive: true })
const dbPath = join(TMP_DIR, 'state.db')
await writeFile(dbPath, 'main-only')
const fp = await fingerprintFile(`${dbPath}#hermes-session=abc`)
expect(fp).not.toBeNull()
expect(fp!.sizeBytes).toBe('main-only'.length)
})
it('does not fold sibling files into non-SQLite fingerprints', async () => {
await mkdir(TMP_DIR, { recursive: true })
const filePath = join(TMP_DIR, 'session.jsonl')
await writeFile(filePath, 'jsonl-data')
// A stray neighbor that happens to match the -wal naming must not leak
// into a transcript fingerprint (offset-based append detection relies on
// sizeBytes being the transcript's real byte length).
await writeFile(`${filePath}-wal`, 'stray')
const fp = await fingerprintFile(filePath)
expect(fp).not.toBeNull()
expect(fp!.sizeBytes).toBe('jsonl-data'.length)
})
})
// ── reconcileFile ──────────────────────────────────────────────────────
describe('reconcileFile', () => {
it('returns "new" when no cached entry', () => {
const fp: FileFingerprint = { dev: 1, ino: 100, mtimeMs: 1000, sizeBytes: 5000 }
expect(reconcileFile(fp, undefined)).toEqual({ action: 'new' })
})
it('returns "unchanged" when all fields match', () => {
const fp: FileFingerprint = { dev: 1, ino: 100, mtimeMs: 1000, sizeBytes: 5000 }
const cached = makeCachedFile({ fingerprint: { ...fp } })
expect(reconcileFile(fp, cached)).toEqual({ action: 'unchanged' })
})
it('returns "appended" when ino same, size grew, and has lastCompleteLineOffset', () => {
const cached = makeCachedFile({
fingerprint: { dev: 1, ino: 100, mtimeMs: 1000, sizeBytes: 5000 },
lastCompleteLineOffset: 4500,
})
const current: FileFingerprint = { dev: 1, ino: 100, mtimeMs: 2000, sizeBytes: 8000 }
const result = reconcileFile(current, cached)
expect(result).toEqual({ action: 'appended', readFromOffset: 4500 })
})
it('returns "modified" when ino changed', () => {
const cached = makeCachedFile({
fingerprint: { dev: 1, ino: 100, mtimeMs: 1000, sizeBytes: 5000 },
})
const current: FileFingerprint = { dev: 1, ino: 200, mtimeMs: 2000, sizeBytes: 5000 }
expect(reconcileFile(current, cached)).toEqual({ action: 'modified' })
})
it('a failed marker at the same fingerprint stays "unchanged" (not re-parsed)', () => {
const fp: FileFingerprint = { dev: 1, ino: 100, mtimeMs: 1000, sizeBytes: 5000 }
const marker = makeCachedFile({ fingerprint: { ...fp }, turns: [], failed: true })
expect(reconcileFile(fp, marker)).toEqual({ action: 'unchanged' })
})
it('a failed marker is re-parsed once the file changes', () => {
const marker = makeCachedFile({
fingerprint: { dev: 1, ino: 100, mtimeMs: 1000, sizeBytes: 5000 },
turns: [],
failed: true,
})
const changed: FileFingerprint = { dev: 1, ino: 100, mtimeMs: 2000, sizeBytes: 6000 }
expect(reconcileFile(changed, marker)).toEqual({ action: 'modified' })
})
it('returns "modified" when the cached offset is stranded beyond the current EOF', () => {
// A truncate-then-regrow can leave the resume offset past live bytes; resuming
// there would drop the appended tail, so it must fall back to a full re-parse.
const cached = makeCachedFile({
fingerprint: { dev: 1, ino: 100, mtimeMs: 1000, sizeBytes: 5000 },
lastCompleteLineOffset: 9_000_000,
})
const current: FileFingerprint = { dev: 1, ino: 100, mtimeMs: 2000, sizeBytes: 8000 }
expect(reconcileFile(current, cached)).toEqual({ action: 'modified' })
})
it('returns "modified" when size shrank', () => {
const cached = makeCachedFile({
fingerprint: { dev: 1, ino: 100, mtimeMs: 1000, sizeBytes: 5000 },
lastCompleteLineOffset: 4500,
})
const current: FileFingerprint = { dev: 1, ino: 100, mtimeMs: 2000, sizeBytes: 3000 }
expect(reconcileFile(current, cached)).toEqual({ action: 'modified' })
})
it('returns "modified" when same size but different mtime', () => {
const cached = makeCachedFile({
fingerprint: { dev: 1, ino: 100, mtimeMs: 1000, sizeBytes: 5000 },
})
const current: FileFingerprint = { dev: 1, ino: 100, mtimeMs: 2000, sizeBytes: 5000 }
expect(reconcileFile(current, cached)).toEqual({ action: 'modified' })
})
it('returns "modified" for DB provider (no lastCompleteLineOffset) on any fingerprint change', () => {
const cached = makeCachedFile({
fingerprint: { dev: 1, ino: 100, mtimeMs: 1000, sizeBytes: 5000 },
})
const current: FileFingerprint = { dev: 1, ino: 100, mtimeMs: 2000, sizeBytes: 8000 }
expect(reconcileFile(current, cached)).toEqual({ action: 'modified' })
})
it('returns "modified" when dev changed even if ino same and size grew', () => {
const cached = makeCachedFile({
fingerprint: { dev: 1, ino: 100, mtimeMs: 1000, sizeBytes: 5000 },
lastCompleteLineOffset: 4500,
})
const current: FileFingerprint = { dev: 2, ino: 100, mtimeMs: 2000, sizeBytes: 8000 }
expect(reconcileFile(current, cached)).toEqual({ action: 'modified' })
})
})
// ── mergeCallByDedupKey ────────────────────────────────────────────────
describe('mergeCallByDedupKey', () => {
it('keeps earlier timestamp', () => {
const existing = makeCall({ timestamp: '2026-05-15T10:00:00Z' })
const incoming = makeCall({ timestamp: '2026-05-15T10:01:00Z' })
const merged = mergeCallByDedupKey(existing, incoming)
expect(merged.timestamp).toBe('2026-05-15T10:00:00Z')
})
it('takes incoming usage (latest wins)', () => {
const existing = makeCall({ usage: { ...makeCall().usage, outputTokens: 100 } })
const incoming = makeCall({ usage: { ...makeCall().usage, outputTokens: 999 } })
const merged = mergeCallByDedupKey(existing, incoming)
expect(merged.usage.outputTokens).toBe(999)
})
it('takes incoming tools (latest wins)', () => {
const existing = makeCall({ tools: ['Read'] })
const incoming = makeCall({ tools: ['Read', 'Edit', 'Bash'] })
const merged = mergeCallByDedupKey(existing, incoming)
expect(merged.tools).toEqual(['Read', 'Edit', 'Bash'])
})
})
// ── deep validation (loadCache) ────────────────────────────────────────
describe('loadCache validation', () => {
async function writeRawCache(data: unknown): Promise<void> {
await mkdir(TMP_DIR, { recursive: true })
await writeFile(join(TMP_DIR, 'session-cache.json'), JSON.stringify(data))
}
it('rejects providers as array', async () => {
await writeRawCache({ version: CACHE_VERSION, providers: [] })
expect((await loadCache()).providers).toEqual({})
})
it('rejects provider section missing envFingerprint', async () => {
await writeRawCache({ version: CACHE_VERSION, providers: { claude: { files: {} } } })
expect((await loadCache()).providers).toEqual({})
})
it('rejects provider section with files as array', async () => {
await writeRawCache({ version: CACHE_VERSION, providers: { claude: { envFingerprint: 'x', files: [] } } })
expect((await loadCache()).providers).toEqual({})
})
it('rejects file with invalid fingerprint (missing ino)', async () => {
await writeRawCache({
version: CACHE_VERSION,
providers: { claude: { envFingerprint: 'x', files: {
'/f': { fingerprint: { dev: 1, mtimeMs: 1, sizeBytes: 1 }, mcpInventory: [], turns: [] },
} } },
})
expect((await loadCache()).providers).toEqual({})
})
it('rejects file with non-numeric fingerprint field', async () => {
await writeRawCache({
version: CACHE_VERSION,
providers: { claude: { envFingerprint: 'x', files: {
'/f': { fingerprint: { dev: 1, ino: 'bad', mtimeMs: 1, sizeBytes: 1 }, mcpInventory: [], turns: [] },
} } },
})
expect((await loadCache()).providers).toEqual({})
})
it('rejects turn with missing sessionId', async () => {
const badTurn = { timestamp: 'x', userMessage: 'y', calls: [] }
await writeRawCache({
version: CACHE_VERSION,
providers: { claude: { envFingerprint: 'x', files: {
'/f': { fingerprint: { dev: 1, ino: 2, mtimeMs: 3, sizeBytes: 4 }, mcpInventory: [], turns: [badTurn] },
} } },
})
expect((await loadCache()).providers).toEqual({})
})
it('rejects call with missing usage object', async () => {
const badCall = { provider: 'claude', model: 'm', deduplicationKey: 'k', timestamp: 't', tools: [], bashCommands: [], skills: [] }
const turn = { timestamp: 'x', sessionId: 's', userMessage: 'y', calls: [badCall] }
await writeRawCache({
version: CACHE_VERSION,
providers: { claude: { envFingerprint: 'x', files: {
'/f': { fingerprint: { dev: 1, ino: 2, mtimeMs: 3, sizeBytes: 4 }, mcpInventory: [], turns: [turn] },
} } },
})
expect((await loadCache()).providers).toEqual({})
})
it('rejects call with NaN in usage', async () => {
const badUsage = { inputTokens: NaN, outputTokens: 0, cacheCreationInputTokens: 0, cacheReadInputTokens: 0, cachedInputTokens: 0, reasoningTokens: 0, webSearchRequests: 0, cacheCreationOneHourTokens: 0 }
const call = { provider: 'claude', model: 'm', usage: badUsage, deduplicationKey: 'k', timestamp: 't', tools: [], bashCommands: [], skills: [], speed: 'standard' }
const turn = { timestamp: 'x', sessionId: 's', userMessage: 'y', calls: [call] }
await writeRawCache({
version: CACHE_VERSION,
providers: { claude: { envFingerprint: 'x', files: {
'/f': { fingerprint: { dev: 1, ino: 2, mtimeMs: 3, sizeBytes: 4 }, mcpInventory: [], turns: [turn] },
} } },
})
expect((await loadCache()).providers).toEqual({})
})
function validCallJson() {
return {
provider: 'claude', model: 'm', deduplicationKey: 'k', timestamp: 't', speed: 'standard',
tools: ['Read'], bashCommands: ['ls'], skills: [],
usage: { inputTokens: 1, outputTokens: 1, cacheCreationInputTokens: 0, cacheReadInputTokens: 0, cachedInputTokens: 0, reasoningTokens: 0, webSearchRequests: 0, cacheCreationOneHourTokens: 0 },
}
}
function wrapCall(callOverride: Record<string, unknown>) {
return {
version: CACHE_VERSION,
providers: { claude: { envFingerprint: 'x', files: {
'/f': { fingerprint: { dev: 1, ino: 2, mtimeMs: 3, sizeBytes: 4 }, mcpInventory: [], turns: [
{ timestamp: 'x', sessionId: 's', userMessage: 'y', calls: [{ ...validCallJson(), ...callOverride }] },
] },
} } },
}
}
function wrapFile(fileOverride: Record<string, unknown>) {
return {
version: CACHE_VERSION,
providers: { claude: { envFingerprint: 'x', files: {
'/f': { fingerprint: { dev: 1, ino: 2, mtimeMs: 3, sizeBytes: 4 }, mcpInventory: [], turns: [], ...fileOverride },
} } },
}
}
it('rejects tools containing non-string element', async () => {
await writeRawCache(wrapCall({ tools: ['Read', 42] }))
expect((await loadCache()).providers).toEqual({})
})
it('rejects bashCommands containing object element', async () => {
await writeRawCache(wrapCall({ bashCommands: [{}] }))
expect((await loadCache()).providers).toEqual({})
})
it('rejects skills containing null element', async () => {
await writeRawCache(wrapCall({ skills: [null] }))
expect((await loadCache()).providers).toEqual({})
})
it('rejects invalid speed value', async () => {
await writeRawCache(wrapCall({ speed: 'turbo' }))
expect((await loadCache()).providers).toEqual({})
})
it('rejects non-string project', async () => {
await writeRawCache(wrapCall({ project: 123 }))
expect((await loadCache()).providers).toEqual({})
})
it('rejects non-string projectPath', async () => {
await writeRawCache(wrapCall({ projectPath: true }))
expect((await loadCache()).providers).toEqual({})
})
it('rejects mcpInventory containing non-string element', async () => {
await writeRawCache(wrapFile({ mcpInventory: ['valid', 99] }))
expect((await loadCache()).providers).toEqual({})
})
it('rejects non-numeric lastCompleteLineOffset', async () => {
await writeRawCache(wrapFile({ lastCompleteLineOffset: 'bad' }))
expect((await loadCache()).providers).toEqual({})
})
it('rejects NaN lastCompleteLineOffset', async () => {
await writeRawCache(wrapFile({ lastCompleteLineOffset: null }))
expect((await loadCache()).providers).toEqual({})
})
it('rejects non-string canonicalCwd', async () => {
await writeRawCache(wrapFile({ canonicalCwd: 42 }))
expect((await loadCache()).providers).toEqual({})
})
it('accepts optional fields when absent', async () => {
const cache: SessionCache = {
version: CACHE_VERSION,
providers: { claude: { envFingerprint: 'x', files: {
'/f': { fingerprint: { dev: 1, ino: 2, mtimeMs: 3, sizeBytes: 4 }, mcpInventory: [], turns: [] },
} } },
}
await writeRawCache(cache)
expect((await loadCache())).toEqual(cache)
})
it('accepts a fully valid cache with all fields populated', async () => {
const cache: SessionCache = {
version: CACHE_VERSION,
providers: {
claude: {
envFingerprint: 'abc',
files: { '/f': makeCachedFile() },
},
},
}
await writeRawCache(cache)
const loaded = await loadCache()
expect(loaded).toEqual(cache)
})
})
// ── cleanupOrphanedTempFiles ───────────────────────────────────────────
describe('cleanupOrphanedTempFiles', () => {
it('removes .tmp files older than 5 minutes', async () => {
await mkdir(TMP_DIR, { recursive: true })
const oldTmp = join(TMP_DIR, `${CACHE_FILE()}.abc123.tmp`)
await writeFile(oldTmp, 'stale')
const { utimes } = await import('fs/promises')
const oldTime = new Date(Date.now() - 10 * 60 * 1000)
await utimes(oldTmp, oldTime, oldTime)
await cleanupOrphanedTempFiles()
expect(existsSync(oldTmp)).toBe(false)
})
it('preserves recent .tmp files', async () => {
await mkdir(TMP_DIR, { recursive: true })
const recentTmp = join(TMP_DIR, `${CACHE_FILE()}.def456.tmp`)
await writeFile(recentTmp, 'recent')
await cleanupOrphanedTempFiles()
expect(existsSync(recentTmp)).toBe(true)
})
it('ignores .tmp files from other caches', async () => {
await mkdir(TMP_DIR, { recursive: true })
const otherTmp = join(TMP_DIR, 'codex-results.json.abc123.tmp')
await writeFile(otherTmp, 'other cache temp')
const { utimes } = await import('fs/promises')
const oldTime = new Date(Date.now() - 10 * 60 * 1000)
await utimes(otherTmp, oldTime, oldTime)
await cleanupOrphanedTempFiles()
expect(existsSync(otherTmp)).toBe(true)
})
it('does not fail when cache dir does not exist', async () => {
process.env['CODEBURN_CACHE_DIR'] = '/no/such/dir'
await cleanupOrphanedTempFiles()
})
})