fix(migrate): guard report timestamp formatting

This commit is contained in:
Peter Steinberger
2026-05-30 08:46:51 -04:00
parent a89abcb1e9
commit d2f69ecc3b
5 changed files with 57 additions and 7 deletions

View File

@@ -0,0 +1,18 @@
import path from "node:path";
import { describe, expect, it } from "vitest";
import { buildMigrationReportDir } from "./context.js";
describe("migration context helpers", () => {
it("builds report directories with filename-safe timestamps", () => {
const now = Date.parse("2026-02-23T12:34:56.000Z");
expect(buildMigrationReportDir("codex", "/state", now)).toBe(
path.join("/state", "migration", "codex", "2026-02-23T12-34-56.000Z"),
);
});
it("falls back instead of throwing for out-of-range report timestamps", () => {
expect(buildMigrationReportDir("codex", "/state", 9_000_000_000_000_000)).toMatch(
/[/\\]migration[/\\]codex[/\\]\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}\.\d{3}Z$/,
);
});
});

View File

@@ -4,6 +4,7 @@ import { resolveStateDir } from "../../config/paths.js";
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import type { MigrationProviderContext } from "../../plugins/types.js";
import type { RuntimeEnv } from "../../runtime.js";
import { timestampMsToIsoFileStamp } from "../../shared/number-coercion.js";
export function createMigrationLogger(runtime: RuntimeEnv, opts: { json?: boolean } = {}) {
const info = opts.json ? runtime.error : runtime.log;
@@ -24,7 +25,7 @@ export function buildMigrationReportDir(
stateDir: string,
nowMs = Date.now(),
): string {
const stamp = new Date(nowMs).toISOString().replaceAll(":", "-");
const stamp = timestampMsToIsoFileStamp(nowMs);
return path.join(stateDir, "migration", providerId, stamp);
}

View File

@@ -1,4 +1,4 @@
import { timestampMsToIsoString } from "../../shared/number-coercion.js";
import { timestampMsToIsoFileStamp } from "../../shared/number-coercion.js";
import { escapeRegExp } from "../../shared/regexp.js";
export type SessionArchiveReason = "bak" | "reset" | "deleted";
@@ -123,11 +123,7 @@ export function parseUsageCountedSessionIdFromFileName(fileName: string): string
}
export function formatSessionArchiveTimestamp(nowMs = Date.now()): string {
const iso =
timestampMsToIsoString(nowMs) ??
timestampMsToIsoString(Date.now()) ??
"1970-01-01T00:00:00.000Z";
return iso.replaceAll(":", "-");
return timestampMsToIsoFileStamp(nowMs);
}
function restoreSessionArchiveTimestamp(raw: string): string {

View File

@@ -25,6 +25,8 @@ import {
parseStrictNonNegativeInteger,
parseStrictPositiveInteger,
resolveTimerTimeoutMs,
resolveTimestampMsToIsoString,
timestampMsToIsoFileStamp,
timestampMsToIsoString,
} from "./number-coercion.js";
@@ -132,6 +134,22 @@ describe("number-coercion", () => {
expect(timestampMsToIsoString("0")).toBeUndefined();
});
test("timestamp fallback helpers resolve Date-invalid timestamps", () => {
expect(resolveTimestampMsToIsoString(0)).toBe("1970-01-01T00:00:00.000Z");
expect(resolveTimestampMsToIsoString(Number.POSITIVE_INFINITY, 1_000)).toBe(
"1970-01-01T00:00:01.000Z",
);
expect(resolveTimestampMsToIsoString(Number.POSITIVE_INFINITY, Number.NaN)).toBe(
"1970-01-01T00:00:00.000Z",
);
expect(timestampMsToIsoFileStamp(Date.parse("2026-02-23T12:34:56.000Z"))).toBe(
"2026-02-23T12-34-56.000Z",
);
expect(timestampMsToIsoFileStamp(9_000_000_000_000_000, 1_000)).toBe(
"1970-01-01T00-00-01.000Z",
);
});
test("expiry helpers resolve safe absolute timestamps", () => {
expect(
resolveExpiresAtMsFromDurationSeconds("3600", {

View File

@@ -96,6 +96,7 @@ export function asPositiveSafeInteger(value: unknown): number | undefined {
export const MAX_TIMER_TIMEOUT_MS = 2_147_000_000;
export const MAX_TIMER_TIMEOUT_SECONDS = Math.floor(MAX_TIMER_TIMEOUT_MS / 1000);
export const MAX_DATE_TIMESTAMP_MS = 8_640_000_000_000_000;
export const UNIX_EPOCH_ISO_STRING = "1970-01-01T00:00:00.000Z";
export function asDateTimestampMs(value: unknown): number | undefined {
return asFiniteNumberInRange(value, {
@@ -109,6 +110,22 @@ export function timestampMsToIsoString(value: unknown): string | undefined {
return timestampMs === undefined ? undefined : new Date(timestampMs).toISOString();
}
export function resolveTimestampMsToIsoString(
value: unknown,
fallbackValue: unknown = Date.now(),
): string {
return (
timestampMsToIsoString(value) ?? timestampMsToIsoString(fallbackValue) ?? UNIX_EPOCH_ISO_STRING
);
}
export function timestampMsToIsoFileStamp(
value: unknown,
fallbackValue: unknown = Date.now(),
): string {
return resolveTimestampMsToIsoString(value, fallbackValue).replaceAll(":", "-");
}
export function clampTimerTimeoutMs(valueMs: unknown, minMs = 1): number | undefined {
const value = asFiniteNumber(valueMs);
if (value === undefined) {