diff --git a/scripts/docker/install-sh-smoke/run.sh b/scripts/docker/install-sh-smoke/run.sh index 1611df660446..7cf5a4362504 100755 --- a/scripts/docker/install-sh-smoke/run.sh +++ b/scripts/docker/install-sh-smoke/run.sh @@ -1,6 +1,34 @@ #!/usr/bin/env bash set -euo pipefail +read_positive_int_env() { + local name="${1:?missing environment variable name}" + local fallback="${2:?missing fallback value}" + local value="${!name-}" + if [[ -z "${!name+x}" ]]; then + value="$fallback" + fi + if [[ ! "$value" =~ ^[0-9]+$ ]] || (( 10#$value < 1 )); then + echo "invalid $name: $value" >&2 + return 2 + fi + printf "%s\n" "$((10#$value))" +} + +read_nonnegative_int_env() { + local name="${1:?missing environment variable name}" + local fallback="${2:?missing fallback value}" + local value="${!name-}" + if [[ -z "${!name+x}" ]]; then + value="$fallback" + fi + if [[ ! "$value" =~ ^[0-9]+$ ]]; then + echo "invalid $name: $value" >&2 + return 2 + fi + printf "%s\n" "$((10#$value))" +} + INSTALL_URL="${OPENCLAW_INSTALL_URL:-https://openclaw.bot/install.sh}" SMOKE_MODE="${OPENCLAW_INSTALL_SMOKE_MODE:-install}" SMOKE_PREVIOUS_VERSION="${OPENCLAW_INSTALL_SMOKE_PREVIOUS:-}" @@ -18,8 +46,8 @@ FRESHNESS_VERSION="${OPENCLAW_INSTALL_FRESHNESS_VERSION:-latest}" # npm min-release-age is days; 10000 keeps the control failure independent of normal release cadence. FRESHNESS_MIN_RELEASE_AGE="${OPENCLAW_INSTALL_FRESHNESS_MIN_RELEASE_AGE:-10000}" FRESHNESS_NPM_VERSION="${OPENCLAW_INSTALL_FRESHNESS_NPM_VERSION:-11.14.1}" -HEARTBEAT_INTERVAL="${OPENCLAW_INSTALL_SMOKE_HEARTBEAT_INTERVAL:-60}" -INSTALL_COMMAND_TIMEOUT="${OPENCLAW_INSTALL_SMOKE_COMMAND_TIMEOUT:-900}" +HEARTBEAT_INTERVAL="$(read_nonnegative_int_env OPENCLAW_INSTALL_SMOKE_HEARTBEAT_INTERVAL 60)" +INSTALL_COMMAND_TIMEOUT="$(read_positive_int_env OPENCLAW_INSTALL_SMOKE_COMMAND_TIMEOUT 900)" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" # shellcheck source=../install-sh-common/cli-verify.sh @@ -75,7 +103,7 @@ run_with_heartbeat() { local label="$1" shift local interval="$HEARTBEAT_INTERVAL" - if ! [[ "$interval" =~ ^[0-9]+$ ]] || [[ "$interval" == "0" ]]; then + if [[ "$interval" == "0" ]]; then "$@" return fi diff --git a/test/scripts/test-install-sh-docker.test.ts b/test/scripts/test-install-sh-docker.test.ts index 29ac365127bb..4a6265434b2c 100644 --- a/test/scripts/test-install-sh-docker.test.ts +++ b/test/scripts/test-install-sh-docker.test.ts @@ -473,11 +473,12 @@ describe("install-sh smoke runner", () => { const script = readFileSync(SMOKE_RUNNER_PATH, "utf8"); expect(script).toContain( - 'HEARTBEAT_INTERVAL="${OPENCLAW_INSTALL_SMOKE_HEARTBEAT_INTERVAL:-60}"', + 'HEARTBEAT_INTERVAL="$(read_nonnegative_int_env OPENCLAW_INSTALL_SMOKE_HEARTBEAT_INTERVAL 60)"', ); expect(script).toContain( - 'INSTALL_COMMAND_TIMEOUT="${OPENCLAW_INSTALL_SMOKE_COMMAND_TIMEOUT:-900}"', + 'INSTALL_COMMAND_TIMEOUT="$(read_positive_int_env OPENCLAW_INSTALL_SMOKE_COMMAND_TIMEOUT 900)"', ); + expect(script).toContain('if [[ "$interval" == "0" ]]; then'); expect(script).toContain("run_with_heartbeat"); expect(script).toContain("npm_install_global"); expect(script).toContain('timeout --kill-after=30s "${INSTALL_COMMAND_TIMEOUT}s"'); @@ -491,6 +492,23 @@ describe("install-sh smoke runner", () => { expect(script).toContain("unterminated update JSON object"); }); + it.each([ + ["command timeout", "OPENCLAW_INSTALL_SMOKE_COMMAND_TIMEOUT", "900s"], + ["heartbeat interval", "OPENCLAW_INSTALL_SMOKE_HEARTBEAT_INTERVAL", "60s"], + ])("rejects invalid install smoke %s before running npm", (_label, envName, value) => { + const result = spawnSync("bash", [SMOKE_RUNNER_PATH], { + encoding: "utf8", + env: { + ...process.env, + [envName]: value, + }, + }); + + expect(result.status).toBe(2); + expect(result.stderr).toContain(`invalid ${envName}: ${value}`); + expect(result.stderr).not.toContain("unsupported OPENCLAW_INSTALL_SMOKE_MODE"); + }); + it("covers plain npm global installs and npm-driven updates", () => { const script = readFileSync(SCRIPT_PATH, "utf8"); const runner = readFileSync(SMOKE_RUNNER_PATH, "utf8");