mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-08-21 06:24:32 +00:00
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.
104 lines
3.8 KiB
TypeScript
104 lines
3.8 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import { mkdir, mkdtemp, readFile, rm, writeFile } from 'fs/promises'
|
|
import { tmpdir } from 'os'
|
|
import { join } from 'path'
|
|
|
|
vi.mock('../src/cache-refresh-lock.js', () => ({
|
|
acquireCacheRefreshLock: async () => ({ outcome: 'timed-out' as const }),
|
|
}))
|
|
|
|
import { clearSessionCache, isSessionHydrationComplete, parseAllSessions } from '../src/parser.js'
|
|
import { cacheDirSnapshot } from './fixtures/session-cache-io.js'
|
|
|
|
let root: string
|
|
let sessionPath: string
|
|
|
|
function output(projects: Awaited<ReturnType<typeof parseAllSessions>>): number {
|
|
return projects.flatMap(p => p.sessions).flatMap(s => s.turns)
|
|
.flatMap(t => t.assistantCalls).reduce((sum, call) => sum + call.usage.outputTokens, 0)
|
|
}
|
|
|
|
async function writeSession(value: number): Promise<void> {
|
|
await writeFile(sessionPath, JSON.stringify({
|
|
type: 'assistant',
|
|
sessionId: 'sess',
|
|
timestamp: '2026-05-15T10:00:00Z',
|
|
cwd: '/tmp/proj',
|
|
message: {
|
|
id: `msg-${value}`, type: 'message', role: 'assistant', model: 'claude-sonnet-4-5',
|
|
content: [], usage: { input_tokens: 100, output_tokens: value },
|
|
},
|
|
}) + '\n')
|
|
}
|
|
|
|
beforeEach(async () => {
|
|
clearSessionCache()
|
|
root = await mkdtemp(join(tmpdir(), 'cb-refresh-timeout-'))
|
|
const home = join(root, 'home')
|
|
const project = join(home, 'projects', 'proj')
|
|
await mkdir(project, { recursive: true })
|
|
sessionPath = join(project, 'sess.jsonl')
|
|
process.env['CLAUDE_CONFIG_DIR'] = home
|
|
process.env['CODEBURN_CACHE_DIR'] = join(root, 'cache')
|
|
process.env['CODEBURN_DESKTOP_SESSIONS_DIR'] = join(home, 'desktop-sessions')
|
|
})
|
|
|
|
afterEach(async () => {
|
|
clearSessionCache()
|
|
await rm(root, { recursive: true, force: true })
|
|
})
|
|
|
|
describe('parseAllSessions warm refresh timeout', () => {
|
|
it('serves the prior complete snapshot and leaves the holder cache untouched', async () => {
|
|
await writeSession(50)
|
|
expect(output(await parseAllSessions(undefined, 'claude'))).toBe(50)
|
|
const before = await cacheDirSnapshot()
|
|
|
|
await writeSession(5000)
|
|
clearSessionCache()
|
|
expect(output(await parseAllSessions(undefined, 'claude'))).toBe(50)
|
|
expect(await cacheDirSnapshot()).toBe(before)
|
|
})
|
|
|
|
// The snapshot a timed-out refresh serves is only as good as what has changed
|
|
// under it. Anything the daily backfill finalizes off a snapshot that skipped
|
|
// real files freezes those days out of history for good, so the completeness
|
|
// signal has to distinguish the two cases.
|
|
it('does not report a complete hydration when the served snapshot is stale', async () => {
|
|
await writeSession(50)
|
|
await parseAllSessions(undefined, 'claude')
|
|
expect(isSessionHydrationComplete()).toBe(true)
|
|
|
|
await writeSession(5000)
|
|
clearSessionCache()
|
|
await parseAllSessions(undefined, 'claude')
|
|
expect(isSessionHydrationComplete()).toBe(false)
|
|
})
|
|
|
|
it('does not report a complete hydration when a session file is missing from the snapshot', async () => {
|
|
await writeSession(50)
|
|
await parseAllSessions(undefined, 'claude')
|
|
|
|
await writeFile(join(sessionPath, '..', 'other.jsonl'), JSON.stringify({
|
|
type: 'assistant',
|
|
sessionId: 'sess-2',
|
|
timestamp: '2026-05-16T10:00:00Z',
|
|
cwd: '/tmp/proj',
|
|
message: {
|
|
id: 'msg-other', type: 'message', role: 'assistant', model: 'claude-sonnet-4-5',
|
|
content: [], usage: { input_tokens: 100, output_tokens: 7 },
|
|
},
|
|
}) + '\n')
|
|
clearSessionCache()
|
|
await parseAllSessions(undefined, 'claude')
|
|
expect(isSessionHydrationComplete()).toBe(false)
|
|
})
|
|
|
|
it('still reports a complete hydration when nothing changed under the snapshot', async () => {
|
|
await writeSession(50)
|
|
await parseAllSessions(undefined, 'claude')
|
|
clearSessionCache()
|
|
await parseAllSessions(undefined, 'claude')
|
|
expect(isSessionHydrationComplete()).toBe(true)
|
|
})
|
|
})
|