fix(ui): ignore invalid reset timestamps · openclaw/openclaw@7c5b55c
steipete
·
2026-05-30
·
via Recent Commits to openclaw:main
| Original file line number | Diff line number | Diff line change |
|---|
|
1 | 1 | import { describe, expect, test } from "vitest"; |
2 | 2 | import { |
| 3 | +asDateTimestampMs, |
3 | 4 | asFiniteNumber, |
4 | 5 | asFiniteNumberInRange, |
5 | 6 | asSafeIntegerInRange, |
@@ -119,6 +120,11 @@ describe("number-coercion", () => {
|
119 | 120 | }); |
120 | 121 | |
121 | 122 | test("timestamp ISO helper rejects Date-invalid timestamps", () => { |
| 123 | +expect(asDateTimestampMs(0)).toBe(0); |
| 124 | +expect(asDateTimestampMs(8_640_000_000_000_000)).toBe(8_640_000_000_000_000); |
| 125 | +expect(asDateTimestampMs(8_640_000_000_000_001)).toBeUndefined(); |
| 126 | +expect(asDateTimestampMs(Number.POSITIVE_INFINITY)).toBeUndefined(); |
| 127 | +expect(asDateTimestampMs("0")).toBeUndefined(); |
122 | 128 | expect(timestampMsToIsoString(0)).toBe("1970-01-01T00:00:00.000Z"); |
123 | 129 | expect(timestampMsToIsoString(8_640_000_000_000_000)).toBe("+275760-09-13T00:00:00.000Z"); |
124 | 130 | expect(timestampMsToIsoString(8_640_000_000_000_001)).toBeUndefined(); |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -97,11 +97,15 @@ export const MAX_TIMER_TIMEOUT_MS = 2_147_000_000;
|
97 | 97 | export const MAX_TIMER_TIMEOUT_SECONDS = Math.floor(MAX_TIMER_TIMEOUT_MS / 1000); |
98 | 98 | export const MAX_DATE_TIMESTAMP_MS = 8_640_000_000_000_000; |
99 | 99 | |
100 | | -export function timestampMsToIsoString(value: unknown): string | undefined { |
101 | | -const timestampMs = asFiniteNumberInRange(value, { |
| 100 | +export function asDateTimestampMs(value: unknown): number | undefined { |
| 101 | +return asFiniteNumberInRange(value, { |
102 | 102 | min: -MAX_DATE_TIMESTAMP_MS, |
103 | 103 | max: MAX_DATE_TIMESTAMP_MS, |
104 | 104 | }); |
| 105 | +} |
| 106 | + |
| 107 | +export function timestampMsToIsoString(value: unknown): string | undefined { |
| 108 | +const timestampMs = asDateTimestampMs(value); |
105 | 109 | return timestampMs === undefined ? undefined : new Date(timestampMs).toISOString(); |
106 | 110 | } |
107 | 111 | |
|
| Original file line number | Diff line number | Diff line change |
|---|
|
1 | 1 | import { describe, expect, it } from "vitest"; |
2 | 2 | import { |
| 3 | +formatMs, |
3 | 4 | formatRelativeTimestamp, |
4 | 5 | formatUnknownText, |
5 | 6 | parseSessionKeyParts, |
@@ -37,6 +38,17 @@ describe("formatAgo", () => {
|
37 | 38 | }); |
38 | 39 | }); |
39 | 40 | |
| 41 | +describe("formatMs", () => { |
| 42 | +it("formats epoch timestamps", () => { |
| 43 | +expect(formatMs(0)).not.toBe("n/a"); |
| 44 | +}); |
| 45 | + |
| 46 | +it("returns n/a for Date-invalid timestamps", () => { |
| 47 | +expect(formatMs(8_640_000_000_000_001)).toBe("n/a"); |
| 48 | +expect(formatMs(Number.POSITIVE_INFINITY)).toBe("n/a"); |
| 49 | +}); |
| 50 | +}); |
| 51 | + |
40 | 52 | describe("stripThinkingTags", () => { |
41 | 53 | it("strips <think>…</think> segments", () => { |
42 | 54 | const input = ["<think>", "secret", "</think>", "", "Hello"].join("\n"); |
|
| Original file line number | Diff line number | Diff line change |
|---|
|
1 | 1 | import { formatDurationHuman } from "../../../src/infra/format-time/format-duration.ts"; |
2 | 2 | import { formatRelativeTimestamp } from "../../../src/infra/format-time/format-relative.ts"; |
| 3 | +import { asDateTimestampMs } from "../../../src/shared/number-coercion.js"; |
3 | 4 | import { t } from "../i18n/index.ts"; |
4 | 5 | |
5 | 6 | export { formatRelativeTimestamp, formatDurationHuman }; |
@@ -37,10 +38,11 @@ export function formatUnknownText(
|
37 | 38 | } |
38 | 39 | |
39 | 40 | export function formatMs(ms?: number | null): string { |
40 | | -if (!ms && ms !== 0) { |
| 41 | +const timestampMs = asDateTimestampMs(ms); |
| 42 | +if (timestampMs === undefined) { |
41 | 43 | return t("common.na"); |
42 | 44 | } |
43 | | -return new Date(ms).toLocaleString(); |
| 45 | +return new Date(timestampMs).toLocaleString(); |
44 | 46 | } |
45 | 47 | |
46 | 48 | export function formatList(values?: Array<string | null | undefined>): string { |
|
| Original file line number | Diff line number | Diff line change |
|---|
|
| 1 | +import { afterEach, describe, expect, it, vi } from "vitest"; |
| 2 | +import { formatQuotaReset } from "./provider-quota-summary.ts"; |
| 3 | + |
| 4 | +describe("formatQuotaReset", () => { |
| 5 | +afterEach(() => { |
| 6 | +vi.restoreAllMocks(); |
| 7 | +}); |
| 8 | + |
| 9 | +it("returns compact relative reset windows", () => { |
| 10 | +vi.spyOn(Date, "now").mockReturnValue(Date.parse("2026-05-30T12:00:00.000Z")); |
| 11 | + |
| 12 | +expect(formatQuotaReset(Date.now() + 30 * 60_000)).toBe("30m"); |
| 13 | +expect(formatQuotaReset(Date.now() + 2 * 60 * 60_000 + 15 * 60_000)).toBe("2h 15m"); |
| 14 | +}); |
| 15 | + |
| 16 | +it("ignores Date-invalid reset timestamps", () => { |
| 17 | +expect(formatQuotaReset(8_640_000_000_000_001)).toBeNull(); |
| 18 | +expect(formatQuotaReset(Number.POSITIVE_INFINITY)).toBeNull(); |
| 19 | +}); |
| 20 | +}); |
| Original file line number | Diff line number | Diff line change |
|---|
|
| 1 | +import { asDateTimestampMs } from "../../../src/shared/number-coercion.js"; |
1 | 2 | import type { ModelAuthStatusProvider, ModelAuthStatusResult } from "./types.ts"; |
2 | 3 | |
3 | 4 | export type QuotaWindowSummary = { |
@@ -8,10 +9,11 @@ export type QuotaWindowSummary = {
|
8 | 9 | }; |
9 | 10 | |
10 | 11 | export function formatQuotaReset(resetAt?: number): string | null { |
11 | | -if (!resetAt || !Number.isFinite(resetAt)) { |
| 12 | +const timestampMs = asDateTimestampMs(resetAt); |
| 13 | +if (timestampMs === undefined) { |
12 | 14 | return null; |
13 | 15 | } |
14 | | -const diffMs = resetAt - Date.now(); |
| 16 | +const diffMs = timestampMs - Date.now(); |
15 | 17 | if (diffMs <= 0) { |
16 | 18 | return "now"; |
17 | 19 | } |
@@ -29,7 +31,7 @@ export function formatQuotaReset(resetAt?: number): string | null {
|
29 | 31 | const remainingHours = hours % 24; |
30 | 32 | return remainingHours > 0 ? `${days}d ${remainingHours}h` : `${days}d`; |
31 | 33 | } |
32 | | -return new Date(resetAt).toLocaleDateString(undefined, { month: "short", day: "numeric" }); |
| 34 | +return new Date(timestampMs).toLocaleDateString(undefined, { month: "short", day: "numeric" }); |
33 | 35 | } |
34 | 36 | |
35 | 37 | export function collectQuotaWindows( |
|
| Original file line number | Diff line number | Diff line change |
|---|
|
1 | 1 | import { html, nothing, type TemplateResult } from "lit"; |
2 | 2 | import { unsafeHTML } from "lit/directives/unsafe-html.js"; |
| 3 | +import { asDateTimestampMs } from "../../../../src/shared/number-coercion.js"; |
3 | 4 | import { t } from "../../i18n/index.ts"; |
4 | 5 | import { resolveCronJobLastRunStatus } from "../cron-status.ts"; |
5 | 6 | import { formatCost, formatTokens, formatRelativeTimestamp } from "../format.ts"; |
@@ -224,14 +225,12 @@ export function renderOverviewCards(props: OverviewCardsProps) {
|
224 | 225 | // Hidden for windows with plenty of headroom to keep the hint readable; |
225 | 226 | // shown when a window is below 25% to signal urgency. |
226 | 227 | const formatReset = (resetAt: number | undefined, pctLeft: number): string | null => { |
227 | | -if (!resetAt || !Number.isFinite(resetAt) || pctLeft >= 25) { |
| 228 | +const timestampMs = asDateTimestampMs(resetAt); |
| 229 | +if (timestampMs === undefined || pctLeft >= 25) { |
228 | 230 | return null; |
229 | 231 | } |
230 | | -const d = new Date(resetAt); |
231 | | -if (Number.isNaN(d.getTime())) { |
232 | | -return null; |
233 | | -} |
234 | | -const withinADay = resetAt - Date.now() < 24 * 60 * 60 * 1000; |
| 232 | +const d = new Date(timestampMs); |
| 233 | +const withinADay = timestampMs - Date.now() < 24 * 60 * 60 * 1000; |
235 | 234 | return withinADay |
236 | 235 | ? d.toLocaleTimeString(undefined, { hour: "numeric", minute: "2-digit" }) |
237 | 236 | : d.toLocaleDateString(undefined, { month: "short", day: "numeric" }); |
|
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。