From 11e5ab93a9d303159d445c939b410f57a6e4c738 Mon Sep 17 00:00:00 2001 From: Isaac Sanchez-Hawkins <266845420+isanchez404@users.noreply.github.com> Date: Wed, 13 May 2026 07:51:46 -0400 Subject: [PATCH] fix(quota): guard remaining usage percent (#1242) * fix(quota): guard remaining usage percent * test(quota): cover non-finite usage percents * fix(ui): clean up virtual scroll frame --------- Co-authored-by: Isaac Sanchez Co-authored-by: Bohdan Triapitsyn --- .../ui/src/components/chat/MessageList.tsx | 8 ++++++-- .../web/server/lib/quota/utils/formatters.js | 3 ++- .../server/lib/quota/utils/formatters.test.js | 19 +++++++++++++++++++ 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/packages/ui/src/components/chat/MessageList.tsx b/packages/ui/src/components/chat/MessageList.tsx index ebf73005..56bf01de 100644 --- a/packages/ui/src/components/chat/MessageList.tsx +++ b/packages/ui/src/components/chat/MessageList.tsx @@ -1355,8 +1355,9 @@ const MessageList = React.forwardRef(({ // measure() defers via useAnimationFrameWithResizeObserver. // Wait two frames then, if we were near the estimated bottom, scroll // to the real bottom after measurements settle. + let frame2: number | null = null; const frame1 = requestAnimationFrame(() => { - const frame2 = requestAnimationFrame(() => { + frame2 = requestAnimationFrame(() => { if (!nearBottom) return const el = resolveScrollContainer() if (!el) return @@ -1368,8 +1369,11 @@ const MessageList = React.forwardRef(({ }) return () => { cancelAnimationFrame(frame1) + if (frame2 !== null) { + cancelAnimationFrame(frame2) + } } - }, [historyVirtualizer, shouldVirtualizeHistory]); + }, [historyVirtualizer, resolveScrollContainer, shouldVirtualizeHistory]); const scheduleVirtualMeasure = React.useCallback(() => { if (!shouldVirtualizeHistory) { diff --git a/packages/web/server/lib/quota/utils/formatters.js b/packages/web/server/lib/quota/utils/formatters.js index 7cf85c2a..5bdf8632 100644 --- a/packages/web/server/lib/quota/utils/formatters.js +++ b/packages/web/server/lib/quota/utils/formatters.js @@ -40,9 +40,10 @@ export const calculateResetAfterSeconds = (resetAt) => { export const toUsageWindow = ({ usedPercent, windowSeconds, resetAt, valueLabel }) => { const resetAfterSeconds = calculateResetAfterSeconds(resetAt); const resetFormatted = hasResetTimestamp(resetAt) ? formatResetTime(resetAt) : null; + const hasFiniteUsedPercent = typeof usedPercent === 'number' && Number.isFinite(usedPercent); return { usedPercent, - remainingPercent: usedPercent !== null ? Math.max(0, 100 - usedPercent) : null, + remainingPercent: hasFiniteUsedPercent ? Math.max(0, 100 - usedPercent) : null, windowSeconds: windowSeconds ?? null, resetAfterSeconds, resetAt, diff --git a/packages/web/server/lib/quota/utils/formatters.test.js b/packages/web/server/lib/quota/utils/formatters.test.js index 38a7700e..28c1324e 100644 --- a/packages/web/server/lib/quota/utils/formatters.test.js +++ b/packages/web/server/lib/quota/utils/formatters.test.js @@ -32,4 +32,23 @@ describe('toUsageWindow', () => { expect(usageWindow.resetAtFormatted).toBe(formatResetTime(0)); expect(usageWindow.resetAfterFormatted).toBe(formatResetTime(0)); }); + + it('does not derive remaining percent from missing usage', () => { + expect(toUsageWindow({ usedPercent: undefined }).remainingPercent).toBeNull(); + }); + + it('does not derive remaining percent from non-finite usage', () => { + expect(toUsageWindow({ usedPercent: NaN }).remainingPercent).toBeNull(); + expect(toUsageWindow({ usedPercent: Infinity }).remainingPercent).toBeNull(); + expect(toUsageWindow({ usedPercent: -Infinity }).remainingPercent).toBeNull(); + expect(toUsageWindow({ usedPercent: null }).remainingPercent).toBeNull(); + }); + + it('derives remaining percent from a valid usage value', () => { + expect(toUsageWindow({ usedPercent: 60 }).remainingPercent).toBe(40); + }); + + it('clamps remaining percent to zero when usage exceeds 100', () => { + expect(toUsageWindow({ usedPercent: 110 }).remainingPercent).toBe(0); + }); });