Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions src/daily-cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,19 @@ import type { DateRange, ProjectSummary } from './types.js'
// label. After the upgrade, the breakdown produces per-workspace project
// labels for new days; without invalidation the dashboard would show
// 'cursor' for historical days and `-Users-you-myproject` for new ones
// in the same window, producing a confusing mixed projection. v5 forces a
// full recompute.
// in the same window, producing a confusing mixed projection.
export const DAILY_CACHE_VERSION = 5
const MIN_SUPPORTED_VERSION = 2
// MIN_SUPPORTED_VERSION bumped to 5 too. The migration path
// (isMigratableCache + migrateDays) only fills in missing default fields;
// it does NOT recompute the providers / categories / models rollups from
// session data, because those raw sessions are not stored in the cache.
// So a migrated v2/v3/v4 cache would carry forward stale provider totals
// (single 'cursor' bucket instead of per-workspace) for the full cache
// retention window. Setting the floor to 5 forces those older caches to
// be discarded and recomputed cleanly. Confirmed by live test:
// menubar-json --period all reported cursor=$3.78 against a migrated
// v4 cache but $4.08 (correct) after the cache was discarded.
const MIN_SUPPORTED_VERSION = 5
const DAILY_CACHE_FILENAME = 'daily-cache.json'

export type DailyEntry = {
Expand Down
20 changes: 11 additions & 9 deletions tests/daily-cache.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,13 @@ describe('loadDailyCache', () => {
expect(existsSync(join(TMP_CACHE_ROOT, 'daily-cache.json.v1.bak'))).toBe(true)
})

it('migrates an older supported version by filling missing fields', async () => {
it('discards a v2 cache and starts fresh (provider rollups would be stale)', async () => {
// MIN_SUPPORTED_VERSION was raised to DAILY_CACHE_VERSION because the
// migration path cannot recompute the providers / categories / models
// rollups from session data (the cache does not retain raw sessions),
// so a migrated old cache would carry forward stale provider totals
// for the full retention window. Older caches now get discarded and
// recomputed from scratch on next run.
const saved = {
version: 2,
lastComputedDate: '2026-04-10',
Expand All @@ -92,14 +98,10 @@ describe('loadDailyCache', () => {
await writeFile(join(TMP_CACHE_ROOT, 'daily-cache.json'), JSON.stringify(saved), 'utf-8')
const cache = await loadDailyCache()
expect(cache.version).toBe(DAILY_CACHE_VERSION)
expect(cache.days).toHaveLength(1)
expect(cache.days[0].date).toBe('2026-04-10')
expect(cache.days[0].cost).toBe(10)
expect(cache.days[0].editTurns).toBe(0)
expect(cache.days[0].oneShotTurns).toBe(0)
expect(cache.days[0].categories).toEqual({})
expect(cache.days[0].providers).toEqual({})
expect(cache.days[0].models['claude-opus-4-6'].calls).toBe(5)
expect(cache.days).toEqual([])
expect(cache.lastComputedDate).toBeNull()
// Old cache is renamed to .v2.bak rather than deleted.
expect(existsSync(join(TMP_CACHE_ROOT, 'daily-cache.json.v2.bak'))).toBe(true)
})

it('round-trips a valid cache through save and load', async () => {
Expand Down
Loading