codeburn/tests/cache-persistence-coldstart.test.ts
iamtoruk 0bfdfa372a perf(cache): shard the session cache per provider
A warm launch rewrote the entire session cache whenever any provider
appended a few KB: on a 6 GB corpus that is a 155 MB stringify + fsync
every run. The on-disk cache is now a version-suffixed directory holding
one shard per provider plus a small envelope, and a save rewrites only
the providers marked dirty.

- Dirtiness is tracked per provider (markCacheDirty) instead of one
  global flag, so an appended Claude session no longer republishes
  Codex, Copilot and the rest.
- Shards carry a nonce in their filename and the envelope is renamed
  last, so a save is published at a single point: readers never see a
  half-updated set, and a writer that loses the refresh ownership fence
  leaves the canonical shards untouched.
- A shard that fails validation is treated as an absent provider rather
  than rejecting the whole cache, so one malformed turn costs one
  provider's re-parse instead of every provider's history.
- v7 migrates losslessly: the blob is re-laid-out into shards and
  removed only once that save publishes. Nothing re-parses.
- Cold-parse progress saves now trigger every N files parsed rather than
  every 5s, so a slow cold parse no longer rewrites the growing cache on
  a wall clock.
2026-08-16 19:03:12 -07:00

82 lines
3.2 KiB
TypeScript

import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
import { mkdtemp, mkdir, writeFile, readFile, rm } from 'fs/promises'
import { join } from 'path'
import { tmpdir } from 'os'
import { parseAllSessions, clearSessionCache } from '../src/parser.js'
import { CACHE_VERSION } from '../src/session-cache.js'
import { readCacheOnDisk } from './fixtures/session-cache-io.js'
let tmpDir: string
let cacheDir: string
beforeEach(async () => {
clearSessionCache()
tmpDir = await mkdtemp(join(tmpdir(), 'coldstart-'))
cacheDir = join(tmpDir, 'cache')
process.env['CLAUDE_CONFIG_DIR'] = tmpDir
process.env['CODEBURN_CACHE_DIR'] = cacheDir
process.env['CODEBURN_DESKTOP_SESSIONS_DIR'] = join(tmpDir, 'desktop-sessions')
})
afterEach(async () => {
clearSessionCache()
delete process.env['CODEBURN_PROGRESS']
await rm(tmpDir, { recursive: true, force: true })
})
async function writeSession(): Promise<void> {
const dir = join(tmpDir, 'projects', 'proj')
await mkdir(dir, { recursive: true })
await writeFile(join(dir, 'sess.jsonl'), JSON.stringify({
type: 'assistant',
sessionId: 'sess',
timestamp: '2026-05-15T10:00:00Z',
cwd: '/tmp/proj',
message: {
id: 'msg-1', type: 'message', role: 'assistant', model: 'claude-sonnet-4-5',
content: [], usage: { input_tokens: 100, output_tokens: 50 },
},
}) + '\n')
}
describe('cold-start cache persistence', () => {
// The desktop cold-start bug is "the cache never persists". This pins the
// invariant the fix depends on: a run that reaches the end of parseAllSessions
// writes the current-version session cache to disk, so the next launch is warm.
it('a completed parseAllSessions persists the current-version cache to disk', async () => {
await writeSession()
const projects = await parseAllSessions()
expect(projects.length).toBeGreaterThan(0)
const raw = await readCacheOnDisk()
expect(raw.version).toBe(CACHE_VERSION)
const claudeFiles = Object.keys(raw.providers?.claude?.files ?? {})
expect(claudeFiles.length).toBeGreaterThan(0)
// The persisted entry carries the parsed turn, so a warm reload serves it
// without re-reading the source file.
expect(raw.providers.claude.files[claudeFiles[0]!].turns.length).toBeGreaterThan(0)
})
it('streams per-provider scan progress only when CODEBURN_PROGRESS=1', async () => {
await writeSession()
const lines: string[] = []
const spy = vi.spyOn(process.stderr, 'write').mockImplementation(((chunk: unknown) => {
lines.push(String(chunk)); return true
}) as typeof process.stderr.write)
try {
// Off by default: plain CLI/terminal output is untouched.
await parseAllSessions()
expect(lines.filter(l => l.startsWith('CODEBURN_PROGRESS ')).length).toBe(0)
clearSessionCache()
process.env['CODEBURN_PROGRESS'] = '1'
await parseAllSessions()
const progress = lines.filter(l => l.startsWith('CODEBURN_PROGRESS '))
expect(progress.some(l => l.includes('"providers"'))).toBe(true)
expect(progress.some(l => l.includes('"provider":"claude"') && l.includes('"start"'))).toBe(true)
} finally {
spy.mockRestore()
}
})
})