Files
unslothai--unsloth/tests/studio/install/test_install_node_prebuilt_logic.py
wehub-resource-sync e93507a09c
Lockfile supply-chain audit / lockfile supply-chain audit (push) Has been cancelled
Windows Studio GGUF CI / GPU prebuilt resolves without Visual Studio (push) Has been cancelled
Windows Studio GGUF CI / setup.ps1 unit tests (VS 2026 / CMake guard) (push) Has been cancelled
Windows Studio GGUF CI / real-VS detection (VS 2022) (push) Has been cancelled
Windows Studio GGUF CI / real-VS detection (VS 2026) (push) Has been cancelled
Windows Studio GGUF CI / VC++ runtime detect + install round-trip (windows-2025-vs2026) (push) Has been cancelled
Windows Studio GGUF CI / VC++ runtime detect + install round-trip (windows-latest) (push) Has been cancelled
Windows Studio Update CI / Studio Updating Tests (push) Has been cancelled
Wheel CI / Wheel build + content sanity + import smoke (push) Has been cancelled
Lint CI / Source lint (Python + shell + YAML + JSON + safety nets) (push) Has been cancelled
MLX CI on Mac M1 / dispatch (push) Has been cancelled
Security audit / advisory audit (pip + npm + cargo) (push) Has been cancelled
Security audit / pip scan-packages :: extras (push) Has been cancelled
Security audit / pip scan-packages :: studio (push) Has been cancelled
Security audit / pip scan-packages :: hf-stack (push) Has been cancelled
Security audit / npm scan-packages (Studio frontend tarballs) (push) Has been cancelled
Security audit / workflow-trigger lint (pull_request_target / cache-poisoning) (push) Has been cancelled
Security audit / pytest tests/security (push) Has been cancelled
Security audit / npm provenance + new install-script diff (push) Has been cancelled
Studio API CI / Studio API & Auth Tests (push) Has been cancelled
Backend CI / (Python 3.10) (push) Has been cancelled
Backend CI / (Python 3.11) (push) Has been cancelled
Backend CI / (Python 3.12) (push) Has been cancelled
Backend CI / (Python 3.13) (push) Has been cancelled
Backend CI / Repo tests (CPU) (push) Has been cancelled
Frontend CI / Frontend build + bundle sanity (push) Has been cancelled
Studio GGUF CI / OpenAI, Anthropic API tests (push) Has been cancelled
Studio GGUF CI / Tool calling Tests (push) Has been cancelled
Studio GGUF CI / JSON, images (push) Has been cancelled
Mac Studio GGUF CI / OpenAI, Anthropic API tests (push) Has been cancelled
Mac Studio GGUF CI / Tool calling Tests (push) Has been cancelled
Mac Studio GGUF CI / JSON, images (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-14) (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-15) (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-26) (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-15-intel) (push) Has been cancelled
Mac Studio API CI / Studio API & Auth Tests (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-26-intel) (push) Has been cancelled
Mac Studio UI CI / Chat UI Tests (push) Has been cancelled
Studio Tauri CI / Tauri Linux debug build (no codesign) (push) Has been cancelled
Mac Studio Update CI / Studio Updating Tests (push) Has been cancelled
Studio UI CI / Chat UI Tests (push) Has been cancelled
Windows Studio API CI / Studio API & Auth Tests (push) Has been cancelled
Windows Studio UI CI / Chat UI Tests (push) Has been cancelled
Studio Update CI / Studio Updating Tests (push) Has been cancelled
Core / Core (HF=default + TRL=default) (push) Has been cancelled
Core / Core (HF=4.57.6 + TRL<1) (push) Has been cancelled
Core / Core (HF=latest + TRL=latest) (push) Has been cancelled
Core / llama.cpp build + smoke (push) Has been cancelled
Windows Studio GGUF CI / OpenAI, Anthropic API tests (push) Has been cancelled
Windows Studio GGUF CI / Tool calling Tests (push) Has been cancelled
Windows Studio GGUF CI / JSON, images (push) Has been cancelled
Windows Studio GGUF CI / Studio install + inference without Visual Studio (push) Has been cancelled
Studio export capability / capability (macos-latest) (push) Has been cancelled
Studio export capability / capability (ubuntu-latest) (push) Has been cancelled
Studio export capability / capability (windows-latest) (push) Has been cancelled
Cross-platform parity / parity (macos-latest) (push) Has been cancelled
Cross-platform parity / parity (windows-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
Studio load-orchestrator CI / test (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:59:56 +08:00

765 lines
32 KiB
Python

# SPDX-License-Identifier: AGPL-3.0-only
# Logic tests for studio/install_node_prebuilt.py -- the isolated Node installer.
# No network/GPU: downloads are monkeypatched and archives are built in-memory.
import importlib.util
import io
import json
import os
import sys
import tarfile
import types
import zipfile
from pathlib import Path
import pytest
PACKAGE_ROOT = Path(__file__).resolve().parents[3]
MODULE_PATH = PACKAGE_ROOT / "studio" / "install_node_prebuilt.py"
SPEC = importlib.util.spec_from_file_location("studio_install_node_prebuilt", MODULE_PATH)
assert SPEC is not None and SPEC.loader is not None
M = importlib.util.module_from_spec(SPEC)
sys.modules[SPEC.name] = M
SPEC.loader.exec_module(M)
HostInfo = M.HostInfo
PrebuiltFallback = M.PrebuiltFallback
def _host(node_os: str, node_arch: str) -> HostInfo:
ext = ".zip" if node_os == "win" else ".tar.gz"
return HostInfo(
system = {"linux": "Linux", "darwin": "Darwin", "win": "Windows"}[node_os],
machine = node_arch,
node_os = node_os,
node_arch = node_arch,
archive_ext = ext,
is_windows = node_os == "win",
)
# ── Host detection (per OS/arch) ──
@pytest.mark.parametrize(
"system,machine,exp_os,exp_arch,exp_ext",
[
("Linux", "x86_64", "linux", "x64", ".tar.gz"),
("Linux", "aarch64", "linux", "arm64", ".tar.gz"),
("Darwin", "x86_64", "darwin", "x64", ".tar.gz"),
("Darwin", "arm64", "darwin", "arm64", ".tar.gz"),
("Windows", "AMD64", "win", "x64", ".zip"),
("Windows", "ARM64", "win", "arm64", ".zip"),
],
)
def test_detect_host(monkeypatch, system, machine, exp_os, exp_arch, exp_ext):
monkeypatch.setattr(M.platform, "system", lambda: system)
monkeypatch.setattr(M.platform, "machine", lambda: machine)
host = M.detect_host()
assert (host.node_os, host.node_arch, host.archive_ext) == (exp_os, exp_arch, exp_ext)
assert host.is_windows == (exp_os == "win")
@pytest.mark.parametrize(
"system,machine",
[("Plan9", "x86_64"), ("Linux", "sparc64"), ("Linux", "armv7l"), ("Linux", "armhf")],
)
def test_detect_host_unsupported(monkeypatch, system, machine):
monkeypatch.setattr(M.platform, "system", lambda: system)
monkeypatch.setattr(M.platform, "machine", lambda: machine)
with pytest.raises(PrebuiltFallback):
M.detect_host()
# ── URL / asset construction (pure) ──
def test_asset_and_url_linux():
host = _host("linux", "x64")
assert M.node_asset_name("24.17.0", host) == "node-v24.17.0-linux-x64.tar.gz"
assert (
M.node_download_url("24.17.0", M.node_asset_name("24.17.0", host))
== "https://nodejs.org/dist/v24.17.0/node-v24.17.0-linux-x64.tar.gz"
)
def test_asset_windows_is_zip():
host = _host("win", "x64")
assert M.node_asset_name("24.17.0", host) == "node-v24.17.0-win-x64.zip"
def test_shasums_url():
assert M.node_shasums_url("24.17.0") == "https://nodejs.org/dist/v24.17.0/SHASUMS256.txt"
def test_binary_layout_is_host_aware():
# Windows ships node.exe + node_modules\npm at the root; Unix uses bin/ + lib/.
win = _host("win", "x64")
nix = _host("linux", "x64")
assert M.node_binary_path(Path("/n"), win) == Path("/n/node.exe")
assert M.node_binary_path(Path("/n"), nix) == Path("/n/bin/node")
assert M.npm_cli_path(Path("/n"), win) == Path("/n/node_modules/npm/bin/npm-cli.js")
assert M.npm_cli_path(Path("/n"), nix) == Path("/n/lib/node_modules/npm/bin/npm-cli.js")
# ── SHASUMS256.txt parsing ──
def test_expected_sha256_for():
asset = "node-v24.17.0-linux-x64.tar.gz"
good = "a" * 64
text = (
f"{'b' * 64} node-v24.17.0-linux-arm64.tar.gz\n"
f"{good} {asset}\n"
f"{'c' * 64} node-v24.17.0-win-x64.zip\n"
)
assert M.expected_sha256_for(text, asset) == good
assert M.expected_sha256_for(text, "node-v24.17.0-darwin-x64.tar.gz") is None
def test_expected_sha256_rejects_malformed():
asset = "node-v24.17.0-linux-x64.tar.gz"
assert M.expected_sha256_for(f"notahex {asset}\n", asset) is None
# ── Version selection from index.json ──
INDEX = [
{"version": "v26.3.1", "lts": False},
{"version": "v24.18.0", "lts": "Krypton"},
{"version": "v24.9.0", "lts": "Krypton"},
{"version": "v22.20.0", "lts": "Jod"},
{"version": "v20.19.0", "lts": "Iron"},
]
def test_select_lts_respects_min_major():
# Newest LTS at/above 24 -> 24.18.0 (22.x LTS is below the floor).
assert M.select_node_version(INDEX, channel = "lts", min_major = 24) == "24.18.0"
def test_select_latest_overall():
assert M.select_node_version(INDEX, channel = "latest", min_major = 24) == "26.3.1"
def test_select_explicit_passthrough():
assert M.select_node_version(INDEX, channel = "v24.5.0", min_major = 24) == "24.5.0"
def test_select_no_candidate_raises():
with pytest.raises(PrebuiltFallback):
M.select_node_version(INDEX, channel = "lts", min_major = 99)
# ── Archive extraction (zip + tar.gz with the npm-style symlink), traversal guard ──
def _add_file(
tar: tarfile.TarFile,
name: str,
data: bytes,
mode: int = 0o644,
):
info = tarfile.TarInfo(name)
info.size = len(data)
info.mode = mode
tar.addfile(info, io.BytesIO(data))
def _add_symlink(tar: tarfile.TarFile, name: str, target: str):
info = tarfile.TarInfo(name)
info.type = tarfile.SYMTYPE
info.linkname = target
tar.addfile(info)
@pytest.mark.skipif(
os.name == "nt",
reason = "Node ships a .zip (no symlinks) on Windows; the tar+symlink path is Unix-only",
)
def test_extract_tar_gz_with_npm_symlink(tmp_path: Path):
# Mirrors the real Node tarball: bin/npm -> ../lib/node_modules/npm/bin/npm-cli.js
archive = tmp_path / "node.tar.gz"
with tarfile.open(archive, "w:gz") as tar:
_add_file(tar, "node-v24/bin/node", b"#!/bin/sh\necho v24.17.0\n", mode = 0o755)
_add_file(tar, "node-v24/lib/node_modules/npm/bin/npm-cli.js", b"// npm")
_add_symlink(tar, "node-v24/bin/npm", "../lib/node_modules/npm/bin/npm-cli.js")
dest = tmp_path / "out"
M.extract_archive(archive, dest)
npm_link = dest / "node-v24" / "bin" / "npm"
assert npm_link.is_symlink()
assert (dest / "node-v24" / "bin" / "node").exists()
# executable bit preserved
assert (dest / "node-v24" / "bin" / "node").stat().st_mode & 0o111
def test_extract_zip(tmp_path: Path):
archive = tmp_path / "node.zip"
with zipfile.ZipFile(archive, "w") as zf:
zf.writestr("node-v24-win-x64/node.exe", b"MZ")
zf.writestr("node-v24-win-x64/npm.cmd", b"@echo off")
dest = tmp_path / "out"
M.extract_archive(archive, dest)
assert (dest / "node-v24-win-x64" / "node.exe").exists()
def test_extract_rejects_path_traversal(tmp_path: Path):
archive = tmp_path / "evil.tar.gz"
with tarfile.open(archive, "w:gz") as tar:
_add_file(tar, "../escape.txt", b"pwn")
with pytest.raises(PrebuiltFallback):
M.extract_archive(archive, tmp_path / "out")
# ── Checksum-verified download (accept + reject) ──
def test_download_file_verified_accepts_match(tmp_path: Path, monkeypatch):
payload = b"real-node-archive"
sha = M.hashlib.sha256(payload).hexdigest()
def fake_download(url: str, destination: Path):
destination.write_bytes(payload)
monkeypatch.setattr(M, "download_file", fake_download)
dest = tmp_path / "a.tar.gz"
M.download_file_verified("http://x/a.tar.gz", dest, expected_sha256 = sha, label = "a")
assert dest.read_bytes() == payload
def test_download_file_verified_rejects_mismatch(tmp_path: Path, monkeypatch):
def fake_download(url: str, destination: Path):
destination.write_bytes(b"tampered")
monkeypatch.setattr(M, "download_file", fake_download)
with pytest.raises(PrebuiltFallback):
M.download_file_verified("http://x/a", tmp_path / "a", expected_sha256 = "0" * 64, label = "a")
# ── Lock liveness probe (Windows must not use os.kill(pid, 0)) ──
def test_pid_is_alive_windows_uses_tasklist_not_os_kill(monkeypatch):
monkeypatch.setattr(M.sys, "platform", "win32")
def fail_kill(pid, sig):
raise AssertionError("Windows liveness must not call os.kill(pid, 0)")
def fake_run(cmd, **kwargs):
assert cmd[:2] == ["tasklist", "/FI"]
assert "PID eq 1234" in cmd
return types.SimpleNamespace(stdout = '"node.exe","1234","Console","1","12,345 K"\n')
monkeypatch.setattr(M.os, "kill", fail_kill)
monkeypatch.setattr(M.subprocess, "run", fake_run)
assert M._pid_is_alive(1234) is True
def test_pid_is_alive_windows_false_when_tasklist_omits_pid(monkeypatch):
monkeypatch.setattr(M.sys, "platform", "win32")
monkeypatch.setattr(
M.subprocess,
"run",
lambda *a, **k: types.SimpleNamespace(
stdout = "INFO: No tasks are running which match the specified criteria.\n"
),
)
assert M._pid_is_alive(1234) is False
def test_pid_is_alive_windows_assumes_alive_when_tasklist_fails(monkeypatch):
monkeypatch.setattr(M.sys, "platform", "win32")
def boom(*args, **kwargs):
raise OSError("tasklist unavailable")
monkeypatch.setattr(M.subprocess, "run", boom)
assert M._pid_is_alive(1234) is True
def test_pid_is_alive_posix_signal_zero(monkeypatch):
monkeypatch.setattr(M.sys, "platform", "linux")
calls = []
def fake_kill(pid, sig):
calls.append((pid, sig))
if pid == 9999:
raise ProcessLookupError
monkeypatch.setattr(M.os, "kill", fake_kill)
assert M._pid_is_alive(1234) is True
assert M._pid_is_alive(9999) is False
assert calls == [(1234, 0), (9999, 0)]
# ── existing_install_matches + install_prebuilt short-circuit ──
def test_existing_install_matches_false_without_metadata(tmp_path: Path):
host = _host("linux", "x64")
assert M.existing_install_matches(tmp_path, host, version = "24.17.0") is False
def test_existing_install_matches_true_when_version_and_runtime_ok(tmp_path: Path, monkeypatch):
host = _host("linux", "x64")
M.write_metadata(tmp_path, version = "24.17.0", asset = "x", sha256 = "y")
monkeypatch.setattr(M, "installed_node_version", lambda d, h: "24.17.0")
monkeypatch.setattr(M, "installed_npm_major", lambda d, h: 11)
assert M.existing_install_matches(tmp_path, host, version = "24.17.0") is True
# npm too old -> not a match
monkeypatch.setattr(M, "installed_npm_major", lambda d, h: 10)
assert M.existing_install_matches(tmp_path, host, version = "24.17.0") is False
def test_install_prebuilt_short_circuits_when_version_matches(tmp_path: Path, monkeypatch):
install_dir = tmp_path / "node"
install_dir.mkdir()
version = M.pinned_default_version(M.load_pins()) # == INDEX's newest LTS
asset = M.node_asset_name(version, _host("linux", "x64"))
pin = M.pinned_sha256(M.load_pins(), version, asset) # short-circuit now needs the pin
M.write_metadata(install_dir, version = version, asset = asset, sha256 = pin)
monkeypatch.setattr(M, "detect_host", lambda: _host("linux", "x64"))
monkeypatch.setattr(M, "fetch_json", lambda url: INDEX)
monkeypatch.setattr(M, "installed_node_version", lambda d, h: version)
monkeypatch.setattr(M, "installed_npm_major", lambda d, h: 11)
def boom(*a, **k):
raise AssertionError("must not download when the install already matches")
monkeypatch.setattr(M, "download_file", boom)
monkeypatch.setattr(M, "download_bytes", boom)
rc = M.install_prebuilt(install_dir, channel = "lts", min_major = 24, force = False)
assert rc == M.EXIT_SUCCESS
def test_existing_install_usable_is_version_agnostic(tmp_path: Path, monkeypatch):
host = _host("linux", "x64")
assert M.existing_install_usable(tmp_path, host) is False # no metadata
M.write_metadata(tmp_path, version = "24.17.0", asset = "x", sha256 = "y")
monkeypatch.setattr(M, "installed_node_version", lambda d, h: "24.17.0")
monkeypatch.setattr(M, "installed_npm_major", lambda d, h: 11)
assert M.existing_install_usable(tmp_path, host) is True
monkeypatch.setattr(M, "installed_npm_major", lambda d, h: 10)
assert M.existing_install_usable(tmp_path, host) is False # npm below floor
monkeypatch.setattr(M, "installed_npm_major", lambda d, h: 11)
monkeypatch.setattr(M, "installed_node_version", lambda d, h: None)
assert M.existing_install_usable(tmp_path, host) is False # node does not run
def _offline(*a, **k):
raise OSError("nodejs.org unreachable")
def test_install_prebuilt_keeps_existing_when_index_unreachable(tmp_path: Path, monkeypatch):
install_dir = tmp_path / "node"
install_dir.mkdir()
M.write_metadata(install_dir, version = "24.17.0", asset = "x", sha256 = "y")
monkeypatch.setattr(M, "detect_host", lambda: _host("linux", "x64"))
monkeypatch.setattr(M, "installed_node_version", lambda d, h: "24.17.0")
monkeypatch.setattr(M, "installed_npm_major", lambda d, h: 11)
monkeypatch.setattr(M, "fetch_json", _offline)
def boom(*a, **k):
raise AssertionError("must not download when keeping the existing install")
monkeypatch.setattr(M, "download_file", boom)
monkeypatch.setattr(M, "download_bytes", boom)
rc = M.install_prebuilt(install_dir, channel = "lts", min_major = 24, force = False)
assert rc == M.EXIT_SUCCESS
def test_install_prebuilt_reraises_when_index_unreachable_and_no_install(
tmp_path: Path, monkeypatch
):
install_dir = tmp_path / "node" # nothing on disk to fall back to
monkeypatch.setattr(M, "detect_host", lambda: _host("linux", "x64"))
monkeypatch.setattr(M, "fetch_json", _offline)
with pytest.raises(OSError):
M.install_prebuilt(install_dir, channel = "lts", min_major = 24, force = False)
def test_install_prebuilt_force_does_not_keep_existing_offline(tmp_path: Path, monkeypatch):
install_dir = tmp_path / "node"
install_dir.mkdir()
M.write_metadata(install_dir, version = "24.17.0", asset = "x", sha256 = "y")
monkeypatch.setattr(M, "detect_host", lambda: _host("linux", "x64"))
monkeypatch.setattr(M, "installed_node_version", lambda d, h: "24.17.0")
monkeypatch.setattr(M, "installed_npm_major", lambda d, h: 11)
monkeypatch.setattr(M, "fetch_json", _offline)
with pytest.raises(OSError):
M.install_prebuilt(install_dir, channel = "lts", min_major = 24, force = True)
@pytest.mark.parametrize(
"ver,ok",
[
("20.19.0", True),
("20.18.9", False),
("22.12.0", True),
("22.11.5", False),
("23.0.0", True),
("24.4.1", True),
("21.7.3", False),
("24", True),
("20", False),
],
)
def test_meets_node_floor(ver, ok):
assert M._meets_node_floor(ver) is ok
def test_install_prebuilt_rejects_explicit_below_floor(tmp_path: Path, monkeypatch):
install_dir = tmp_path / "node"
monkeypatch.setattr(M, "detect_host", lambda: _host("linux", "x64"))
def boom(*a, **k):
raise AssertionError("must not download a below-floor Node")
monkeypatch.setattr(M, "download_file", boom)
monkeypatch.setattr(M, "download_bytes", boom)
with pytest.raises(PrebuiltFallback):
M.install_prebuilt(install_dir, channel = "20.18.0", min_major = 24, force = False)
def test_install_prebuilt_keeps_existing_when_download_fails(tmp_path: Path, monkeypatch):
# Archive download fails but a usable older Node is on disk -> keep it.
install_dir = tmp_path / "node"
install_dir.mkdir()
M.write_metadata(install_dir, version = "24.9.0", asset = "x", sha256 = "y")
monkeypatch.setattr(M, "detect_host", lambda: _host("linux", "x64"))
monkeypatch.setattr(M, "fetch_json", lambda url: INDEX) # newest LTS = 24.18.0 (pinned)
monkeypatch.setattr(M, "installed_node_version", lambda d, h: "24.9.0")
monkeypatch.setattr(M, "installed_npm_major", lambda d, h: 11)
monkeypatch.setattr(M, "download_file_verified", _offline) # archive download fails
rc = M.install_prebuilt(install_dir, channel = "lts", min_major = 24, force = False)
assert rc == M.EXIT_SUCCESS
def test_install_prebuilt_reraises_download_failure_without_existing(tmp_path: Path, monkeypatch):
install_dir = tmp_path / "node" # nothing usable on disk
monkeypatch.setattr(M, "detect_host", lambda: _host("linux", "x64"))
monkeypatch.setattr(M, "fetch_json", lambda url: INDEX)
monkeypatch.setattr(M, "download_file_verified", _offline)
with pytest.raises(OSError):
M.install_prebuilt(install_dir, channel = "lts", min_major = 24, force = False)
# ── Isolation invariant: the installer only writes inside its own install_dir ──
def test_run_node_pins_npm_prefix_to_install_dir(tmp_path: Path, monkeypatch):
# Every node/npm call the installer makes redirects npm's global prefix into
# the isolated install_dir and drops an inherited NODE_PATH, so a stray `npm
# -g` can never write to the user's system Node/npm.
install_dir = tmp_path / "node"
monkeypatch.setenv("NPM_CONFIG_PREFIX", "/usr/local") # user's own global prefix
monkeypatch.setenv("NODE_PATH", "/usr/lib/node_modules")
captured = {}
def fake_run(cmd, **kw):
captured["env"] = kw["env"]
return types.SimpleNamespace(returncode = 0, stdout = "v24.17.0\n", stderr = "")
monkeypatch.setattr(M.subprocess, "run", fake_run)
assert M._run_node(install_dir, _host("linux", "x64"), ["-v"]) == "v24.17.0"
env = captured["env"]
assert env["NPM_CONFIG_PREFIX"] == str(install_dir)
assert env["npm_config_prefix"] == str(install_dir)
assert "NODE_PATH" not in env # inherited NODE_PATH is dropped, not leaked in
def test_ensure_npm_floor_scopes_upgrade_to_install_dir(tmp_path: Path, monkeypatch):
# A pinned build shipping npm < 11 self-upgrades, but only inside the isolated
# prefix: it goes through _run_node against install_dir, never the system.
install_dir = tmp_path / "node"
monkeypatch.setattr(M, "installed_npm_major", lambda d, h: 10)
calls = []
monkeypatch.setattr(M, "_run_node", lambda d, h, args, **kw: calls.append((d, args)) or "")
M._ensure_npm_floor(install_dir, _host("linux", "x64"))
assert len(calls) == 1
target_dir, args = calls[0]
assert target_dir == install_dir # upgrade scoped to the isolated dir
assert args[-3:] == ["install", "-g", f"npm@^{M.NPM_MIN_MAJOR}"]
def test_ensure_npm_floor_noop_when_npm_meets_bar(tmp_path: Path, monkeypatch):
monkeypatch.setattr(M, "installed_npm_major", lambda d, h: M.NPM_MIN_MAJOR)
def boom(*a, **k):
raise AssertionError("must not run an npm upgrade when npm already meets the floor")
monkeypatch.setattr(M, "_run_node", boom)
M._ensure_npm_floor(tmp_path / "node", _host("linux", "x64"))
# ── Pinned digest manifest (trust anchor) ──
# Archives must be verified against committed pins, never a same-origin re-fetch.
ALL_SUPPORTED_HOSTS = [
("linux", "x64"),
("linux", "arm64"),
("darwin", "x64"),
("darwin", "arm64"),
("win", "x64"),
("win", "arm64"),
]
def test_load_pins_exposes_valid_default_version():
pins = M.load_pins()
version = M.pinned_default_version(pins)
assert M._version_tuple(version) # parses as a real version
assert M._meets_node_floor(version) # the pinned default clears the build floor
def test_pinned_manifest_covers_every_supported_asset():
# A future Node bump must pin all six os/arch archives or a host loses its anchor.
pins = M.load_pins()
version = M.pinned_default_version(pins)
for node_os, node_arch in ALL_SUPPORTED_HOSTS:
host = _host(node_os, node_arch)
asset = M.node_asset_name(version, host)
digest = M.pinned_sha256(pins, version, asset)
assert digest is not None and len(digest) == 64, f"missing pin for {asset}"
assert all(c in "0123456789abcdef" for c in digest)
def test_pinned_sha256_unknown_pair_returns_none():
pins = M.load_pins()
assert M.pinned_sha256(pins, "99.0.0", "node-v99.0.0-linux-x64.tar.gz") is None
version = M.pinned_default_version(pins)
assert M.pinned_sha256(pins, version, "node-vX-bogus-arch.tar.gz") is None
def test_load_pins_rejects_bad_schema(tmp_path: Path, monkeypatch):
bad = tmp_path / "node_prebuilt_pins.json"
bad.write_text(json.dumps({"schema_version": 999}))
monkeypatch.setattr(M, "pins_path", lambda: bad)
with pytest.raises(PrebuiltFallback):
M.load_pins()
def test_load_pins_raises_when_missing(tmp_path: Path, monkeypatch):
monkeypatch.setattr(M, "pins_path", lambda: tmp_path / "does_not_exist.json")
with pytest.raises(PrebuiltFallback):
M.load_pins()
def test_resolve_expected_sha256_uses_pin_without_touching_network(monkeypatch):
pins = M.load_pins()
version = M.pinned_default_version(pins)
asset = M.node_asset_name(version, _host("linux", "x64"))
def boom(*a, **k):
raise AssertionError("pinned path must not fetch the remote SHASUMS256.txt")
monkeypatch.setattr(M, "download_bytes", boom)
sha = M.resolve_expected_sha256(pins, version, asset, allow_unverified = False)
assert sha == M.pinned_sha256(pins, version, asset)
def test_resolve_expected_sha256_failcloses_on_unpinned(monkeypatch):
pins = M.load_pins()
def boom(*a, **k):
raise AssertionError("must not reach the network when refusing an unpinned version")
monkeypatch.setattr(M, "download_bytes", boom)
with pytest.raises(PrebuiltFallback):
M.resolve_expected_sha256(
pins, "26.3.1", "node-v26.3.1-linux-x64.tar.gz", allow_unverified = False
)
def test_resolve_expected_sha256_optin_falls_back_to_remote_shasums(monkeypatch):
pins = M.load_pins()
asset = "node-v26.3.1-linux-x64.tar.gz"
remote_sha = "d" * 64
monkeypatch.setattr(M, "download_bytes", lambda url, **k: f"{remote_sha} {asset}\n".encode())
# Only with the explicit opt-in does the legacy remote-checksum path run.
sha = M.resolve_expected_sha256(pins, "26.3.1", asset, allow_unverified = True)
assert sha == remote_sha
@pytest.mark.parametrize(
"value,expected",
[("1", True), ("true", True), ("YES", True), ("on", True), ("0", False), ("", False)],
)
def test_allow_unverified_node_reads_env(monkeypatch, value, expected):
monkeypatch.setenv(M.ALLOW_UNVERIFIED_ENV, value)
assert M.allow_unverified_node() is expected
def test_install_prebuilt_default_channel_resolves_pinned_version(tmp_path: Path, monkeypatch):
# Default channel installs the pinned version with no index.json round-trip.
pins = M.load_pins()
version = M.pinned_default_version(pins)
install_dir = tmp_path / "node"
install_dir.mkdir()
asset = M.node_asset_name(version, _host("linux", "x64"))
pin = M.pinned_sha256(pins, version, asset) # kept only if the recorded digest is the pin
M.write_metadata(install_dir, version = version, asset = asset, sha256 = pin)
monkeypatch.setattr(M, "detect_host", lambda: _host("linux", "x64"))
monkeypatch.setattr(M, "installed_node_version", lambda d, h: version)
monkeypatch.setattr(M, "installed_npm_major", lambda d, h: 11)
def boom(*a, **k):
raise AssertionError("default channel must not hit nodejs.org when the install matches")
monkeypatch.setattr(M, "fetch_json", boom) # no index.json
monkeypatch.setattr(M, "download_file", boom)
monkeypatch.setattr(M, "download_bytes", boom)
rc = M.install_prebuilt(install_dir, channel = "pinned", min_major = 24, force = False)
assert rc == M.EXIT_SUCCESS
def test_install_prebuilt_failcloses_on_unpinned_latest(tmp_path: Path, monkeypatch):
# Unpinned `latest`, no opt-in, nothing on disk to keep: refuse.
install_dir = tmp_path / "node"
monkeypatch.setattr(M, "detect_host", lambda: _host("linux", "x64"))
monkeypatch.setattr(M, "fetch_json", lambda url: INDEX) # latest overall = 26.3.1 (unpinned)
monkeypatch.delenv(M.ALLOW_UNVERIFIED_ENV, raising = False)
def boom(*a, **k):
raise AssertionError("must not download an unpinned archive")
monkeypatch.setattr(M, "download_file", boom)
with pytest.raises(M.UnpinnedNodeRefused):
M.install_prebuilt(install_dir, channel = "latest", min_major = 24, force = False)
@pytest.mark.parametrize("channel", ["latest", "26.3.1"])
def test_install_prebuilt_unpinned_refusal_does_not_keep_existing(
tmp_path: Path, monkeypatch, channel
):
# Regression: an unpinned refusal must fail closed even with a usable install on
# disk; the keep-existing fallback is for transient failures only.
install_dir = tmp_path / "node"
install_dir.mkdir()
M.write_metadata(install_dir, version = "24.9.0", asset = "old", sha256 = "old")
monkeypatch.setattr(M, "detect_host", lambda: _host("linux", "x64"))
monkeypatch.setattr(M, "fetch_json", lambda url: INDEX) # latest overall = 26.3.1 (unpinned)
monkeypatch.setattr(M, "installed_node_version", lambda d, h: "24.9.0")
monkeypatch.setattr(M, "installed_npm_major", lambda d, h: 11) # existing install is usable
monkeypatch.delenv(M.ALLOW_UNVERIFIED_ENV, raising = False)
def boom(*a, **k):
raise AssertionError("must not download an unpinned archive")
monkeypatch.setattr(M, "download_file", boom)
with pytest.raises(M.UnpinnedNodeRefused):
M.install_prebuilt(install_dir, channel = channel, min_major = 24, force = False)
def test_unpinned_refusal_maps_to_fallback_exit_code(tmp_path: Path, monkeypatch, capsys):
# main() must surface the refusal as EXIT_FALLBACK (setup treats it as a failed
# install with guidance), not as a success masked by the keep-existing path.
install_dir = tmp_path / "node"
install_dir.mkdir()
M.write_metadata(install_dir, version = "24.9.0", asset = "old", sha256 = "old")
monkeypatch.setattr(M, "detect_host", lambda: _host("linux", "x64"))
monkeypatch.setattr(M, "fetch_json", lambda url: INDEX)
monkeypatch.setattr(M, "installed_node_version", lambda d, h: "24.9.0")
monkeypatch.setattr(M, "installed_npm_major", lambda d, h: 11)
monkeypatch.delenv(M.ALLOW_UNVERIFIED_ENV, raising = False)
rc = M.main(["--install-dir", str(install_dir), "--node-version", "latest"])
assert rc == M.EXIT_FALLBACK
# Guard the main() catch order: UnpinnedNodeRefused must be caught before the
# generic PrebuiltFallback, so assert the message, not just the exit code.
out = capsys.readouterr().out
assert "refusing to install Node" in out
assert "prebuilt unavailable" not in out
def test_resolve_expected_sha256_rejects_malformed_pins():
# A structurally-broken manifest is "not pinned" (None), never a bogus digest.
asset = "node-v24.17.0-linux-x64.tar.gz"
assert M.pinned_sha256({"versions": "nope"}, "24.17.0", asset) is None
assert M.pinned_sha256({"versions": {"24.17.0": "nope"}}, "24.17.0", asset) is None
assert M.pinned_sha256({"versions": {"24.17.0": {asset: "x" * 63}}}, "24.17.0", asset) is None
assert M.pinned_sha256({"versions": {"24.17.0": {asset: "z" * 64}}}, "24.17.0", asset) is None
# an uppercase but otherwise valid digest is normalized to lowercase
up = "A" * 64
assert M.pinned_sha256({"versions": {"24.17.0": {asset: up}}}, "24.17.0", asset) == up.lower()
for bad in [{}, {"default_version": ""}, {"default_version": "not-a-version"}]:
with pytest.raises(PrebuiltFallback):
M.pinned_default_version(bad)
def test_install_prebuilt_optin_takes_remote_shasums_path(tmp_path: Path, monkeypatch):
# With the opt-in set, an unpinned version drives the remote-SHASUMS path end to
# end: fetch SHASUMS256.txt, then the verified archive download (no refusal).
install_dir = tmp_path / "node" # nothing on disk -> errors re-raise, not keep-existing
asset = "node-v26.3.1-linux-x64.tar.gz"
monkeypatch.setattr(M, "detect_host", lambda: _host("linux", "x64"))
monkeypatch.setattr(M, "fetch_json", lambda url: INDEX) # latest = 26.3.1 (unpinned)
monkeypatch.setenv(M.ALLOW_UNVERIFIED_ENV, "1")
shasums_fetched = {"n": 0}
def fake_shasums(url, **k):
shasums_fetched["n"] += 1
return f"{'d' * 64} {asset}\n".encode()
class _ReachedDownload(Exception):
pass
def reached(*a, **k):
raise _ReachedDownload
monkeypatch.setattr(M, "download_bytes", fake_shasums)
monkeypatch.setattr(M, "download_file_verified", reached)
with pytest.raises(_ReachedDownload):
M.install_prebuilt(install_dir, channel = "latest", min_major = 24, force = False)
assert shasums_fetched["n"] == 1 # the opt-in path fetched the remote SHASUMS
def test_pins_manifest_ships_next_to_installer():
# The committed manifest must sit beside the installer so __file__ resolution finds it.
assert M.pins_path() == MODULE_PATH.parent / M.PINS_FILENAME
assert M.pins_path().is_file()
def test_pins_manifest_is_declared_in_package_data():
# An unpackaged trust anchor is no trust anchor: a pip install must ship it.
# tomllib is stdlib only on 3.11+; fall back to tomli, else skip on 3.9/3.10.
tomllib = pytest.importorskip("tomllib" if sys.version_info >= (3, 11) else "tomli")
data = tomllib.loads((PACKAGE_ROOT / "pyproject.toml").read_text(encoding = "utf-8"))
studio_globs = data["tool"]["setuptools"]["package-data"]["studio"]
assert M.PINS_FILENAME in studio_globs
def test_existing_install_matches_enforces_expected_sha(tmp_path: Path, monkeypatch):
# The short-circuit must not keep a version-matching install whose recorded digest
# is not the pin (old remote-SHASUMS install or a tampered artifact).
host = _host("linux", "x64")
M.write_metadata(tmp_path, version = "24.17.0", asset = "x", sha256 = "aa")
monkeypatch.setattr(M, "installed_node_version", lambda d, h: "24.17.0")
monkeypatch.setattr(M, "installed_npm_major", lambda d, h: 11)
assert M.existing_install_matches(tmp_path, host, version = "24.17.0") is True # back-compat
assert M.existing_install_matches(tmp_path, host, version = "24.17.0", expected_sha = "aa") is True
assert M.existing_install_matches(tmp_path, host, version = "24.17.0", expected_sha = "bb") is False
def test_install_prebuilt_refuses_existing_unpinned_install(tmp_path: Path, monkeypatch):
# Codex P2: an unpinned version already on disk must still fail closed without the
# opt-in, not be kept by the version-only short-circuit.
install_dir = tmp_path / "node"
install_dir.mkdir()
M.write_metadata(install_dir, version = "26.3.1", asset = "a", sha256 = "s")
monkeypatch.setattr(M, "detect_host", lambda: _host("linux", "x64"))
monkeypatch.setattr(M, "installed_node_version", lambda d, h: "26.3.1")
monkeypatch.setattr(M, "installed_npm_major", lambda d, h: 11)
monkeypatch.delenv(M.ALLOW_UNVERIFIED_ENV, raising = False)
def boom(*a, **k):
raise AssertionError("must not keep or download an unpinned install")
monkeypatch.setattr(M, "download_file", boom)
with pytest.raises(M.UnpinnedNodeRefused):
M.install_prebuilt(install_dir, channel = "26.3.1", min_major = 24, force = False)
def test_pinned_target_wrong_sha_not_kept_when_download_fails(tmp_path: Path, monkeypatch):
# Symmetry with the short-circuit guard: the transient-failure fallback must not
# keep a same-version install whose recorded digest is not the pin. (A different
# usable version is still kept for offline resilience -- covered above.)
host = _host("linux", "x64")
version = M.pinned_default_version(M.load_pins())
asset = M.node_asset_name(version, host)
install_dir = tmp_path / "node"
install_dir.mkdir()
M.write_metadata(install_dir, version = version, asset = asset, sha256 = "0" * 64) # not the pin
monkeypatch.setattr(M, "detect_host", lambda: host)
monkeypatch.setattr(M, "installed_node_version", lambda d, h: version)
monkeypatch.setattr(M, "installed_npm_major", lambda d, h: 11)
monkeypatch.setattr(M, "download_file_verified", _offline) # transient download failure
with pytest.raises(OSError):
M.install_prebuilt(install_dir, channel = "pinned", min_major = 24, force = False)