diff --git a/src/daily-cache.ts b/src/daily-cache.ts index c5641bf..6c93065 100644 --- a/src/daily-cache.ts +++ b/src/daily-cache.ts @@ -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 = { diff --git a/tests/daily-cache.test.ts b/tests/daily-cache.test.ts index 199d7a4..5ec2661 100644 --- a/tests/daily-cache.test.ts +++ b/tests/daily-cache.test.ts @@ -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', @@ -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 () => {