From 4e055d8df2fbce7130650f21f0cc64adedb7d3d5 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 14 Mar 2026 00:56:40 +0000 Subject: [PATCH] refactor: share gateway timeout parsing --- src/cli/gateway-cli/discover.ts | 22 ++----------- src/cli/parse-timeout.test.ts | 43 ++++++++++++++++++++++++++ src/cli/parse-timeout.ts | 36 +++++++++++++++++++++ src/commands/gateway-status/helpers.ts | 16 ++-------- 4 files changed, 83 insertions(+), 34 deletions(-) create mode 100644 src/cli/parse-timeout.test.ts diff --git a/src/cli/gateway-cli/discover.ts b/src/cli/gateway-cli/discover.ts index 8465cf449ca..51eac4feb76 100644 --- a/src/cli/gateway-cli/discover.ts +++ b/src/cli/gateway-cli/discover.ts @@ -1,5 +1,6 @@ import type { GatewayBonjourBeacon } from "../../infra/bonjour-discovery.js"; import { colorize, theme } from "../../terminal/theme.js"; +import { parseTimeoutMsWithFallback } from "../parse-timeout.js"; export type GatewayDiscoverOpts = { timeout?: string; @@ -7,26 +8,7 @@ export type GatewayDiscoverOpts = { }; export function parseDiscoverTimeoutMs(raw: unknown, fallbackMs: number): number { - if (raw === undefined || raw === null) { - return fallbackMs; - } - const value = - typeof raw === "string" - ? raw.trim() - : typeof raw === "number" || typeof raw === "bigint" - ? String(raw) - : null; - if (value === null) { - throw new Error("invalid --timeout"); - } - if (!value) { - return fallbackMs; - } - const parsed = Number.parseInt(value, 10); - if (!Number.isFinite(parsed) || parsed <= 0) { - throw new Error(`invalid --timeout: ${value}`); - } - return parsed; + return parseTimeoutMsWithFallback(raw, fallbackMs, { invalidType: "error" }); } export function pickBeaconHost(beacon: GatewayBonjourBeacon): string | null { diff --git a/src/cli/parse-timeout.test.ts b/src/cli/parse-timeout.test.ts new file mode 100644 index 00000000000..9d05cf2d244 --- /dev/null +++ b/src/cli/parse-timeout.test.ts @@ -0,0 +1,43 @@ +import { describe, expect, it } from "vitest"; +import { parseTimeoutMs, parseTimeoutMsWithFallback } from "./parse-timeout.js"; + +describe("parseTimeoutMs", () => { + it("parses positive string values", () => { + expect(parseTimeoutMs("1500")).toBe(1500); + }); + + it("returns undefined for empty or invalid values", () => { + expect(parseTimeoutMs(undefined)).toBeUndefined(); + expect(parseTimeoutMs("")).toBeUndefined(); + expect(parseTimeoutMs("nope")).toBeUndefined(); + }); +}); + +describe("parseTimeoutMsWithFallback", () => { + it("returns the fallback for missing or empty values", () => { + expect(parseTimeoutMsWithFallback(undefined, 3000)).toBe(3000); + expect(parseTimeoutMsWithFallback(null, 3000)).toBe(3000); + expect(parseTimeoutMsWithFallback(" ", 3000)).toBe(3000); + }); + + it("parses positive numbers and strings", () => { + expect(parseTimeoutMsWithFallback(2500, 3000)).toBe(2500); + expect(parseTimeoutMsWithFallback(2500n, 3000)).toBe(2500); + expect(parseTimeoutMsWithFallback("2500", 3000)).toBe(2500); + }); + + it("falls back on unsupported types by default", () => { + expect(parseTimeoutMsWithFallback({}, 3000)).toBe(3000); + }); + + it("throws on unsupported types when requested", () => { + expect(() => parseTimeoutMsWithFallback({}, 3000, { invalidType: "error" })).toThrow( + "invalid --timeout", + ); + }); + + it("throws on non-positive parsed values", () => { + expect(() => parseTimeoutMsWithFallback("0", 3000)).toThrow("invalid --timeout: 0"); + expect(() => parseTimeoutMsWithFallback("-1", 3000)).toThrow("invalid --timeout: -1"); + }); +}); diff --git a/src/cli/parse-timeout.ts b/src/cli/parse-timeout.ts index 090559add6e..139393c0176 100644 --- a/src/cli/parse-timeout.ts +++ b/src/cli/parse-timeout.ts @@ -16,3 +16,39 @@ export function parseTimeoutMs(raw: unknown): number | undefined { } return Number.isFinite(value) ? value : undefined; } + +export function parseTimeoutMsWithFallback( + raw: unknown, + fallbackMs: number, + options: { + invalidType?: "fallback" | "error"; + } = {}, +): number { + if (raw === undefined || raw === null) { + return fallbackMs; + } + + const value = + typeof raw === "string" + ? raw.trim() + : typeof raw === "number" || typeof raw === "bigint" + ? String(raw) + : null; + + if (value === null) { + if (options.invalidType === "error") { + throw new Error("invalid --timeout"); + } + return fallbackMs; + } + + if (!value) { + return fallbackMs; + } + + const parsed = Number.parseInt(value, 10); + if (!Number.isFinite(parsed) || parsed <= 0) { + throw new Error(`invalid --timeout: ${value}`); + } + return parsed; +} diff --git a/src/commands/gateway-status/helpers.ts b/src/commands/gateway-status/helpers.ts index 24519e6e8be..7697d6af143 100644 --- a/src/commands/gateway-status/helpers.ts +++ b/src/commands/gateway-status/helpers.ts @@ -1,3 +1,4 @@ +import { parseTimeoutMsWithFallback } from "../../cli/parse-timeout.js"; import { resolveGatewayPort } from "../../config/config.js"; import type { OpenClawConfig, ConfigFileSnapshot } from "../../config/types.js"; import { hasConfiguredSecretInput } from "../../config/types.secrets.js"; @@ -64,20 +65,7 @@ function parseIntOrNull(value: unknown): number | null { } export function parseTimeoutMs(raw: unknown, fallbackMs: number): number { - const value = - typeof raw === "string" - ? raw.trim() - : typeof raw === "number" || typeof raw === "bigint" - ? String(raw) - : ""; - if (!value) { - return fallbackMs; - } - const parsed = Number.parseInt(value, 10); - if (!Number.isFinite(parsed) || parsed <= 0) { - throw new Error(`invalid --timeout: ${value}`); - } - return parsed; + return parseTimeoutMsWithFallback(raw, fallbackMs); } function normalizeWsUrl(value: string): string | null {