refactor(update-cli): share timeout option validation

This commit is contained in:
Peter Steinberger
2026-02-18 22:49:15 +00:00
parent b704bad8f3
commit 61c0c147ad
5 changed files with 42 additions and 14 deletions

View File

@@ -557,6 +557,16 @@ describe("update-cli", () => {
expect(defaultRuntime.exit).toHaveBeenCalledWith(1); 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 () => { it("persists update channel when --channel is set", async () => {
const mockResult: UpdateRunResult = { const mockResult: UpdateRunResult = {
status: "ok", status: "ok",
@@ -611,6 +621,17 @@ describe("update-cli", () => {
expect(defaultRuntime.exit).toHaveBeenCalledWith(1); 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 () => { it("updateWizardCommand offers dev checkout and forwards selections", async () => {
const tempDir = await createCaseDir("openclaw-update-wizard"); const tempDir = await createCaseDir("openclaw-update-wizard");
const envSnapshot = captureEnv(["OPENCLAW_GIT_DIR"]); const envSnapshot = captureEnv(["OPENCLAW_GIT_DIR"]);

View File

@@ -37,6 +37,18 @@ export type UpdateWizardOptions = {
timeout?: string; 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 OPENCLAW_REPO_URL = "https://github.com/openclaw/openclaw.git";
const MAX_LOG_CHARS = 8000; const MAX_LOG_CHARS = 8000;

View File

@@ -12,7 +12,7 @@ import { checkUpdateStatus } from "../../infra/update-check.js";
import { defaultRuntime } from "../../runtime.js"; import { defaultRuntime } from "../../runtime.js";
import { renderTable } from "../../terminal/table.js"; import { renderTable } from "../../terminal/table.js";
import { theme } from "../../terminal/theme.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: { function formatGitStatusLine(params: {
branch: string | null; branch: string | null;
@@ -31,10 +31,8 @@ function formatGitStatusLine(params: {
} }
export async function updateStatusCommand(opts: UpdateStatusOptions): Promise<void> { export async function updateStatusCommand(opts: UpdateStatusOptions): Promise<void> {
const timeoutMs = opts.timeout ? Number.parseInt(opts.timeout, 10) * 1000 : undefined; const timeoutMs = parseTimeoutMsOrExit(opts.timeout);
if (timeoutMs !== undefined && (Number.isNaN(timeoutMs) || timeoutMs <= 0)) { if (timeoutMs === null) {
defaultRuntime.error("--timeout must be a positive integer (seconds)");
defaultRuntime.exit(1);
return; return;
} }

View File

@@ -40,6 +40,7 @@ import {
DEFAULT_PACKAGE_NAME, DEFAULT_PACKAGE_NAME,
ensureGitCheckout, ensureGitCheckout,
normalizeTag, normalizeTag,
parseTimeoutMsOrExit,
readPackageName, readPackageName,
readPackageVersion, readPackageVersion,
resolveGitInstallDir, resolveGitInstallDir,
@@ -468,12 +469,9 @@ async function maybeRestartService(params: {
export async function updateCommand(opts: UpdateCommandOptions): Promise<void> { export async function updateCommand(opts: UpdateCommandOptions): Promise<void> {
suppressDeprecations(); suppressDeprecations();
const timeoutMs = opts.timeout ? Number.parseInt(opts.timeout, 10) * 1000 : undefined; const timeoutMs = parseTimeoutMsOrExit(opts.timeout);
const shouldRestart = opts.restart !== false; const shouldRestart = opts.restart !== false;
if (timeoutMs === null) {
if (timeoutMs !== undefined && (Number.isNaN(timeoutMs) || timeoutMs <= 0)) {
defaultRuntime.error("--timeout must be a positive integer (seconds)");
defaultRuntime.exit(1);
return; return;
} }

View File

@@ -14,6 +14,7 @@ import { pathExists } from "../../utils.js";
import { import {
isEmptyDir, isEmptyDir,
isGitCheckout, isGitCheckout,
parseTimeoutMsOrExit,
resolveGitInstallDir, resolveGitInstallDir,
resolveUpdateRoot, resolveUpdateRoot,
type UpdateWizardOptions, type UpdateWizardOptions,
@@ -29,10 +30,8 @@ export async function updateWizardCommand(opts: UpdateWizardOptions = {}): Promi
return; return;
} }
const timeoutMs = opts.timeout ? Number.parseInt(opts.timeout, 10) * 1000 : undefined; const timeoutMs = parseTimeoutMsOrExit(opts.timeout);
if (timeoutMs !== undefined && (Number.isNaN(timeoutMs) || timeoutMs <= 0)) { if (timeoutMs === null) {
defaultRuntime.error("--timeout must be a positive integer (seconds)");
defaultRuntime.exit(1);
return; return;
} }