fix(installer): reject stale cli node runtimes

This commit is contained in:
Vincent Koc
2026-05-27 05:31:03 +02:00
parent acbb06e266
commit 49d605ece7
2 changed files with 176 additions and 9 deletions

View File

@@ -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}\"}"
}

View File

@@ -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"');