mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-07-29 02:45:13 +00:00
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:
parent
db8ef9b466
commit
b30818138e
6 changed files with 34 additions and 9 deletions
|
|
@ -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')}`
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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) }),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
})
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue