Files
getagentseal--codeburn/tests/daily-cache-version-rederivation.test.ts
iamtoruk f77687a8b3 Merge origin/main, and act on vidoluco's real-Copilot findings
Merge resolution
- daily-cache: main shipped v20 with #1040, so 21 is now the first FREE number
  rather than one dodging an unmerged head. Both version notes kept, ours on
  top. The re-derivation seed stays one below current (20), now the shipped
  predecessor rather than a draft.
- upgrade-path: expected daily cache filename stays v21.

The rollup is cumulative across resume legs — already handled, now pinned
Measured on a real 3-leg session (CLI 1.0.80): every counter in a leg includes
the legs before it, and the last leg of a complete session equals its store-row
total to the nano. The parser has always emitted `cumulative - previous
cumulative` per model, so the interval arithmetic downstream already consumes
per-leg claims; it was the PR body and the provider doc that described the raw
journal as per-leg and misled the reader. Doc corrected, and (c3) pins it end
to end: a complete 3-leg session serves 900 (its last leg / store total), not
1,400 (the sum of its legs); an uncovered one serves 900 exactly once; and a
leg reporting LESS than its predecessor is taken as a fresh epoch rather than
clamped to a negative delta, so an older per-leg CLI never loses a leg.

initiator = 'compaction' replaces the timestamp heuristic where it exists
The summarization request does write its own assistant_usage_events row, and
newer stores label it. The label is now read — schema-adaptively, and the
enrichment select is a graduated chain so a store carrying the billing columns
but not `initiator` keeps its billing metadata instead of falling all the way
back to the base select. Two uses: the row is subtracted from the leg it
belongs to even though it commits before the compaction stamp, and it is kept
out of per-turn pairing since it has no assistant.message to pair with.
Optional twice over — absent on older stores, NULL on 1,504 of 2,509 rows on a
real one — so (c5) pins the labelled path at 350, the identical UNLABELLED
fixture at 400 (the documented one-request-per-compaction over-serve, which no
timestamp rule can close: the request that triggered the compaction completes
immediately before it too), and a compaction row never stealing a pairing
partner.

Attribution invariant, standing guard for the 1.8x report
(c4) runs `codeburn audit`'s own two numbers — attributed vs recomputed —
over all four combinations of the three representations a session can be
written in, at the magnitudes of a real reported day (gpt-5.6-terra, 146 rows,
input 17,792 / cache write 501,395 / cache read 12,097,364 / output 63,344 /
reasoning 24,831, billed $4.47). Every shape reconciles at 1.000, and the
covered case lands on $4.4687 = what the tokens price at = what GitHub billed,
with output 63,344 rather than the pre-fix 88,175 that re-priced reasoning on
top of itself.

upgrade-path: a day KEY is not the unit of never-lose
A parse change that re-dates a call to its true day legitimately empties one
day and fills its neighbour, token for token — observed on a real cache where
2026-08-08's single call moved to 08-07 exactly. The aging step now compares a
+/-1 day WINDOW rather than the day alone, so only a window that shrinks is a
loss, and an emptied day key is reported rather than failed.

RECONCILE_SETTLE_MS keeps 24h, with the measurement that argues against it
recorded at the constant and in the provider doc: across 91 real sessions zero
rows landed after shutdown (median -0.1s, max -0.0s). One machine, one CLI
version — the number to beat is seconds, not hours, once a second machine
agrees.
2026-08-21 02:14:14 -07:00

110 lines
3.2 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 {
currentTzKey,
ensureCacheHydrated,
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)
})
})