From 49d605ece7b238f2bba9db4e3b9b3de63d9c8dc0 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Wed, 27 May 2026 05:31:03 +0200 Subject: [PATCH] fix(installer): reject stale cli node runtimes --- scripts/install-cli.sh | 18 ++-- test/scripts/install-cli.test.ts | 167 +++++++++++++++++++++++++++++++ 2 files changed, 176 insertions(+), 9 deletions(-) diff --git a/scripts/install-cli.sh b/scripts/install-cli.sh index 397d35ef250..451b35c71ab 100755 --- a/scripts/install-cli.sh +++ b/scripts/install-cli.sh @@ -749,7 +749,6 @@ install_node() { local url local tmp local dir - local current_major local base_url local tarball local expected_sha @@ -764,12 +763,9 @@ install_node() { return fi - if [[ -x "$(node_bin)" ]]; then - current_major="$("$(node_bin)" -v 2>/dev/null | tr -d 'v' | cut -d'.' -f1 || echo "")" - if [[ -n "$current_major" && "$current_major" -ge 22 ]]; then - emit_json "{\"event\":\"step\",\"name\":\"node\",\"status\":\"skip\",\"path\":\"${dir//\"/\\\\\\\"}\"}" - return - fi + if linked_node_is_usable; then + emit_json "{\"event\":\"step\",\"name\":\"node\",\"status\":\"skip\",\"path\":\"${dir//\"/\\\\\\\"}\"}" + return fi emit_json "{\"event\":\"step\",\"name\":\"node\",\"status\":\"start\",\"version\":\"${NODE_VERSION}\"}" @@ -803,8 +799,12 @@ install_node() { ln -sfn "$dir" "${PREFIX}/tools/node" - if ! "$(node_bin)" -e "require('node:sqlite')" >/dev/null 2>&1; then - fail "Installed Node ${NODE_VERSION} is missing node:sqlite; re-run with --node-version 22.22.0 (or newer)" + if ! linked_node_is_usable; then + local installed_version + local required_version + installed_version="$("$(node_bin)" -v 2>/dev/null || echo unknown)" + required_version="$(required_node_version)" + fail "Installed Node ${NODE_VERSION} must provide Node >= ${required_version} with node:sqlite; found ${installed_version}. Re-run with --node-version 22.22.0 (or newer)" fi emit_json "{\"event\":\"step\",\"name\":\"node\",\"status\":\"ok\",\"version\":\"${NODE_VERSION}\"}" } diff --git a/test/scripts/install-cli.test.ts b/test/scripts/install-cli.test.ts index cfebf359c8b..f9436b1639c 100644 --- a/test/scripts/install-cli.test.ts +++ b/test/scripts/install-cli.test.ts @@ -557,6 +557,173 @@ describe("install-cli.sh", () => { } }); + it("replaces cached generic Node runtimes below the runtime floor", () => { + const tmp = mkdtempSync(join(tmpdir(), "openclaw-install-cli-generic-stale-node-")); + const prefix = join(tmp, "prefix"); + const nodePrefixBin = join(prefix, "tools", "node-v22.22.0", "bin"); + const staleNode = join(nodePrefixBin, "node"); + const staleNpm = join(nodePrefixBin, "npm"); + const newNode = join(tmp, "new-node"); + const newNpm = join(tmp, "new-npm"); + + mkdirSync(nodePrefixBin, { recursive: true }); + writeFileSync( + staleNode, + [ + "#!/bin/bash", + 'if [[ "${1:-}" == "-v" ]]; then', + " printf 'v22.18.0\\n'", + " exit 0", + "fi", + 'if [[ "${1:-}" == "-e" ]]; then', + " exit 0", + "fi", + "exit 0", + "", + ].join("\n"), + ); + writeFileSync(staleNpm, ["#!/bin/bash", "exit 0", ""].join("\n")); + writeFileSync( + newNode, + [ + "#!/bin/bash", + 'if [[ "${1:-}" == "-v" ]]; then', + " printf 'v22.22.0\\n'", + " exit 0", + "fi", + 'if [[ "${1:-}" == "-e" ]]; then', + " exit 0", + "fi", + "exit 0", + "", + ].join("\n"), + ); + writeFileSync(newNpm, ["#!/bin/bash", "exit 0", ""].join("\n")); + chmodSync(staleNode, 0o755); + chmodSync(staleNpm, 0o755); + chmodSync(newNode, 0o755); + chmodSync(newNpm, 0o755); + + try { + const result = runInstallCliShell( + [ + "set -euo pipefail", + `cd ${JSON.stringify(process.cwd())}`, + `source ${JSON.stringify(SCRIPT_PATH)}`, + "os_detect() { printf 'linux\\n'; }", + "arch_detect() { printf 'x64\\n'; }", + "is_musl_linux() { return 1; }", + "detect_downloader() { :; }", + "require_bin() { :; }", + "download_file() {", + ' case "$1" in', + " */SHASUMS256.txt) printf 'fixture-sha node-v22.22.0-linux-x64.tar.gz\\n' > \"$2\" ;;", + " *) printf 'node tarball fixture\\n' > \"$2\" ;;", + " esac", + "}", + "sha256_file() { printf 'fixture-sha\\n'; }", + "tar() {", + " local dest=''", + " while [[ $# -gt 0 ]]; do", + ' if [[ "$1" == \'-C\' ]]; then dest="$2"; shift 2; else shift; fi', + " done", + ' mkdir -p "$dest/bin"', + ' cp "$NEW_NODE" "$dest/bin/node"', + ' cp "$NEW_NPM" "$dest/bin/npm"', + "}", + `PREFIX=${JSON.stringify(prefix)}`, + "NODE_VERSION=22.22.0", + "NODE_VERSION_REQUESTED=1", + "install_node", + ].join("\n"), + { + NEW_NODE: newNode, + NEW_NPM: newNpm, + }, + ); + + expect(result.status).toBe(0); + expect(result.stdout).toContain("Installing Node 22.22.0 (user-space)"); + expect(result.stdout).not.toContain('"status":"skip"'); + expect(readFileSync(staleNode, "utf8")).toContain("v22.22.0"); + } finally { + rmSync(tmp, { force: true, recursive: true }); + } + }); + + it("rejects downloaded generic Node runtimes below the runtime floor", () => { + const tmp = mkdtempSync(join(tmpdir(), "openclaw-install-cli-generic-old-node-")); + const prefix = join(tmp, "prefix"); + const newNode = join(tmp, "new-node"); + const newNpm = join(tmp, "new-npm"); + + writeFileSync( + newNode, + [ + "#!/bin/bash", + 'if [[ "${1:-}" == "-v" ]]; then', + " printf 'v22.18.0\\n'", + " exit 0", + "fi", + 'if [[ "${1:-}" == "-e" ]]; then', + " exit 0", + "fi", + "exit 0", + "", + ].join("\n"), + ); + writeFileSync(newNpm, ["#!/bin/bash", "exit 0", ""].join("\n")); + chmodSync(newNode, 0o755); + chmodSync(newNpm, 0o755); + + try { + const result = runInstallCliShell( + [ + "set -euo pipefail", + `cd ${JSON.stringify(process.cwd())}`, + `source ${JSON.stringify(SCRIPT_PATH)}`, + "os_detect() { printf 'linux\\n'; }", + "arch_detect() { printf 'x64\\n'; }", + "is_musl_linux() { return 1; }", + "detect_downloader() { :; }", + "require_bin() { :; }", + "download_file() {", + ' case "$1" in', + " */SHASUMS256.txt) printf 'fixture-sha node-v22.18.0-linux-x64.tar.gz\\n' > \"$2\" ;;", + " *) printf 'node tarball fixture\\n' > \"$2\" ;;", + " esac", + "}", + "sha256_file() { printf 'fixture-sha\\n'; }", + "tar() {", + " local dest=''", + " while [[ $# -gt 0 ]]; do", + ' if [[ "$1" == \'-C\' ]]; then dest="$2"; shift 2; else shift; fi', + " done", + ' mkdir -p "$dest/bin"', + ' cp "$NEW_NODE" "$dest/bin/node"', + ' cp "$NEW_NPM" "$dest/bin/npm"', + "}", + `PREFIX=${JSON.stringify(prefix)}`, + "NODE_VERSION=22.18.0", + "NODE_VERSION_REQUESTED=1", + "install_node", + ].join("\n"), + { + NEW_NODE: newNode, + NEW_NPM: newNpm, + }, + ); + + expect(result.status).toBe(1); + expect(result.stdout).toContain( + "Installed Node 22.18.0 must provide Node >= 22.19.0 with node:sqlite", + ); + expect(result.stdout).toContain("found v22.18.0"); + } finally { + rmSync(tmp, { force: true, recursive: true }); + } + }); + it("clears npm freshness filters for package installs", () => { expect(script).toContain('freshness_flag="--min-release-age=0"'); expect(script).toContain('npm_config_has_raw_key "$(npm_bin)" "min-release-age"');