mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-05-02 00:40:14 +00:00
363 lines
13 KiB
TypeScript
363 lines
13 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
|
import { createHash } from 'crypto'
|
|
import { existsSync } from 'fs'
|
|
import { mkdir, mkdtemp, readFile, readdir, rm, writeFile } from 'fs/promises'
|
|
import { tmpdir } from 'os'
|
|
import { join } from 'path'
|
|
|
|
import {
|
|
SOURCE_CACHE_VERSION,
|
|
emptySourceCacheManifest,
|
|
loadSourceCacheManifest,
|
|
saveSourceCacheManifest,
|
|
readSourceCacheEntry,
|
|
writeSourceCacheEntry,
|
|
computeFileFingerprint,
|
|
type SourceCacheEntry,
|
|
} from '../src/source-cache.js'
|
|
import type { SessionSummary } from '../src/types.js'
|
|
|
|
let root = ''
|
|
|
|
function emptySession(sessionId: string, overrides: Partial<SessionSummary> = {}): SessionSummary {
|
|
return {
|
|
sessionId,
|
|
project: 'project',
|
|
firstTimestamp: '2026-04-10T00:00:00Z',
|
|
lastTimestamp: '2026-04-10T00:00:00Z',
|
|
totalCostUSD: 0,
|
|
totalInputTokens: 0,
|
|
totalOutputTokens: 0,
|
|
totalCacheReadTokens: 0,
|
|
totalCacheWriteTokens: 0,
|
|
apiCalls: 0,
|
|
turns: [],
|
|
modelBreakdown: {},
|
|
toolBreakdown: {},
|
|
mcpBreakdown: {},
|
|
bashBreakdown: {},
|
|
categoryBreakdown: {},
|
|
...overrides,
|
|
}
|
|
}
|
|
|
|
beforeEach(async () => {
|
|
root = await mkdtemp(join(tmpdir(), 'codeburn-source-cache-'))
|
|
process.env['CODEBURN_CACHE_DIR'] = root
|
|
})
|
|
|
|
afterEach(async () => {
|
|
delete process.env['CODEBURN_CACHE_DIR']
|
|
if (root) await rm(root, { recursive: true, force: true })
|
|
})
|
|
|
|
describe('source cache manifest', () => {
|
|
it('returns an empty manifest when no file exists', async () => {
|
|
await expect(loadSourceCacheManifest()).resolves.toEqual(emptySourceCacheManifest())
|
|
})
|
|
|
|
it('returns an empty manifest when the manifest shape is invalid', async () => {
|
|
await mkdir(join(root, 'source-cache-v1'), { recursive: true })
|
|
await writeFile(join(root, 'source-cache-v1', 'manifest.json'), JSON.stringify({
|
|
version: SOURCE_CACHE_VERSION,
|
|
entries: { bad: { file: 123, provider: 'fake' } },
|
|
}), 'utf-8')
|
|
|
|
await expect(loadSourceCacheManifest()).resolves.toEqual(emptySourceCacheManifest())
|
|
})
|
|
|
|
it('returns an empty manifest when an entry filename is unsafe', async () => {
|
|
await mkdir(join(root, 'source-cache-v1'), { recursive: true })
|
|
await writeFile(join(root, 'source-cache-v1', 'manifest.json'), JSON.stringify({
|
|
version: SOURCE_CACHE_VERSION,
|
|
entries: {
|
|
bad: {
|
|
file: '../escape.json',
|
|
provider: 'fake',
|
|
logicalPath: join(root, 'source.jsonl'),
|
|
},
|
|
},
|
|
}), 'utf-8')
|
|
|
|
await expect(loadSourceCacheManifest()).resolves.toEqual(emptySourceCacheManifest())
|
|
})
|
|
|
|
it('round-trips a manifest and entry', async () => {
|
|
const sourcePath = join(root, 'source.jsonl')
|
|
await writeFile(sourcePath, '{"ok":true}\n', 'utf-8')
|
|
const fingerprint = await computeFileFingerprint(sourcePath)
|
|
const entry: SourceCacheEntry = {
|
|
version: SOURCE_CACHE_VERSION,
|
|
provider: 'fake',
|
|
logicalPath: sourcePath,
|
|
fingerprintPath: sourcePath,
|
|
cacheStrategy: 'full-reparse',
|
|
parserVersion: 'fake-v1',
|
|
fingerprint,
|
|
sessions: [],
|
|
}
|
|
|
|
const manifest = await loadSourceCacheManifest()
|
|
await writeSourceCacheEntry(manifest, entry)
|
|
await saveSourceCacheManifest(manifest)
|
|
|
|
const loadedManifest = await loadSourceCacheManifest()
|
|
const loadedEntry = await readSourceCacheEntry(loadedManifest, 'fake', sourcePath)
|
|
expect(loadedEntry).toEqual(entry)
|
|
})
|
|
|
|
it('returns null when the fingerprint no longer matches', async () => {
|
|
const sourcePath = join(root, 'source.jsonl')
|
|
await writeFile(sourcePath, 'one\n', 'utf-8')
|
|
const fingerprint = await computeFileFingerprint(sourcePath)
|
|
const entry: SourceCacheEntry = {
|
|
version: SOURCE_CACHE_VERSION,
|
|
provider: 'fake',
|
|
logicalPath: sourcePath,
|
|
fingerprintPath: sourcePath,
|
|
cacheStrategy: 'full-reparse',
|
|
parserVersion: 'fake-v1',
|
|
fingerprint,
|
|
sessions: [],
|
|
}
|
|
|
|
const manifest = await loadSourceCacheManifest()
|
|
await writeSourceCacheEntry(manifest, entry)
|
|
await saveSourceCacheManifest(manifest)
|
|
|
|
await writeFile(sourcePath, 'one\ntwo\n', 'utf-8')
|
|
const loaded = await readSourceCacheEntry(await loadSourceCacheManifest(), 'fake', sourcePath)
|
|
expect(loaded).toBeNull()
|
|
})
|
|
|
|
it('returns null when the cached entry shape is invalid', async () => {
|
|
const sourcePath = join(root, 'source.jsonl')
|
|
await writeFile(sourcePath, 'one\n', 'utf-8')
|
|
const manifest = await loadSourceCacheManifest()
|
|
const file = `${createHash('sha1').update(`fake:${sourcePath}`).digest('hex')}.json`
|
|
manifest.entries[`fake:${sourcePath}`] = { file, provider: 'fake', logicalPath: sourcePath }
|
|
await saveSourceCacheManifest(manifest)
|
|
await mkdir(join(root, 'source-cache-v1', 'entries'), { recursive: true })
|
|
await writeFile(join(root, 'source-cache-v1', 'entries', file), JSON.stringify({
|
|
version: SOURCE_CACHE_VERSION,
|
|
provider: 'fake',
|
|
logicalPath: sourcePath,
|
|
fingerprintPath: sourcePath,
|
|
cacheStrategy: 'full-reparse',
|
|
parserVersion: 'fake-v1',
|
|
fingerprint: { mtimeMs: 'nope', sizeBytes: 4 },
|
|
sessions: [],
|
|
}), 'utf-8')
|
|
|
|
const loaded = await readSourceCacheEntry(await loadSourceCacheManifest(), 'fake', sourcePath)
|
|
expect(loaded).toBeNull()
|
|
})
|
|
|
|
it('returns null when the manifest metadata does not match the lookup request', async () => {
|
|
const sourcePath = join(root, 'source.jsonl')
|
|
await writeFile(sourcePath, 'one\n', 'utf-8')
|
|
const fingerprint = await computeFileFingerprint(sourcePath)
|
|
const file = `${createHash('sha1').update(`fake:${sourcePath}`).digest('hex')}.json`
|
|
const manifest = await loadSourceCacheManifest()
|
|
manifest.entries[`fake:${sourcePath}`] = {
|
|
file,
|
|
provider: 'other',
|
|
logicalPath: sourcePath,
|
|
}
|
|
await saveSourceCacheManifest(manifest)
|
|
await mkdir(join(root, 'source-cache-v1', 'entries'), { recursive: true })
|
|
await writeFile(join(root, 'source-cache-v1', 'entries', file), JSON.stringify({
|
|
version: SOURCE_CACHE_VERSION,
|
|
provider: 'fake',
|
|
logicalPath: sourcePath,
|
|
fingerprintPath: sourcePath,
|
|
cacheStrategy: 'full-reparse',
|
|
parserVersion: 'fake-v1',
|
|
fingerprint,
|
|
sessions: [],
|
|
}), 'utf-8')
|
|
|
|
const loaded = await readSourceCacheEntry(await loadSourceCacheManifest(), 'fake', sourcePath)
|
|
expect(loaded).toBeNull()
|
|
})
|
|
|
|
it('returns null when a nested assistant call is malformed', async () => {
|
|
const sourcePath = join(root, 'source.jsonl')
|
|
await writeFile(sourcePath, 'one\n', 'utf-8')
|
|
const fingerprint = await computeFileFingerprint(sourcePath)
|
|
const entry: SourceCacheEntry = {
|
|
version: SOURCE_CACHE_VERSION,
|
|
provider: 'fake',
|
|
logicalPath: sourcePath,
|
|
fingerprintPath: sourcePath,
|
|
cacheStrategy: 'full-reparse',
|
|
parserVersion: 'fake-v1',
|
|
fingerprint,
|
|
sessions: [
|
|
emptySession('session-1', {
|
|
turns: [{
|
|
userMessage: 'hello',
|
|
assistantCalls: [{
|
|
provider: 'fake',
|
|
model: 'model',
|
|
usage: {
|
|
inputTokens: 1,
|
|
outputTokens: 1,
|
|
cacheCreationInputTokens: 0,
|
|
cacheReadInputTokens: 0,
|
|
cachedInputTokens: 0,
|
|
reasoningTokens: 0,
|
|
webSearchRequests: 0,
|
|
},
|
|
costUSD: 1,
|
|
tools: [],
|
|
mcpTools: [],
|
|
hasAgentSpawn: false,
|
|
hasPlanMode: false,
|
|
speed: 'standard',
|
|
timestamp: '2026-04-10T00:00:00Z',
|
|
bashCommands: [],
|
|
deduplicationKey: 'k',
|
|
}],
|
|
timestamp: '2026-04-10T00:00:00Z',
|
|
sessionId: 'session-1',
|
|
}],
|
|
}),
|
|
],
|
|
}
|
|
|
|
const manifest = await loadSourceCacheManifest()
|
|
await writeSourceCacheEntry(manifest, entry)
|
|
await saveSourceCacheManifest(manifest)
|
|
|
|
await writeFile(join(root, 'source-cache-v1', 'entries', `${createHash('sha1').update(`fake:${sourcePath}`).digest('hex')}.json`), JSON.stringify({
|
|
...entry,
|
|
sessions: [{
|
|
...entry.sessions[0],
|
|
turns: [{
|
|
...entry.sessions[0].turns[0],
|
|
assistantCalls: [{
|
|
...entry.sessions[0].turns[0].assistantCalls[0],
|
|
usage: { ...entry.sessions[0].turns[0].assistantCalls[0].usage, inputTokens: 'bad' },
|
|
}],
|
|
}],
|
|
}],
|
|
}), 'utf-8')
|
|
|
|
const loaded = await readSourceCacheEntry(await loadSourceCacheManifest(), 'fake', sourcePath)
|
|
expect(loaded).toBeNull()
|
|
})
|
|
|
|
it('returns null when append state is malformed', async () => {
|
|
const sourcePath = join(root, 'source.jsonl')
|
|
await writeFile(sourcePath, 'one\n', 'utf-8')
|
|
const fingerprint = await computeFileFingerprint(sourcePath)
|
|
const entry = {
|
|
version: SOURCE_CACHE_VERSION,
|
|
provider: 'fake',
|
|
logicalPath: sourcePath,
|
|
fingerprintPath: sourcePath,
|
|
cacheStrategy: 'append-jsonl' as const,
|
|
parserVersion: 'fake-v1',
|
|
fingerprint,
|
|
sessions: [],
|
|
appendState: { endOffset: 'bad', tailHash: 'abc' },
|
|
}
|
|
|
|
const manifest = await loadSourceCacheManifest()
|
|
await writeSourceCacheEntry(manifest, entry as SourceCacheEntry)
|
|
await saveSourceCacheManifest(manifest)
|
|
|
|
const loaded = await readSourceCacheEntry(await loadSourceCacheManifest(), 'fake', sourcePath)
|
|
expect(loaded).toBeNull()
|
|
})
|
|
|
|
it('returns null when a breakdown map contains malformed values', async () => {
|
|
const sourcePath = join(root, 'source.jsonl')
|
|
await writeFile(sourcePath, 'one\n', 'utf-8')
|
|
const fingerprint = await computeFileFingerprint(sourcePath)
|
|
const entry: SourceCacheEntry = {
|
|
version: SOURCE_CACHE_VERSION,
|
|
provider: 'fake',
|
|
logicalPath: sourcePath,
|
|
fingerprintPath: sourcePath,
|
|
cacheStrategy: 'full-reparse',
|
|
parserVersion: 'fake-v1',
|
|
fingerprint,
|
|
sessions: [
|
|
emptySession('session-2', {
|
|
modelBreakdown: {
|
|
modelA: {
|
|
calls: 'bad',
|
|
costUSD: 0,
|
|
tokens: {
|
|
inputTokens: 0,
|
|
outputTokens: 0,
|
|
cacheCreationInputTokens: 0,
|
|
cacheReadInputTokens: 0,
|
|
cachedInputTokens: 0,
|
|
reasoningTokens: 0,
|
|
webSearchRequests: 0,
|
|
},
|
|
},
|
|
},
|
|
}),
|
|
],
|
|
}
|
|
|
|
const manifest = await loadSourceCacheManifest()
|
|
await writeSourceCacheEntry(manifest, entry)
|
|
await saveSourceCacheManifest(manifest)
|
|
|
|
const loaded = await readSourceCacheEntry(await loadSourceCacheManifest(), 'fake', sourcePath)
|
|
expect(loaded).toBeNull()
|
|
})
|
|
|
|
it('writes atomically without leaving temp files behind', async () => {
|
|
const sourcePath = join(root, 'source.jsonl')
|
|
await writeFile(sourcePath, 'x\n', 'utf-8')
|
|
const manifest = await loadSourceCacheManifest()
|
|
await writeSourceCacheEntry(manifest, {
|
|
version: SOURCE_CACHE_VERSION,
|
|
provider: 'fake',
|
|
logicalPath: sourcePath,
|
|
fingerprintPath: sourcePath,
|
|
cacheStrategy: 'full-reparse',
|
|
parserVersion: 'fake-v1',
|
|
fingerprint: await computeFileFingerprint(sourcePath),
|
|
sessions: [],
|
|
})
|
|
await saveSourceCacheManifest(manifest)
|
|
|
|
const files = JSON.parse(await readFile(join(root, 'source-cache-v1', 'manifest.json'), 'utf-8'))
|
|
expect(files.version).toBe(SOURCE_CACHE_VERSION)
|
|
expect(existsSync(join(root, 'source-cache-v1', 'entries'))).toBe(true)
|
|
const cacheFiles = await readdir(join(root, 'source-cache-v1'))
|
|
const entryFiles = await readdir(join(root, 'source-cache-v1', 'entries'))
|
|
expect(cacheFiles.some(f => f.endsWith('.tmp'))).toBe(false)
|
|
expect(entryFiles.some(f => f.endsWith('.tmp'))).toBe(false)
|
|
})
|
|
|
|
it('does not mutate the manifest when the entry write fails', async () => {
|
|
const sourcePath = join(root, 'source.jsonl')
|
|
await writeFile(sourcePath, 'x\n', 'utf-8')
|
|
const manifest = await loadSourceCacheManifest()
|
|
const provider = 'fake'
|
|
const logicalPath = sourcePath
|
|
const file = `${createHash('sha1').update(`${provider}:${logicalPath}`).digest('hex')}.json`
|
|
await mkdir(join(root, 'source-cache-v1', 'entries', file), { recursive: true })
|
|
|
|
await expect(writeSourceCacheEntry(manifest, {
|
|
version: SOURCE_CACHE_VERSION,
|
|
provider,
|
|
logicalPath,
|
|
fingerprintPath: sourcePath,
|
|
cacheStrategy: 'full-reparse',
|
|
parserVersion: 'fake-v1',
|
|
fingerprint: await computeFileFingerprint(sourcePath),
|
|
sessions: [],
|
|
})).rejects.toBeTruthy()
|
|
|
|
expect(manifest.entries[`fake:${sourcePath}`]).toBeUndefined()
|
|
})
|
|
})
|