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.
205 lines
8 KiB
TypeScript
205 lines
8 KiB
TypeScript
/**
|
|
* Tests for kiro projectPath emission (sync attribution support).
|
|
*
|
|
* The kiro provider historically reduced the session's working directory to
|
|
* `basename(cwd)` for display and discarded the full path. Sync attribution
|
|
* (`codeburn sync push --attribution`) needs the full path on
|
|
* `ParsedProviderCall.projectPath` to resolve the git repo — without it,
|
|
* every kiro session is attribution-blind.
|
|
*
|
|
* Also covers the cache side: projectPath is persisted via CachedCall, so
|
|
* entries cached BEFORE the parser learned to emit it must re-parse. That is
|
|
* driven by the PROVIDER_PARSE_VERSIONS.kiro bump (project-path-v1); a cache
|
|
* seeded at the pre-bump fingerprint must be discarded.
|
|
*/
|
|
|
|
import { mkdir, writeFile, rm } from 'node:fs/promises'
|
|
import { createHash } from 'node:crypto'
|
|
import { join } from 'node:path'
|
|
|
|
import { describe, it, expect, beforeEach, afterAll, vi } from 'vitest'
|
|
|
|
import { clearSessionCache, parseAllSessions } from '../src/parser.js'
|
|
import {
|
|
CACHE_VERSION,
|
|
computeEnvFingerprint,
|
|
fingerprintFile,
|
|
type SessionCache,
|
|
} from '../src/session-cache.js'
|
|
import { writeCacheOnDisk } from './fixtures/session-cache-io.js'
|
|
|
|
// The kiro provider reads homedir()/env at call time in discovery; HOME must
|
|
// point at the test root before ../src/parser.js is evaluated (see the
|
|
// equivalent note in kiro-cache-invalidation.test.ts).
|
|
const testRoot = vi.hoisted(() => {
|
|
const root = `${process.env['TMPDIR'] || '/tmp'}/kiro-projpath-${process.pid}-${Date.now()}`
|
|
process.env['HOME'] = `${root}/home`
|
|
process.env['USERPROFILE'] = `${root}/home`
|
|
return root
|
|
})
|
|
|
|
const HOME = join(testRoot, 'home')
|
|
const CACHE_DIR = join(testRoot, 'cache')
|
|
const KIRO_SESSIONS = join(HOME, '.kiro', 'sessions')
|
|
const CLI_DIR = join(KIRO_SESSIONS, 'cli')
|
|
|
|
const CLI_CWD = '/local/home/testuser/workplace/my-project'
|
|
const V2_WORKSPACE = '/local/home/testuser/workplace/ide-project'
|
|
|
|
beforeEach(() => {
|
|
process.env['HOME'] = HOME
|
|
process.env['USERPROFILE'] = HOME
|
|
process.env['CODEBURN_CACHE_DIR'] = CACHE_DIR
|
|
delete process.env['KIRO_HOME']
|
|
clearSessionCache()
|
|
})
|
|
|
|
afterAll(async () => {
|
|
await rm(testRoot, { recursive: true, force: true })
|
|
})
|
|
|
|
/** Write a minimal kiro CLI session: <id>.jsonl entries + companion .json meta. */
|
|
async function seedCliSession(id: string, cwd: string): Promise<string> {
|
|
await mkdir(CLI_DIR, { recursive: true })
|
|
const jsonlPath = join(CLI_DIR, `${id}.jsonl`)
|
|
const entries = [
|
|
{ kind: 'Prompt', data: { content: [{ kind: 'text', data: 'add a feature' }] } },
|
|
{ kind: 'AssistantMessage', data: { content: [{ kind: 'text', data: 'Done — added the feature and tests.' }] } },
|
|
]
|
|
await writeFile(jsonlPath, entries.map(e => JSON.stringify(e)).join('\n'))
|
|
await writeFile(join(CLI_DIR, `${id}.json`), JSON.stringify({
|
|
session_id: id,
|
|
cwd,
|
|
created_at: '2026-08-01T10:00:00Z',
|
|
updated_at: '2026-08-01T10:05:00Z',
|
|
session_state: {
|
|
rts_model_state: { model_info: { model_id: 'auto' } },
|
|
conversation_metadata: {
|
|
user_turn_metadatas: [
|
|
{ end_timestamp: '2026-08-01T10:05:00Z', metering_usage: [] },
|
|
],
|
|
},
|
|
},
|
|
}))
|
|
return jsonlPath
|
|
}
|
|
|
|
/** Write a minimal v2 IDE session: sessions/<hash>/sess_<id>/{session.json,messages.jsonl}. */
|
|
async function seedV2Session(id: string, workspacePath: string): Promise<void> {
|
|
const sessDir = join(KIRO_SESSIONS, 'f'.repeat(32), `sess_${id}`)
|
|
await mkdir(sessDir, { recursive: true })
|
|
await writeFile(join(sessDir, 'session.json'), JSON.stringify({
|
|
id,
|
|
modelId: 'auto',
|
|
workspacePaths: [workspacePath],
|
|
createdAt: '2026-08-01T11:00:00Z',
|
|
}))
|
|
const events = [
|
|
{ timestamp: '2026-08-01T11:00:00Z', payload: { type: 'user', content: 'fix the bug' } },
|
|
{ timestamp: '2026-08-01T11:00:01Z', payload: { type: 'turn_start', executionId: 'x1' } },
|
|
{ timestamp: '2026-08-01T11:00:05Z', payload: { type: 'assistant', content: 'Fixed the bug in handler.ts by checking null first.' } },
|
|
{ timestamp: '2026-08-01T11:00:06Z', payload: { type: 'turn_end', executionId: 'x1' } },
|
|
]
|
|
await writeFile(join(sessDir, 'messages.jsonl'), events.map(e => JSON.stringify(e)).join('\n'))
|
|
}
|
|
|
|
function kiroAgentDir(): string {
|
|
if (process.platform === 'darwin') {
|
|
return join(HOME, 'Library', 'Application Support', 'Kiro', 'User', 'globalStorage', 'kiro.kiroagent')
|
|
}
|
|
if (process.platform === 'win32') {
|
|
return join(HOME, 'AppData', 'Roaming', 'Kiro', 'User', 'globalStorage', 'kiro.kiroagent')
|
|
}
|
|
return join(HOME, '.config', 'Kiro', 'User', 'globalStorage', 'kiro.kiroagent')
|
|
}
|
|
|
|
/** Write a minimal IDE workspace-session:
|
|
* <agentDir>/workspace-sessions/<base64(workspacePath), '='→'_'>/<sessionId>.json */
|
|
async function seedWorkspaceSession(id: string, workspaceDirectory: string): Promise<void> {
|
|
const encoded = Buffer.from(workspaceDirectory, 'utf-8').toString('base64').replace(/=/g, '_')
|
|
const dir = join(kiroAgentDir(), 'workspace-sessions', encoded)
|
|
await mkdir(dir, { recursive: true })
|
|
await writeFile(join(dir, `${id}.json`), JSON.stringify({
|
|
sessionId: id,
|
|
selectedModel: 'auto',
|
|
workspaceDirectory,
|
|
history: [
|
|
{ message: { role: 'user', content: 'refactor the config loader' } },
|
|
{ message: { role: 'assistant', content: 'Refactored the loader into three small functions with tests.' } },
|
|
],
|
|
}))
|
|
}
|
|
|
|
async function kiroCalls() {
|
|
const projects = await parseAllSessions(undefined, 'kiro')
|
|
return projects.flatMap(p => p.sessions.map(s => ({ project: p.project, projectPath: p.projectPath, session: s })))
|
|
}
|
|
|
|
describe('kiro projectPath emission', () => {
|
|
it('CLI session: projectPath is the full meta.cwd, project the basename', async () => {
|
|
await seedCliSession('cli-001', CLI_CWD)
|
|
const rows = await kiroCalls()
|
|
const row = rows.find(r => r.project === 'my-project')
|
|
expect(row).toBeDefined()
|
|
expect(row!.projectPath).toBe(CLI_CWD)
|
|
})
|
|
|
|
it('v2 IDE session: projectPath is workspacePaths[0]', async () => {
|
|
await seedV2Session('v2-001', V2_WORKSPACE)
|
|
const rows = await kiroCalls()
|
|
const row = rows.find(r => r.project === 'ide-project')
|
|
expect(row).toBeDefined()
|
|
expect(row!.projectPath).toBe(V2_WORKSPACE)
|
|
})
|
|
|
|
it('workspace session: projectPath is workspaceDirectory', async () => {
|
|
const WS_DIR = '/local/home/testuser/workplace/ws-project'
|
|
await seedWorkspaceSession('ws-001', WS_DIR)
|
|
const rows = await kiroCalls()
|
|
const row = rows.find(r => r.project === 'ws-project')
|
|
expect(row).toBeDefined()
|
|
expect(row!.projectPath).toBe(WS_DIR)
|
|
})
|
|
})
|
|
|
|
describe('kiro projectPath cache invalidation (project-path-v1 bump)', () => {
|
|
// The fingerprint a cache written by the PREVIOUS release carries: same env
|
|
// vars, but the parser version before the project-path-v1 bump.
|
|
function preBumpFingerprint(): string {
|
|
const parts = [`KIRO_HOME=${process.env['KIRO_HOME'] ?? ''}`, 'parser=ide-parsing-v1-est-cost']
|
|
return createHash('sha256').update(parts.join('\0')).digest('hex').slice(0, 16)
|
|
}
|
|
|
|
it('the bump changed the env fingerprint', () => {
|
|
expect(computeEnvFingerprint('kiro')).not.toBe(preBumpFingerprint())
|
|
})
|
|
|
|
it('a pre-bump cache entry (no projectPath) is re-parsed and gains projectPath', async () => {
|
|
const jsonlPath = await seedCliSession('cli-002', CLI_CWD)
|
|
|
|
// Seed a cache exactly as the pre-bump release would have left it:
|
|
// correct file fingerprint, pre-bump env fingerprint, turns WITHOUT
|
|
// projectPath on the cached calls.
|
|
const fp = await fingerprintFile(jsonlPath)
|
|
if (!fp) throw new Error('failed to fingerprint seeded session file')
|
|
const cache: SessionCache = {
|
|
version: CACHE_VERSION,
|
|
providers: {
|
|
kiro: {
|
|
envFingerprint: preBumpFingerprint(),
|
|
files: {
|
|
[jsonlPath]: { fingerprint: fp, mcpInventory: [], turns: [] },
|
|
},
|
|
},
|
|
},
|
|
}
|
|
await mkdir(CACHE_DIR, { recursive: true })
|
|
await writeCacheOnDisk(cache)
|
|
clearSessionCache()
|
|
|
|
const rows = await kiroCalls()
|
|
const row = rows.find(r => r.project === 'my-project')
|
|
expect(row).toBeDefined()
|
|
expect(row!.projectPath).toBe(CLI_CWD)
|
|
})
|
|
})
|