fix(time): centralize date timestamp fallback · openclaw/openclaw@f823123
steipete
·
2026-05-30
·
via Recent Commits to openclaw:main
| Original file line number | Diff line number | Diff line change |
|---|
@@ -17,4 +17,15 @@ describe("resolveCronStyleNow", () => {
|
17 | 17 | expect(result.formattedTime).toBe("Saturday, May 30th, 2026 - 12:00"); |
18 | 18 | expect(result.timeLine).toContain("Reference UTC: 2026-05-30 12:00 UTC"); |
19 | 19 | }); |
| 20 | + |
| 21 | +it("falls back to epoch when both nowMs and Date.now are outside Date range", () => { |
| 22 | +vi.spyOn(Date, "now").mockReturnValue(8_640_000_000_000_001); |
| 23 | + |
| 24 | +const result = resolveCronStyleNow( |
| 25 | +{ agents: { defaults: { userTimezone: "UTC", timeFormat: "24" } } }, |
| 26 | +8_640_000_000_000_001, |
| 27 | +); |
| 28 | + |
| 29 | +expect(result.timeLine).toContain("Reference UTC: 1970-01-01 00:00 UTC"); |
| 30 | +}); |
20 | 31 | }); |
| Original file line number | Diff line number | Diff line change |
|---|
|
1 | | -import { asDateTimestampMs } from "../shared/number-coercion.js"; |
| 1 | +import { resolveDateTimestampMs } from "../shared/number-coercion.js"; |
2 | 2 | import { |
3 | 3 | type TimeFormatPreference, |
4 | 4 | formatUserTime, |
@@ -24,7 +24,7 @@ type TimeConfigLike = {
|
24 | 24 | export function resolveCronStyleNow(cfg: TimeConfigLike, nowMs: number): CronStyleNow { |
25 | 25 | const userTimezone = resolveUserTimezone(cfg.agents?.defaults?.userTimezone); |
26 | 26 | const userTimeFormat = resolveUserTimeFormat(cfg.agents?.defaults?.timeFormat); |
27 | | -const timestampMs = asDateTimestampMs(nowMs) ?? Date.now(); |
| 27 | +const timestampMs = resolveDateTimestampMs(nowMs); |
28 | 28 | const date = new Date(timestampMs); |
29 | 29 | const formattedTime = formatUserTime(date, userTimezone, userTimeFormat) ?? date.toISOString(); |
30 | 30 | const utcTime = date.toISOString().replace("T", " ").slice(0, 16) + " UTC"; |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -29,4 +29,10 @@ describe("formatDateStamp", () => {
|
29 | 29 | |
30 | 30 | expect(formatDateStamp(8_640_000_000_000_001, "UTC")).toBe("2026-05-30"); |
31 | 31 | }); |
| 32 | + |
| 33 | +it("falls back to epoch when both nowMs and Date.now are outside Date range", () => { |
| 34 | +vi.spyOn(Date, "now").mockReturnValue(8_640_000_000_000_001); |
| 35 | + |
| 36 | +expect(formatDateStamp(8_640_000_000_000_001, "UTC")).toBe("1970-01-01"); |
| 37 | +}); |
32 | 38 | }); |
| Original file line number | Diff line number | Diff line change |
|---|
|
1 | 1 | import { execFileSync } from "node:child_process"; |
2 | | -import { asDateTimestampMs } from "../shared/number-coercion.js"; |
| 2 | +import { resolveDateTimestampMs } from "../shared/number-coercion.js"; |
3 | 3 | |
4 | 4 | export type TimeFormatPreference = "auto" | "12" | "24"; |
5 | 5 | export type ResolvedTimeFormat = "12" | "24"; |
@@ -42,7 +42,7 @@ export function resolveUserTimeFormat(preference?: TimeFormatPreference): Resolv
|
42 | 42 | } |
43 | 43 | |
44 | 44 | export function formatDateStamp(nowMs: number, timeZone: string): string { |
45 | | -const timestampMs = asDateTimestampMs(nowMs) ?? Date.now(); |
| 45 | +const timestampMs = resolveDateTimestampMs(nowMs); |
46 | 46 | const date = new Date(timestampMs); |
47 | 47 | const parts = new Intl.DateTimeFormat("en-US", { |
48 | 48 | timeZone, |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -336,6 +336,36 @@ describe("createBackupArchive", () => {
|
336 | 336 | ); |
337 | 337 | }); |
338 | 338 | |
| 339 | +it("falls back to epoch when injected nowMs and Date.now are outside Date range", async () => { |
| 340 | +await withOpenClawTestState( |
| 341 | +{ |
| 342 | +layout: "state-only", |
| 343 | +prefix: "openclaw-backup-invalid-fallback-now-", |
| 344 | +scenario: "minimal", |
| 345 | +}, |
| 346 | +async (state) => { |
| 347 | +const outputDir = state.path("backups"); |
| 348 | +await fs.mkdir(outputDir, { recursive: true }); |
| 349 | +const dateNowSpy = vi.spyOn(Date, "now").mockReturnValue(8_640_000_000_000_001); |
| 350 | + |
| 351 | +try { |
| 352 | +const result = await createBackupArchive({ |
| 353 | +output: outputDir, |
| 354 | +dryRun: true, |
| 355 | +includeWorkspace: false, |
| 356 | +nowMs: 8_640_000_000_000_001, |
| 357 | +}); |
| 358 | + |
| 359 | +expect(result.createdAt).toBe("1970-01-01T00:00:00.000Z"); |
| 360 | +expect(path.basename(result.archivePath)).toContain("openclaw-backup.tar.gz"); |
| 361 | +expect(path.basename(result.archivePath)).not.toContain("NaN"); |
| 362 | +} finally { |
| 363 | +dateNowSpy.mockRestore(); |
| 364 | +} |
| 365 | +}, |
| 366 | +); |
| 367 | +}); |
| 368 | + |
339 | 369 | it("skips current live volatile state files while preserving workspace locks", async () => { |
340 | 370 | await withOpenClawTestState( |
341 | 371 | { |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -11,7 +11,7 @@ import {
|
11 | 11 | resolveBackupPlanFromDisk, |
12 | 12 | } from "../commands/backup-shared.js"; |
13 | 13 | import { isPathWithin } from "../commands/cleanup-utils.js"; |
14 | | -import { asDateTimestampMs } from "../shared/number-coercion.js"; |
| 14 | +import { resolveDateTimestampMs } from "../shared/number-coercion.js"; |
15 | 15 | import { resolveHomeDir, resolveUserPath } from "../utils.js"; |
16 | 16 | import { resolveRuntimeServiceVersion } from "../version.js"; |
17 | 17 | import { isVolatileBackupPath } from "./backup-volatile-filter.js"; |
@@ -447,7 +447,7 @@ export function buildExtensionsNodeModulesFilter(stateDir: string): (filePath: s
|
447 | 447 | export async function createBackupArchive( |
448 | 448 | opts: BackupCreateOptions = {}, |
449 | 449 | ): Promise<BackupCreateResult> { |
450 | | -const nowMs = asDateTimestampMs(opts.nowMs) ?? Date.now(); |
| 450 | +const nowMs = resolveDateTimestampMs(opts.nowMs); |
451 | 451 | const archiveRoot = buildBackupArchiveRoot(nowMs); |
452 | 452 | const onlyConfig = Boolean(opts.onlyConfig); |
453 | 453 | const includeWorkspace = onlyConfig ? false : (opts.includeWorkspace ?? true); |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -20,6 +20,7 @@ import {
|
20 | 20 | resolveNonNegativeIntegerOption, |
21 | 21 | resolveOptionalIntegerOption, |
22 | 22 | resolvePositiveTimerTimeoutMs, |
| 23 | +resolveDateTimestampMs, |
23 | 24 | parseStrictFiniteNumber, |
24 | 25 | parseStrictInteger, |
25 | 26 | parseStrictNonNegativeInteger, |
@@ -135,6 +136,9 @@ describe("number-coercion", () => {
|
135 | 136 | }); |
136 | 137 | |
137 | 138 | test("timestamp fallback helpers resolve Date-invalid timestamps", () => { |
| 139 | +expect(resolveDateTimestampMs(1_000)).toBe(1_000); |
| 140 | +expect(resolveDateTimestampMs(Number.POSITIVE_INFINITY, 1_000)).toBe(1_000); |
| 141 | +expect(resolveDateTimestampMs(Number.POSITIVE_INFINITY, Number.NaN)).toBe(0); |
138 | 142 | expect(resolveTimestampMsToIsoString(0)).toBe("1970-01-01T00:00:00.000Z"); |
139 | 143 | expect(resolveTimestampMsToIsoString(Number.POSITIVE_INFINITY, 1_000)).toBe( |
140 | 144 | "1970-01-01T00:00:01.000Z", |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -110,6 +110,13 @@ export function timestampMsToIsoString(value: unknown): string | undefined {
|
110 | 110 | return timestampMs === undefined ? undefined : new Date(timestampMs).toISOString(); |
111 | 111 | } |
112 | 112 | |
| 113 | +export function resolveDateTimestampMs( |
| 114 | +value: unknown, |
| 115 | +fallbackValue: unknown = Date.now(), |
| 116 | +): number { |
| 117 | +return asDateTimestampMs(value) ?? asDateTimestampMs(fallbackValue) ?? 0; |
| 118 | +} |
| 119 | + |
113 | 120 | export function resolveTimestampMsToIsoString( |
114 | 121 | value: unknown, |
115 | 122 | fallbackValue: unknown = Date.now(), |
|
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。