codeburn/tests/cache-directory-switch.test.ts
iamtoruk 72ed163db0 perf(codex): resume an appended rollout from its last task boundary
Codex rollout files are append-only and the active ones run to hundreds
of MB, but any growth re-read the file from byte 0 because the cache
keyed only on mtime+size. The parser now records a restart point at every
task_started boundary — the byte offset plus the state the single-pass
decode carries across it — and a grown file with the same dev/ino picks
up from there.

The boundary sits at the task_started line itself, so the task it opens
is re-decoded from the tail; the entry stores how many calls were decoded
before that point so the resumed run starts from exactly those and cannot
double-count the open task. An unusable or absent snapshot falls back to
a full re-parse.

CODEX_CACHE_VERSION is deliberately not bumped: the new fields are
additive and absence-safe both ways, so a bump would discard a warm
cache for nothing.
2026-08-16 19:03:12 -07:00

277 lines
11 KiB
TypeScript

import { mkdir, mkdtemp, readFile, rm, stat, writeFile } from 'fs/promises'
import { existsSync } from 'fs'
import { tmpdir } from 'os'
import { join } from 'path'
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
import {
clearCodexMemCaches,
fingerprintFile,
flushCodexCache,
readCachedCodexResults,
writeCachedCodexResults,
} from '../src/codex-cache.js'
import {
clearAntigravityCacheStates,
createAntigravityProvider,
flushAntigravityCache,
} from '../src/providers/antigravity.js'
import type { ParsedProviderCall } from '../src/providers/types.js'
const originalCacheDir = process.env['CODEBURN_CACHE_DIR']
const originalHome = process.env['HOME']
const originalCodexHome = process.env['CODEX_HOME']
let root: string
function call(provider: string, marker: string): ParsedProviderCall {
return {
provider,
model: marker,
inputTokens: 1,
outputTokens: 1,
cacheCreationInputTokens: 0,
cacheReadInputTokens: 0,
cachedInputTokens: 0,
reasoningTokens: 0,
webSearchRequests: 0,
costUSD: 0,
tools: [],
bashCommands: [],
timestamp: '2026-08-12T00:00:00.000Z',
speed: 'standard',
deduplicationKey: `${provider}:${marker}`,
userMessage: '',
sessionId: marker,
}
}
async function seedAntigravityCache(
cacheDir: string,
sourcePath: string,
marker: string,
): Promise<void> {
const sourceStat = await stat(sourcePath)
await mkdir(cacheDir, { recursive: true })
await writeFile(join(cacheDir, 'antigravity-results.json'), JSON.stringify({
version: 5,
cascades: {
shared: {
mtimeMs: sourceStat.mtimeMs,
sizeBytes: sourceStat.size,
calls: [call('antigravity', marker)],
},
},
}))
}
async function readAntigravityModel(sourcePath: string): Promise<string | undefined> {
const parser = createAntigravityProvider().createSessionParser({
path: sourcePath,
project: 'fixture',
provider: 'antigravity',
}, new Set())
for await (const parsed of parser.parse()) return parsed.model
return undefined
}
beforeEach(async () => {
root = await mkdtemp(join(tmpdir(), 'codeburn-cache-switch-'))
})
afterEach(async () => {
if (originalCacheDir === undefined) delete process.env['CODEBURN_CACHE_DIR']
else process.env['CODEBURN_CACHE_DIR'] = originalCacheDir
if (originalHome === undefined) delete process.env['HOME']
else process.env['HOME'] = originalHome
if (originalCodexHome === undefined) delete process.env['CODEX_HOME']
else process.env['CODEX_HOME'] = originalCodexHome
await rm(root, { recursive: true, force: true })
})
describe('call-time CODEBURN_CACHE_DIR isolation', () => {
it('keeps Codex reads and writes keyed by the active cache directory', async () => {
const sourcePath = join(root, 'rollout.jsonl')
const cacheA = join(root, 'cache-a')
const cacheB = join(root, 'cache-b')
await writeFile(sourcePath, '{}\n')
const fingerprint = await fingerprintFile(sourcePath)
expect(fingerprint).not.toBeNull()
process.env['CODEBURN_CACHE_DIR'] = cacheA
await writeCachedCodexResults(sourcePath, 'project-a', [call('codex', 'from-a')], fingerprint!)
await flushCodexCache()
process.env['CODEBURN_CACHE_DIR'] = cacheB
expect(await readCachedCodexResults(sourcePath)).toBeNull()
await writeCachedCodexResults(sourcePath, 'project-b', [call('codex', 'from-b')], fingerprint!)
await flushCodexCache()
const diskB = JSON.parse(await readFile(join(cacheB, 'codex-results.json'), 'utf8'))
expect(diskB.files[sourcePath].calls.map((entry: ParsedProviderCall) => entry.model)).toEqual(['from-b'])
process.env['CODEBURN_CACHE_DIR'] = cacheA
expect((await readCachedCodexResults(sourcePath))?.calls.map(entry => entry.model)).toEqual(['from-a'])
})
it('does not flush dirty Codex state from A into B', async () => {
const sourceA = join(root, 'a.jsonl')
const sourceB = join(root, 'b.jsonl')
const cacheA = join(root, 'cache-a-dirty')
const cacheB = join(root, 'cache-b-dirty')
await writeFile(sourceA, 'a\n')
await writeFile(sourceB, 'b\n')
process.env['CODEBURN_CACHE_DIR'] = cacheA
await writeCachedCodexResults(sourceA, 'project-a', [call('codex', 'dirty-a')], (await fingerprintFile(sourceA))!)
process.env['CODEBURN_CACHE_DIR'] = cacheB
await writeCachedCodexResults(sourceB, 'project-b', [call('codex', 'dirty-b')], (await fingerprintFile(sourceB))!)
await flushCodexCache()
const diskB = JSON.parse(await readFile(join(cacheB, 'codex-results.json'), 'utf8'))
expect(Object.keys(diskB.files)).toEqual([sourceB])
process.env['CODEBURN_CACHE_DIR'] = cacheA
await flushCodexCache()
const diskA = JSON.parse(await readFile(join(cacheA, 'codex-results.json'), 'utf8'))
expect(Object.keys(diskA.files)).toEqual([sourceA])
})
it('pins Codex reads, dirty writes, and flushes to the parse call-time directory', async () => {
const home = join(root, 'parse-home')
const codexHome = join(root, 'parse-codex-home')
const sessionDir = join(codexHome, 'sessions', '2026', '08', '12')
const cacheA = join(root, 'parse-cache-a')
const cacheB = join(root, 'parse-cache-b')
await mkdir(sessionDir, { recursive: true })
await mkdir(home, { recursive: true })
const sourcePath = join(sessionDir, 'rollout-cache-dir-switch.jsonl')
await writeFile(sourcePath, [
JSON.stringify({
type: 'session_meta',
timestamp: '2026-08-12T10:00:00.000Z',
payload: {
cwd: '/Users/test/cache-dir-transaction',
originator: 'codex-cli',
session_id: 'cache-dir-transaction',
model: 'gpt-5.3-codex',
},
}),
JSON.stringify({
type: 'event_msg',
timestamp: '2026-08-12T10:01:00.000Z',
payload: {
type: 'token_count',
info: {
model: 'gpt-5.3-codex',
last_token_usage: {
input_tokens: 10,
cached_input_tokens: 0,
output_tokens: 5,
reasoning_output_tokens: 0,
total_tokens: 15,
},
total_token_usage: {
input_tokens: 10,
cached_input_tokens: 0,
output_tokens: 5,
reasoning_output_tokens: 0,
total_tokens: 15,
},
},
},
}),
].join('\n') + '\n')
process.env['HOME'] = home
process.env['CODEX_HOME'] = codexHome
process.env['CODEBURN_CACHE_DIR'] = cacheA
const { clearSessionCache, parseAllSessions } = await import('../src/parser.js')
clearSessionCache()
// parseAllSessions reaches its first await before any Codex cache access.
// Switching the host env immediately after invocation deterministically
// exercises every later read/write/flush under the captured A transaction.
const parsing = parseAllSessions(undefined, 'codex')
process.env['CODEBURN_CACHE_DIR'] = cacheB
const projects = await parsing
expect(projects.some(project => project.sessions.some(session =>
session.turns.some(turn => turn.assistantCalls.some(entry => entry.provider === 'codex'))
))).toBe(true)
expect(existsSync(join(cacheA, 'codex-results.json'))).toBe(true)
expect(existsSync(join(cacheB, 'codex-results.json'))).toBe(false)
const diskA = JSON.parse(await readFile(join(cacheA, 'codex-results.json'), 'utf8'))
expect(diskA.files[sourcePath].calls).toHaveLength(1)
clearSessionCache()
})
it('loads Antigravity cache entries from the active directory after A to B', async () => {
const sourcePath = join(root, 'shared.pb')
const cacheA = join(root, 'agy-cache-a')
const cacheB = join(root, 'agy-cache-b')
await writeFile(sourcePath, 'fixture')
await seedAntigravityCache(cacheA, sourcePath, 'from-a')
await seedAntigravityCache(cacheB, sourcePath, 'from-b')
process.env['CODEBURN_CACHE_DIR'] = cacheA
expect(await readAntigravityModel(sourcePath)).toBe('from-a')
process.env['CODEBURN_CACHE_DIR'] = cacheB
expect(await readAntigravityModel(sourcePath)).toBe('from-b')
})
it('does not flush dirty Antigravity state from A into B', async () => {
const sourcePath = join(root, 'shared.pb')
const cacheA = join(root, 'agy-cache-a-dirty')
const cacheB = join(root, 'agy-cache-b-dirty')
await writeFile(sourcePath, 'fixture')
await seedAntigravityCache(cacheA, sourcePath, 'from-a')
await seedAntigravityCache(cacheB, sourcePath, 'from-b')
process.env['CODEBURN_CACHE_DIR'] = cacheA
expect(await readAntigravityModel(sourcePath)).toBe('from-a')
// The provider parse transaction captures A. Even if the host changes its
// call-time env before the deferred flush, eviction/publication stays on A.
process.env['CODEBURN_CACHE_DIR'] = cacheB
await flushAntigravityCache(new Set(), cacheA)
expect(existsSync(join(cacheB, 'antigravity-results.json'))).toBe(true)
const diskB = JSON.parse(await readFile(join(cacheB, 'antigravity-results.json'), 'utf8'))
expect(diskB.cascades.shared.calls[0].model).toBe('from-b')
const diskA = JSON.parse(await readFile(join(cacheA, 'antigravity-results.json'), 'utf8'))
expect(diskA.cascades).toEqual({})
})
it('drops clean per-directory memos when the resident RSS guard clears them', async () => {
const codexSource = join(root, 'guard.jsonl')
const antigravitySource = join(root, 'shared.pb')
const cacheDir = join(root, 'guard-cache')
await writeFile(codexSource, '{}\n')
await writeFile(antigravitySource, 'fixture')
await seedAntigravityCache(cacheDir, antigravitySource, 'before')
process.env['CODEBURN_CACHE_DIR'] = cacheDir
await writeCachedCodexResults(codexSource, 'project', [call('codex', 'before')], (await fingerprintFile(codexSource))!)
await flushCodexCache()
expect(await readAntigravityModel(antigravitySource)).toBe('before')
// Another process republishes both cache files. Without the clear, the
// resident keeps serving its warm copies.
await seedAntigravityCache(cacheDir, antigravitySource, 'after')
const codexDisk = JSON.parse(await readFile(join(cacheDir, 'codex-results.json'), 'utf8'))
codexDisk.files[codexSource].calls[0].model = 'after'
await writeFile(join(cacheDir, 'codex-results.json'), JSON.stringify(codexDisk))
expect((await readCachedCodexResults(codexSource))?.calls.map(entry => entry.model)).toEqual(['before'])
expect(await readAntigravityModel(antigravitySource)).toBe('before')
clearCodexMemCaches()
clearAntigravityCacheStates()
expect((await readCachedCodexResults(codexSource))?.calls.map(entry => entry.model)).toEqual(['after'])
expect(await readAntigravityModel(antigravitySource)).toBe('after')
})
})