diff --git a/gui/src/components/QuotaBars.tsx b/gui/src/components/QuotaBars.tsx index da782d0d02..5101a8eae5 100644 --- a/gui/src/components/QuotaBars.tsx +++ b/gui/src/components/QuotaBars.tsx @@ -341,9 +341,8 @@ function StackedQuotaRow({ row, threshold, t, locale, incomplete }: { } function formatResetAt(resetAt: number | undefined, t: TFn, locale: Locale): { day: string; time: string } { - if (typeof resetAt !== "number" || !Number.isFinite(resetAt)) return { day: "", time: "" }; - const ms = resetAt < 10_000_000_000 ? resetAt * 1000 : resetAt; - const date = new Date(ms); + const date = resetAtDate(resetAt); + if (!date) return { day: "", time: "" }; const now = new Date(); const tag = bcp47(locale); const time = new Intl.DateTimeFormat(tag, { hour: "2-digit", minute: "2-digit", hour12: false }).format(date); @@ -362,9 +361,9 @@ export function formatResetFuture( locale: Locale = "en", now = Date.now(), ): string { - if (typeof resetAt !== "number" || !Number.isFinite(resetAt)) return ""; - const ms = resetAt < 10_000_000_000 ? resetAt * 1000 : resetAt; - const date = new Date(ms); + const date = resetAtDate(resetAt); + if (!date) return ""; + const ms = date.getTime(); const tag = bcp47(locale); const time = new Intl.DateTimeFormat(tag, { hour: "2-digit", minute: "2-digit", hour12: false }).format(date); const nowDate = new Date(now); @@ -395,3 +394,10 @@ export function formatResetFuture( return t("quota.resetsAt", { date: dateStr, time, when: `${dateStr}, ${time}` }); } + +function resetAtDate(resetAt: number | undefined): Date | null { + if (typeof resetAt !== "number" || !Number.isFinite(resetAt)) return null; + const milliseconds = resetAt < 10_000_000_000 ? resetAt * 1000 : resetAt; + const date = new Date(milliseconds); + return Number.isFinite(date.getTime()) ? date : null; +} diff --git a/gui/tests/intl-formatters.test.ts b/gui/tests/intl-formatters.test.ts index fa38faace3..186abe2690 100644 --- a/gui/tests/intl-formatters.test.ts +++ b/gui/tests/intl-formatters.test.ts @@ -1,5 +1,7 @@ import { describe, expect, test } from "bun:test"; import { formatCreditDate, formatCreditDateTime } from "../src/intl-formatters"; +import { formatResetFuture } from "../src/components/QuotaBars"; +import type { TFn } from "../src/i18n"; describe("credit date formatting", () => { test("keeps the compact date format for grant dates", () => { @@ -22,3 +24,11 @@ describe("credit date formatting", () => { expect(formatCreditDateTime("invalid")).toBe("—"); }); }); + +describe("quota reset formatting", () => { + test("ignores timestamps outside the JavaScript Date range", () => { + const t = ((key: string) => key) as TFn; + + expect(formatResetFuture(1e20, t)).toBe(""); + }); +}); diff --git a/src/codex/quota.ts b/src/codex/quota.ts index 562e7082e3..e66ee3db01 100644 --- a/src/codex/quota.ts +++ b/src/codex/quota.ts @@ -152,6 +152,8 @@ function normalizeResetAt(value: unknown): number | undefined { ? Number(value) : undefined; if (typeof numeric !== "number" || !Number.isFinite(numeric) || numeric < 0) return undefined; + const milliseconds = numeric < 10_000_000_000 ? numeric * 1000 : numeric; + if (!Number.isFinite(new Date(milliseconds).getTime())) return undefined; return numeric; } diff --git a/tests/rate-limit-reset-credits.test.ts b/tests/rate-limit-reset-credits.test.ts index 122d78c01a..0030738902 100644 --- a/tests/rate-limit-reset-credits.test.ts +++ b/tests/rate-limit-reset-credits.test.ts @@ -34,6 +34,17 @@ describe("rate-limit reset credits", () => { expect(quota!.resetCredits).toBeUndefined(); }); + it("discards reset timestamps outside the JavaScript Date range", () => { + const quota = parseUsageQuota({ + rate_limit: { + primary_window: { used_percent: 10, reset_at: 1e20 }, + tertiary_window: { used_percent: 20, reset_at: 1e20 }, + }, + }); + + expect(quota).toEqual({ weeklyPercent: 10, monthlyPercent: 20 }); + }); + it("handles credits-only response (no rate_limit)", () => { const data: WhamUsageResponse = { rate_limit_reset_credits: { available_count: 1 },