From 61c0c147ad2fbc34ebe96492873efdf38da25c42 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 18 Feb 2026 22:49:15 +0000 Subject: [PATCH] refactor(update-cli): share timeout option validation --- src/cli/update-cli.test.ts | 21 +++++++++++++++++++++ src/cli/update-cli/shared.ts | 12 ++++++++++++ src/cli/update-cli/status.ts | 8 +++----- src/cli/update-cli/update-command.ts | 8 +++----- src/cli/update-cli/wizard.ts | 7 +++---- 5 files changed, 42 insertions(+), 14 deletions(-) diff --git a/src/cli/update-cli.test.ts b/src/cli/update-cli.test.ts index a028e1f983..9b755a7c6d 100644 --- a/src/cli/update-cli.test.ts +++ b/src/cli/update-cli.test.ts @@ -557,6 +557,16 @@ describe("update-cli", () => { expect(defaultRuntime.exit).toHaveBeenCalledWith(1); }); + it("updateStatusCommand validates timeout option", async () => { + vi.mocked(defaultRuntime.error).mockClear(); + vi.mocked(defaultRuntime.exit).mockClear(); + + await updateStatusCommand({ timeout: "invalid" }); + + expect(defaultRuntime.error).toHaveBeenCalledWith(expect.stringContaining("timeout")); + expect(defaultRuntime.exit).toHaveBeenCalledWith(1); + }); + it("persists update channel when --channel is set", async () => { const mockResult: UpdateRunResult = { status: "ok", @@ -611,6 +621,17 @@ describe("update-cli", () => { expect(defaultRuntime.exit).toHaveBeenCalledWith(1); }); + it("updateWizardCommand validates timeout option", async () => { + setTty(true); + vi.mocked(defaultRuntime.error).mockClear(); + vi.mocked(defaultRuntime.exit).mockClear(); + + await updateWizardCommand({ timeout: "invalid" }); + + expect(defaultRuntime.error).toHaveBeenCalledWith(expect.stringContaining("timeout")); + expect(defaultRuntime.exit).toHaveBeenCalledWith(1); + }); + it("updateWizardCommand offers dev checkout and forwards selections", async () => { const tempDir = await createCaseDir("openclaw-update-wizard"); const envSnapshot = captureEnv(["OPENCLAW_GIT_DIR"]); diff --git a/src/cli/update-cli/shared.ts b/src/cli/update-cli/shared.ts index f7956fb328..c97e021600 100644 --- a/src/cli/update-cli/shared.ts +++ b/src/cli/update-cli/shared.ts @@ -37,6 +37,18 @@ export type UpdateWizardOptions = { timeout?: string; }; +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)) { + defaultRuntime.error(INVALID_TIMEOUT_ERROR); + defaultRuntime.exit(1); + return null; + } + return timeoutMs; +} + const OPENCLAW_REPO_URL = "https://github.com/openclaw/openclaw.git"; const MAX_LOG_CHARS = 8000; diff --git a/src/cli/update-cli/status.ts b/src/cli/update-cli/status.ts index 452445dfe5..5cf2bf8af4 100644 --- a/src/cli/update-cli/status.ts +++ b/src/cli/update-cli/status.ts @@ -12,7 +12,7 @@ import { checkUpdateStatus } from "../../infra/update-check.js"; import { defaultRuntime } from "../../runtime.js"; import { renderTable } from "../../terminal/table.js"; import { theme } from "../../terminal/theme.js"; -import { resolveUpdateRoot, type UpdateStatusOptions } from "./shared.js"; +import { parseTimeoutMsOrExit, resolveUpdateRoot, type UpdateStatusOptions } from "./shared.js"; function formatGitStatusLine(params: { branch: string | null; @@ -31,10 +31,8 @@ function formatGitStatusLine(params: { } export async function updateStatusCommand(opts: UpdateStatusOptions): Promise { - const timeoutMs = opts.timeout ? Number.parseInt(opts.timeout, 10) * 1000 : undefined; - if (timeoutMs !== undefined && (Number.isNaN(timeoutMs) || timeoutMs <= 0)) { - defaultRuntime.error("--timeout must be a positive integer (seconds)"); - defaultRuntime.exit(1); + const timeoutMs = parseTimeoutMsOrExit(opts.timeout); + if (timeoutMs === null) { return; } diff --git a/src/cli/update-cli/update-command.ts b/src/cli/update-cli/update-command.ts index 87f36a9e41..872a06def1 100644 --- a/src/cli/update-cli/update-command.ts +++ b/src/cli/update-cli/update-command.ts @@ -40,6 +40,7 @@ import { DEFAULT_PACKAGE_NAME, ensureGitCheckout, normalizeTag, + parseTimeoutMsOrExit, readPackageName, readPackageVersion, resolveGitInstallDir, @@ -468,12 +469,9 @@ async function maybeRestartService(params: { export async function updateCommand(opts: UpdateCommandOptions): Promise { suppressDeprecations(); - const timeoutMs = opts.timeout ? Number.parseInt(opts.timeout, 10) * 1000 : undefined; + const timeoutMs = parseTimeoutMsOrExit(opts.timeout); const shouldRestart = opts.restart !== false; - - if (timeoutMs !== undefined && (Number.isNaN(timeoutMs) || timeoutMs <= 0)) { - defaultRuntime.error("--timeout must be a positive integer (seconds)"); - defaultRuntime.exit(1); + if (timeoutMs === null) { return; } diff --git a/src/cli/update-cli/wizard.ts b/src/cli/update-cli/wizard.ts index b5953fda66..e2b53b7afa 100644 --- a/src/cli/update-cli/wizard.ts +++ b/src/cli/update-cli/wizard.ts @@ -14,6 +14,7 @@ import { pathExists } from "../../utils.js"; import { isEmptyDir, isGitCheckout, + parseTimeoutMsOrExit, resolveGitInstallDir, resolveUpdateRoot, type UpdateWizardOptions, @@ -29,10 +30,8 @@ export async function updateWizardCommand(opts: UpdateWizardOptions = {}): Promi return; } - const timeoutMs = opts.timeout ? Number.parseInt(opts.timeout, 10) * 1000 : undefined; - if (timeoutMs !== undefined && (Number.isNaN(timeoutMs) || timeoutMs <= 0)) { - defaultRuntime.error("--timeout must be a positive integer (seconds)"); - defaultRuntime.exit(1); + const timeoutMs = parseTimeoutMsOrExit(opts.timeout); + if (timeoutMs === null) { return; }