prerelease: quota NaN guard, 10-year daily retention, test date bomb (#764)

Three review leftovers plus one inconsistency found while fixing them.

computePace returns undefined for a non-finite usedFraction instead of
letting NaN sail through the clamp into every pace field.

Daily-cache retention goes from 730 days to 3650: the cache is the only
durable record of carried days and lifetime now reads from it, so the
2-year prune would have replayed the lost-history bug in slow motion at
that horizon. Measured cost stays small (~2.3 MB / ~11 ms parse per 730
dense days).

The vercel-gateway dashboard-range test pins a relative day instead of
2026-06-01, which would have started failing once the rolling 6-month
window moved past it around December.
This commit is contained in:
Resham Joshi 2026-07-20 07:37:56 -07:00 committed by GitHub
parent db8ef9b466
commit b30818138e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 34 additions and 9 deletions

View file

@ -623,10 +623,14 @@ export function withDailyCacheLock<T>(fn: () => Promise<T>): Promise<T> {
export const MS_PER_DAY = 24 * 60 * 60 * 1000
export const BACKFILL_DAYS = 365
// Keep 2 years of history so the longest bounded UI period (6 months
// via `all`) and the uncapped `lifetime` period both have headroom to
// read from cache while old entries get pruned.
export const DAILY_CACHE_RETENTION_DAYS = 730
// Ten years. This cache is the ONLY durable record of carried days (their
// session files are long deleted), and the uncapped `lifetime` period reads
// from it via buildDurablePeriod, so pruning at the old 2-year mark would
// have replayed the lost-history bug in slow motion at that horizon.
// Measured envelope keeps this honest: ~2.3 MB / ~11 ms JSON parse per 730
// days of fully dense data, so even a decade of daily use stays ~11 MB and
// well under 100 ms on the polling path.
export const DAILY_CACHE_RETENTION_DAYS = 3650
export function toDateString(date: Date): string {
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`

View file

@ -76,6 +76,10 @@ export function computePace(window: QuotaWindow, now: Date = new Date()): QuotaP
const elapsedSeconds = windowSeconds - remainingSeconds
const expectedFraction = elapsedSeconds / windowSeconds
if (expectedFraction < QUOTA_PACE_MIN_ELAPSED_FRACTION) return undefined
// A NaN usedFraction (e.g. derived from used/limit with limit 0) would sail
// through the clamp below (min/max propagate NaN) and poison every pace
// field. Unknown usage means no pace, not a NaN pace.
if (!Number.isFinite(window.usedFraction)) return undefined
const used = Math.min(Math.max(window.usedFraction, 0), 1)
if (used >= 1) return undefined

View file

@ -388,7 +388,7 @@ describe('never-lose invariant: invalidations with vanished sources', () => {
})
it('retention still prunes ancient carried days after a rebuild', async () => {
await seed({ days: [seededDay(), day('2020-01-01', { claude: slice(1, 1) })], complete: false })
await seed({ days: [seededDay(), day('2010-01-01', { claude: slice(1, 1) })], complete: false })
const out = await ensureCacheHydrated(noSessions, () => [], 'cfg-A')
expect(out.days.map(d => d.date)).toEqual([daysAgoStr(30)])
})
@ -483,7 +483,7 @@ describe('adoption union across older cache files', () => {
const old = {
version: 13,
days: [
day('2023-01-01', { claude: slice(1, 1) }),
day('2010-01-01', { claude: slice(1, 1) }),
day(daysAgoStr(15), { claude: slice(5, 2) }),
day(daysAgoStr(0), { claude: slice(99, 9) }),
],

View file

@ -260,7 +260,7 @@ describe('addNewDays', () => {
})
it('still prunes when newestDate is valid', () => {
const old = '2020-01-01'
const old = '2010-01-01'
const recent = '2026-04-10'
const base: DailyCache = {
version: DAILY_CACHE_VERSION,
@ -269,7 +269,7 @@ describe('addNewDays', () => {
days: [emptyDay(old, 1), emptyDay(recent, 2)],
}
const updated = addNewDays(base, [], recent)
// 730-day retention from 2026-04-10 → cutoff ~2024-04-11; 2020-01-01 must be gone.
// 3650-day retention from 2026-04-10 puts the cutoff in 2016; 2010-01-01 must be gone.
expect(updated.days.find(d => d.date === old)).toBeUndefined()
expect(updated.days.find(d => d.date === recent)).toBeDefined()
})

View file

@ -101,7 +101,9 @@ describe('vercel-gateway end-to-end (parseAllSessions network path)', () => {
ok: true,
json: async () => ({
results: [
{ day: '2026-06-01', model: 'openai/gpt-4o', total_cost: 12.34, input_tokens: 1000, output_tokens: 500, request_count: 3 },
// Relative so the rolling 6-month dashboard window always contains
// it; a fixed date here starts failing once the window moves past it.
{ day: new Date(Date.now() - 15 * 24 * 60 * 60 * 1000).toISOString().slice(0, 10), model: 'openai/gpt-4o', total_cost: 12.34, input_tokens: 1000, output_tokens: 500, request_count: 3 },
],
}),
})) as typeof fetch

View file

@ -179,3 +179,18 @@ describe('buildPlanQuota', () => {
expect(quota.windows[0].pace).toBeUndefined()
})
})
describe('computePace NaN guard', () => {
it('returns undefined for a non-finite usedFraction instead of a NaN pace', () => {
const now = new Date('2026-07-15T00:00:00Z')
const win = {
kind: 'weekly' as const,
usedFraction: NaN,
resetsAt: new Date('2026-07-18T12:00:00Z'),
windowSeconds: 7 * 24 * 3600,
source: 'live' as const,
}
expect(computePace(win, now)).toBeUndefined()
expect(computePace({ ...win, usedFraction: Infinity }, now)).toBeUndefined()
})
})