codeburn/tests/session-cache-rich-capture.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

125 lines
4.4 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it } from 'vitest'
import { rm, writeFile, mkdir } from 'fs/promises'
import { existsSync } from 'fs'
import { tmpdir } from 'os'
import { join } from 'path'
import {
CACHE_VERSION,
type CachedCall,
type CachedFile,
type SessionCache,
emptyCache,
loadCache,
saveCache,
} from '../src/session-cache.js'
import { writeCacheOnDisk } from './fixtures/session-cache-io.js'
const TMP_DIR = join(tmpdir(), `codeburn-rich-cache-${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 richCall(): CachedCall {
return {
provider: 'claude',
model: 'claude-sonnet-4-20250514',
usage: {
inputTokens: 100, outputTokens: 50, cacheCreationInputTokens: 0, cacheReadInputTokens: 0,
cachedInputTokens: 0, reasoningTokens: 0, webSearchRequests: 0, cacheCreationOneHourTokens: 0,
},
speed: 'standard',
timestamp: '2026-07-01T10:00:00Z',
tools: ['Edit'],
bashCommands: [],
skills: [],
subagentTypes: [],
deduplicationKey: 'm1',
locAdded: 12,
locRemoved: 4,
interrupted: true,
userModified: true,
toolErrors: 2,
editFailed: 1,
}
}
function richFile(): CachedFile {
return {
fingerprint: { dev: 1, ino: 2, mtimeMs: 3, sizeBytes: 4 },
mcpInventory: [],
title: 'A rich session',
prLinks: ['https://github.com/o/r/pull/1', 'https://github.com/o/r/pull/2'],
isSidechain: true,
parentSessionId: 'parent-uuid',
agentSpawnLinks: { a17e80ec626c9de38: 'toolu_spawn1' },
ambiguousSpawnAgentIds: ['a999abcdef0123456'],
turns: [
{ timestamp: '2026-07-01T10:00:00Z', sessionId: 's1', userMessage: 'hi', gitBranch: 'feature/x', prRefs: ['https://github.com/o/r/pull/1'], spawnToolUseIds: ['toolu_spawn1'], calls: [richCall()] },
],
}
}
describe('session cache round-trip for rich-capture fields', () => {
it('preserves per-call, per-turn, and per-session fields through save+load', async () => {
const cache: SessionCache = {
...emptyCache(),
providers: { claude: { envFingerprint: 'fp', files: { '/x/s1.jsonl': richFile() } } },
}
await saveCache(cache)
const loaded = await loadCache()
const file = loaded.providers['claude']!.files['/x/s1.jsonl']!
expect(file.title).toBe('A rich session')
expect(file.prLinks).toEqual(['https://github.com/o/r/pull/1', 'https://github.com/o/r/pull/2'])
expect(file.isSidechain).toBe(true)
expect(file.parentSessionId).toBe('parent-uuid')
expect(file.agentSpawnLinks).toEqual({ a17e80ec626c9de38: 'toolu_spawn1' })
expect(file.ambiguousSpawnAgentIds).toEqual(['a999abcdef0123456'])
const turn = file.turns[0]!
expect(turn.gitBranch).toBe('feature/x')
expect(turn.prRefs).toEqual(['https://github.com/o/r/pull/1'])
expect(turn.spawnToolUseIds).toEqual(['toolu_spawn1'])
const call = turn.calls[0]!
expect(call.locAdded).toBe(12)
expect(call.locRemoved).toBe(4)
expect(call.interrupted).toBe(true)
expect(call.userModified).toBe(true)
expect(call.toolErrors).toBe(2)
expect(call.editFailed).toBe(1)
})
it('still loads an old cache written without any rich-capture fields', async () => {
const oldCall = { ...richCall() }
for (const k of ['locAdded', 'locRemoved', 'interrupted', 'userModified', 'toolErrors', 'editFailed'] as const) {
delete (oldCall as Record<string, unknown>)[k]
}
const oldCache: SessionCache = {
version: CACHE_VERSION,
complete: true,
providers: {
claude: {
envFingerprint: 'fp',
files: {
'/x/old.jsonl': {
fingerprint: { dev: 1, ino: 2, mtimeMs: 3, sizeBytes: 4 },
mcpInventory: [],
turns: [{ timestamp: '2026-07-01T10:00:00Z', sessionId: 's1', userMessage: 'hi', calls: [oldCall] }],
},
},
},
},
}
if (!existsSync(TMP_DIR)) await mkdir(TMP_DIR, { recursive: true })
await writeCacheOnDisk(oldCache)
const loaded = await loadCache()
const call = loaded.providers['claude']!.files['/x/old.jsonl']!.turns[0]!.calls[0]!
expect(call.deduplicationKey).toBe('m1')
expect(call.locAdded).toBeUndefined()
expect(call.editFailed).toBeUndefined()
expect(loaded.providers['claude']!.files['/x/old.jsonl']!.title).toBeUndefined()
})
})