mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-10-25 16:19:39 +00:00
* fix(cursor-agent): leave Cursor IDE chat transcripts to the cursor provider Cursor IDE agent chats write agent transcripts too, so on days without a Cursor import they were counted by both cursor (state.vscdb) and cursor-agent. Discovery now skips a transcript or store whose id is an IDE composerData key; cursor keeps them with per-bubble times and models. Daily cache v73 lets cursor-agent days shrink once. * fix(cache): drop old cursor-agent slices the IDE parse now explains On an existing cache, settled days kept their old cursor-agent slice after v73 because the fresh parse no longer has one there and the merge carries missing slices forward. While cursor-agent is pending its v73 re-derive, a day the fresh parse explains with a cursor slice and no cursor-agent slice drops the old cursor-agent slice; days with no fresh cursor slice keep it.
203 lines
6.7 KiB
TypeScript
203 lines
6.7 KiB
TypeScript
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
|
import { mkdir, readFile, rm, writeFile } from 'fs/promises'
|
|
import { join } from 'path'
|
|
import { tmpdir } from 'os'
|
|
|
|
import {
|
|
DAILY_CACHE_VERSION,
|
|
currentTzKey,
|
|
ensureCacheHydrated,
|
|
loadDailyCache,
|
|
toDateString,
|
|
type DailyEntry,
|
|
} from '../src/daily-cache.js'
|
|
|
|
// One below the current version, so this pins the ADJACENT-version case: v20
|
|
// is the SHIPPED predecessor (#1040, codex model attribution), and its days
|
|
// must be re-derived rather than adopted as finalized under a number that now
|
|
// means different accounting. Anything below MIN_SUPPORTED_VERSION is
|
|
// untrusted, which is what makes the re-derivation global rather than
|
|
// provider-scoped.
|
|
const PRE_FIX_DAILY_VERSION = 20
|
|
const cacheRoot = join(tmpdir(), `codeburn-daily-rederive-${process.pid}-${Date.now()}`)
|
|
|
|
function day(date: string, cost: number): DailyEntry {
|
|
return {
|
|
date,
|
|
cost,
|
|
savingsUSD: 0,
|
|
calls: 1,
|
|
sessions: 1,
|
|
inputTokens: 100,
|
|
outputTokens: 20,
|
|
cacheReadTokens: 30,
|
|
cacheWriteTokens: 0,
|
|
editTurns: 0,
|
|
oneShotTurns: 0,
|
|
models: {
|
|
'Grok Build': {
|
|
calls: 1,
|
|
cost,
|
|
savingsUSD: 0,
|
|
inputTokens: 100,
|
|
outputTokens: 20,
|
|
cacheReadTokens: 30,
|
|
cacheWriteTokens: 0,
|
|
},
|
|
},
|
|
categories: {},
|
|
providers: {
|
|
grok: {
|
|
calls: 1,
|
|
cost,
|
|
savingsUSD: 0,
|
|
sessions: 1,
|
|
inputTokens: 100,
|
|
outputTokens: 20,
|
|
cacheReadTokens: 30,
|
|
cacheWriteTokens: 0,
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
beforeEach(async () => {
|
|
process.env['CODEBURN_CACHE_DIR'] = cacheRoot
|
|
await rm(cacheRoot, { recursive: true, force: true })
|
|
await mkdir(cacheRoot, { recursive: true })
|
|
})
|
|
|
|
afterEach(async () => {
|
|
await rm(cacheRoot, { recursive: true, force: true })
|
|
})
|
|
|
|
// Raising MIN_SUPPORTED_VERSION re-derives EVERY day from EVERY provider - the
|
|
// daily cache has no per-provider invalidation. Which provider the seeded day
|
|
// belongs to is incidental; the mechanism under test is version-wide.
|
|
describe('daily-cache re-derivation on a DAILY_CACHE_VERSION bump', () => {
|
|
it('re-derives a day from a below-minimum v20 cache while preserving the old file', async () => {
|
|
const date = toDateString(new Date(Date.now() - 7 * 24 * 60 * 60 * 1000))
|
|
const yesterday = toDateString(new Date(Date.now() - 24 * 60 * 60 * 1000))
|
|
const oldPath = join(cacheRoot, `daily-cache.v${PRE_FIX_DAILY_VERSION}.json`)
|
|
const oldCache = {
|
|
version: PRE_FIX_DAILY_VERSION,
|
|
savingsConfigHash: 'cfg',
|
|
tzKey: currentTzKey(),
|
|
lastComputedDate: yesterday,
|
|
days: [day(date, 99)],
|
|
complete: true,
|
|
watermarkTrusted: true,
|
|
}
|
|
await writeFile(oldPath, JSON.stringify(oldCache))
|
|
|
|
let parseCount = 0
|
|
const corrected = day(date, 2)
|
|
const hydrated = await ensureCacheHydrated(
|
|
async () => {
|
|
parseCount++
|
|
return []
|
|
},
|
|
() => [corrected],
|
|
'cfg',
|
|
() => true,
|
|
)
|
|
|
|
const refreshedDay = hydrated.days.find(entry => entry.date === date)
|
|
expect(parseCount).toBe(1)
|
|
expect(refreshedDay?.providers.grok?.cost).toBe(2)
|
|
expect(refreshedDay?.cost).toBe(2)
|
|
expect(JSON.parse(await readFile(oldPath, 'utf8'))).toEqual(oldCache)
|
|
})
|
|
})
|
|
|
|
// v30 is claimed by two open branches at once: this one and #1132. A v30 file
|
|
// on disk therefore carries UNKNOWN accounting, so adopting it as the
|
|
// finalized base would serve #1132's numbers under this branch's contract.
|
|
describe('daily-cache adoption of a v30 file written under a different accounting', () => {
|
|
it('carries its days forward but never adopts it as the finalized base', async () => {
|
|
const date = toDateString(new Date(Date.now() - 7 * 24 * 60 * 60 * 1000))
|
|
const yesterday = toDateString(new Date(Date.now() - 24 * 60 * 60 * 1000))
|
|
const foreign = day(date, 42)
|
|
foreign.providers = {
|
|
hermes: {
|
|
calls: 1,
|
|
cost: 42,
|
|
savingsUSD: 0,
|
|
sessions: 1,
|
|
inputTokens: 100,
|
|
outputTokens: 20,
|
|
cacheReadTokens: 30,
|
|
cacheWriteTokens: 0,
|
|
},
|
|
}
|
|
await writeFile(
|
|
join(cacheRoot, 'daily-cache.v30.json'),
|
|
JSON.stringify({
|
|
version: 30,
|
|
savingsConfigHash: 'cfg',
|
|
tzKey: currentTzKey(),
|
|
lastComputedDate: yesterday,
|
|
days: [foreign],
|
|
complete: true,
|
|
watermarkTrusted: true,
|
|
}),
|
|
)
|
|
|
|
const loaded = await loadDailyCache()
|
|
|
|
expect(loaded.version).toBe(DAILY_CACHE_VERSION)
|
|
// A v30 candidate must not satisfy same-version adoption, so its trust
|
|
// markers cannot survive migration to the current cache version.
|
|
expect(loaded.complete).toBe(false)
|
|
expect(loaded.watermarkTrusted).toBe(false)
|
|
const carried = loaded.days.find(entry => entry.date === date)
|
|
expect(carried?.carried).toBe(true)
|
|
expect(loaded.pendingRederive).toContain('hermes')
|
|
expect(loaded.pendingRederive).toContain('dsh')
|
|
})
|
|
})
|
|
|
|
describe('cursor-agent re-derive after Cursor IDE chats moved to cursor (v73)', () => {
|
|
function withSlices(date: string, providers: Record<string, number>): DailyEntry {
|
|
const entry = day(date, 0)
|
|
entry.providers = {}
|
|
entry.models = {}
|
|
entry.calls = 0
|
|
for (const [name, cost] of Object.entries(providers)) {
|
|
entry.providers[name] = { ...day(date, cost).providers.grok! }
|
|
entry.cost += cost
|
|
entry.calls += 1
|
|
}
|
|
return entry
|
|
}
|
|
|
|
it('drops the old cursor-agent slice only where the fresh parse has a cursor slice instead', async () => {
|
|
const moved = toDateString(new Date(Date.now() - 7 * 24 * 60 * 60 * 1000))
|
|
const pruned = toDateString(new Date(Date.now() - 8 * 24 * 60 * 60 * 1000))
|
|
const yesterday = toDateString(new Date(Date.now() - 24 * 60 * 60 * 1000))
|
|
await writeFile(join(cacheRoot, 'daily-cache.v72.json'), JSON.stringify({
|
|
version: 72,
|
|
savingsConfigHash: 'cfg',
|
|
tzKey: currentTzKey(),
|
|
lastComputedDate: yesterday,
|
|
days: [withSlices(moved, { 'cursor-agent': 5, claude: 1 }), withSlices(pruned, { 'cursor-agent': 3, claude: 1 })],
|
|
complete: true,
|
|
watermarkTrusted: true,
|
|
}))
|
|
|
|
const hydrated = await ensureCacheHydrated(
|
|
async () => [],
|
|
() => [withSlices(moved, { cursor: 2, claude: 1 }), withSlices(pruned, { claude: 1 })],
|
|
'cfg',
|
|
() => true,
|
|
)
|
|
|
|
const movedDay = hydrated.days.find(entry => entry.date === moved)!
|
|
expect(movedDay.providers['cursor-agent']).toBeUndefined()
|
|
expect(movedDay.providers.cursor?.cost).toBe(2)
|
|
expect(movedDay.cost).toBe(3)
|
|
const prunedDay = hydrated.days.find(entry => entry.date === pruned)!
|
|
expect(prunedDay.providers['cursor-agent']?.cost).toBe(3)
|
|
expect(prunedDay.cost).toBe(4)
|
|
})
|
|
})
|