codeburn/tests/usage-aggregator-freshness.test.ts
iamtoruk be1b212a3f feat: surface-wide progressive hydration via an explicit payload marker
The TTY got the fast cold start in #1109; every JSON surface still waited
for the full parse because a payload has no way to say "partial". Add one.

- `hydration: { complete, indexedFiles, totalFiles }` on the menubar payload,
  add-only and emitted ONLY by the resident serve child (which polls, and
  therefore converges). Absence keeps meaning "complete", so every one-shot
  output is byte-identical and no script can be handed partial data.
- serve: a cold child answers `status --format menubar-json` from the files
  the requested period can show (same mtime floor as the TUI) and schedules
  the unfloored fill behind it. Partial answers are never memoized; requests
  waiting behind the fill are heartbeated by id so the client's no-output
  watchdog stays armed.
- `stale` keeps its own meaning: a first paint is fresh but partial, so it
  reports through `hydration` and never sets `stale`.
- desktop app and web dash render an honest "indexing history · N/M files"
  notice while `complete` is false.
2026-08-23 04:49:58 -07:00

130 lines
3.8 KiB
TypeScript

import { describe, expect, it, beforeAll, vi } from 'vitest'
import { buildMenubarPayloadForRange } from '../src/usage-aggregator.js'
import { getDateRange } from '../src/cli-date.js'
import { loadPricing } from '../src/models.js'
import type { ProjectSummary } from '../src/types.js'
const ts = new Date().toISOString()
function makeCall(savingsUSD: number, supplementary: boolean) {
return {
provider: 'copilot',
model: 'llama3.1:8b',
usage: {
inputTokens: 10,
outputTokens: supplementary ? 0 : 20,
cacheCreationInputTokens: 0,
cacheReadInputTokens: 0,
cachedInputTokens: 0,
reasoningTokens: 0,
webSearchRequests: 0,
},
costUSD: 0.01,
savingsUSD,
savingsBaselineModel: 'gpt-4o',
tools: [],
mcpTools: [],
skills: [],
subagentTypes: [],
hasAgentSpawn: false,
hasPlanMode: false,
speed: 'standard' as const,
timestamp: ts,
bashCommands: [],
deduplicationKey: supplementary ? 'sav-supp' : 'sav-real',
...(supplementary ? { supplementaryAccounting: true } : {}),
}
}
const emptyCat = { turns: 0, costUSD: 0, savingsUSD: 0, retries: 0, editTurns: 0, oneShotTurns: 0 }
function sessionFor(sourceId: string, sourceLabel: string, sourcePath: string) {
return {
sessionId: `sess-${sourceId}`,
project: `proj-${sourceId}`,
firstTimestamp: ts,
lastTimestamp: ts,
totalCostUSD: 0.02,
totalSavingsUSD: 7,
totalInputTokens: 20,
totalOutputTokens: 20,
totalCacheReadTokens: 0,
totalCacheWriteTokens: 0,
apiCalls: 1,
turns: [{
userMessage: 'hi',
timestamp: ts,
sessionId: `sess-${sourceId}`,
category: 'coding',
retries: 0,
hasEdits: false,
assistantCalls: [makeCall(5, false), makeCall(2, true)],
}],
modelBreakdown: {},
toolBreakdown: {},
mcpBreakdown: {},
bashBreakdown: {},
subagentBreakdown: {},
categoryBreakdown: { coding: { ...emptyCat, turns: 1, costUSD: 0.02, savingsUSD: 7 } },
skillBreakdown: {},
source: { id: sourceId, label: sourceLabel, path: sourcePath, kind: 'claude-config' },
}
}
function fixtureProjects(): ProjectSummary[] {
return [{
project: 'proj-a',
projectPath: 'proj-a',
sessions: [sessionFor('claude-config:a', 'A', '/a')],
totalCostUSD: 0.02,
totalSavingsUSD: 7,
totalApiCalls: 1,
}, {
project: 'proj-b',
projectPath: 'proj-b',
sessions: [sessionFor('claude-config:b', 'B', '/b')],
totalCostUSD: 0.02,
totalSavingsUSD: 7,
totalApiCalls: 1,
}] as unknown as ProjectSummary[]
}
vi.mock('../src/parser.js', async (importOriginal) => {
const mod = await importOriginal<typeof import('../src/parser.js')>()
let parseCalls = 0
return {
...mod,
parseAllSessions: vi.fn(async () => {
parseCalls++
return fixtureProjects()
}),
isSessionHydrationComplete: vi.fn(() => parseCalls === 1),
sessionHydrationSnapshot: vi.fn(() => ({
complete: parseCalls === 1,
deferredForFirstPaint: false,
indexedFiles: parseCalls,
pendingFiles: 0,
})),
}
})
describe('buildMenubarPayloadForRange: hydration freshness marker', () => {
beforeAll(async () => {
await loadPricing()
})
it('reflects the primary parse hydration, not a later bystander parse', async () => {
// Two parseAllSessions calls happen on the scoped branch: the primary one
// (claude-only) and a second one for the 365-day history block. The mock
// makes the primary complete (parseCalls === 1 -> true) and the second
// incomplete (parseCalls === 2 -> false). The payload must trust the
// primary outcome, captured immediately after it resolves.
const payload = await buildMenubarPayloadForRange(getDateRange('today'), {
provider: 'all',
optimize: false,
claudeConfigSourceId: 'claude-config:a',
})
expect(payload.stale).toBeUndefined()
})
})