From 8855a4aa589cf97f96708261c4d3e7bbf329ca2b Mon Sep 17 00:00:00 2001 From: Gio Della-Libera Date: Sun, 17 May 2026 18:47:59 -0700 Subject: [PATCH] fix(update): require integer timeout values (#83310) * fix(update): require integer timeout values * fix(update): reject blank timeout values --- .../update-cli/shared.command-runner.test.ts | 40 ++++++++++++++++++- src/cli/update-cli/shared.ts | 10 +++-- 2 files changed, 46 insertions(+), 4 deletions(-) diff --git a/src/cli/update-cli/shared.command-runner.test.ts b/src/cli/update-cli/shared.command-runner.test.ts index cc34766a7ef..7e1f3ab7f37 100644 --- a/src/cli/update-cli/shared.command-runner.test.ts +++ b/src/cli/update-cli/shared.command-runner.test.ts @@ -1,5 +1,6 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; -import { createGlobalCommandRunner } from "./shared.js"; +import { defaultRuntime } from "../../runtime.js"; +import { createGlobalCommandRunner, parseTimeoutMsOrExit } from "./shared.js"; const runCommandWithTimeout = vi.hoisted(() => vi.fn()); @@ -48,4 +49,41 @@ describe("createGlobalCommandRunner", () => { code: 17, }); }); + + it("requires timeout values to be complete positive integer seconds", () => { + const error = vi.spyOn(defaultRuntime, "error").mockImplementation(() => undefined); + const exit = vi.spyOn(defaultRuntime, "exit").mockImplementation(() => undefined as never); + + try { + expect(parseTimeoutMsOrExit("1.5")).toBeNull(); + expect(parseTimeoutMsOrExit("10abc")).toBeNull(); + expect(parseTimeoutMsOrExit("0")).toBeNull(); + expect(parseTimeoutMsOrExit("-1")).toBeNull(); + expect(parseTimeoutMsOrExit(" ")).toBeNull(); + + expect(error).toHaveBeenCalledTimes(5); + expect(error).toHaveBeenCalledWith("--timeout must be a positive integer (seconds)"); + expect(exit).toHaveBeenCalledTimes(5); + expect(exit).toHaveBeenCalledWith(1); + } finally { + error.mockRestore(); + exit.mockRestore(); + } + }); + + it("parses complete positive integer timeout values as milliseconds", () => { + const error = vi.spyOn(defaultRuntime, "error").mockImplementation(() => undefined); + const exit = vi.spyOn(defaultRuntime, "exit").mockImplementation(() => undefined as never); + + try { + expect(parseTimeoutMsOrExit(" 10 ")).toBe(10_000); + expect(parseTimeoutMsOrExit("001")).toBe(1_000); + expect(parseTimeoutMsOrExit()).toBeUndefined(); + expect(error).not.toHaveBeenCalled(); + expect(exit).not.toHaveBeenCalled(); + } finally { + error.mockRestore(); + exit.mockRestore(); + } + }); }); diff --git a/src/cli/update-cli/shared.ts b/src/cli/update-cli/shared.ts index a71296c369a..0b1d153da1f 100644 --- a/src/cli/update-cli/shared.ts +++ b/src/cli/update-cli/shared.ts @@ -54,13 +54,17 @@ export type UpdateWizardOptions = { const INVALID_TIMEOUT_ERROR = "--timeout must be a positive integer (seconds)"; export function parseTimeoutMsOrExit(timeout?: string): number | undefined | null { - const timeoutMs = timeout ? Number.parseInt(timeout, 10) * 1000 : undefined; - if (timeoutMs !== undefined && (Number.isNaN(timeoutMs) || timeoutMs <= 0)) { + if (timeout === undefined) { + return undefined; + } + const trimmed = timeout.trim(); + const seconds = Number(trimmed); + if (!/^\d+$/u.test(trimmed) || !Number.isSafeInteger(seconds) || seconds <= 0) { defaultRuntime.error(INVALID_TIMEOUT_ERROR); defaultRuntime.exit(1); return null; } - return timeoutMs; + return seconds * 1000; } const OPENCLAW_REPO_URL = "https://github.com/openclaw/openclaw.git";