"""Behavioral coverage for install_node's post-install version probe (#87460). The probe used to be `installed_ver=$(node --version 2>/dev/null)` under `set -e`: a Node binary that exists but cannot start (Node 26 linux-x64 links libatomic.so.1, missing on minimal Debian/Ubuntu) aborted the whole installer at exit 127 with the loader's explanation discarded — installs died mid-sentence with no output at all. The probe must degrade with a clear error carrying the loader's message, and Debian/Ubuntu installs must attempt libatomic1 up front. """ from __future__ import annotations import io import os import re import subprocess import tarfile from pathlib import Path REPO_ROOT = Path(__file__).resolve().parent.parent INSTALL_SH = REPO_ROOT / "scripts" / "install.sh" def _extract_function(src: str, name: str) -> str: match = re.search(rf"^{name}\(\) \{{\n(.*?)^\}}", src, re.S | re.M) assert match is not None, f"{name}() not found in scripts/install.sh" return match.group(1) def _extract_install_node() -> str: src = INSTALL_SH.read_text(encoding="utf-8") return _extract_function(src, "install_node") def _extract_install_node_line() -> str: """The line-walking refactor split the download/probe body into install_node_line(); the sandboxed driver needs both functions.""" src = INSTALL_SH.read_text(encoding="utf-8") return _extract_function(src, "install_node_line") def _extract_node_satisfies_build() -> str: src = INSTALL_SH.read_text(encoding="utf-8") return _extract_function(src, "node_satisfies_build") def _make_node_tarball(path: Path, node_body: str) -> None: """A .tar.xz-named gzip archive — both bsdtar and modern GNU tar sniff compression by content, so the extension mismatch is irrelevant here.""" root = "node-v26.7.0-linux-x64" with tarfile.open(path, "w:gz") as tar: for name, body, mode in ( (f"{root}/bin/node", node_body, 0o755), (f"{root}/bin/npm", "#!/bin/sh\nexit 0\n", 0o755), (f"{root}/bin/npx", "#!/bin/sh\nexit 0\n", 0o755), ): data = body.encode("utf-8") info = tarfile.TarInfo(name=name) info.size = len(data) info.mode = mode tar.addfile(info, io.BytesIO(data)) def _run_install_node(tmp_path: Path, node_body: str) -> tuple[int, str, str, list[str]]: bin_dir = tmp_path / "bin" home = tmp_path / "home" link_dir = tmp_path / "links" fixture = tmp_path / "fixture.tar.xz" apt_log = tmp_path / "apt-calls" bin_dir.mkdir() home.mkdir() link_dir.mkdir() _make_node_tarball(fixture, node_body) def _stub(name: str, body: str) -> None: stub = bin_dir / name stub.write_text(body, encoding="utf-8") stub.chmod(0o755) _stub( "uname", "#!/bin/sh\n" 'if [ "${1:-}" = "-m" ]; then echo x86_64; else echo Linux; fi\n', ) _stub( "curl", "#!/bin/sh\n" "# Called as: curl -fsSL (stdout -> tarball name)\n" "# curl -fsSL -o \n" "if [ \"${3:-}\" = \"-o\" ]; then\n" ' cp "$FIXTURE" "$4"\n' "else\n" " echo 'node-v26.7.0-linux-x64.tar.xz'\n" "fi\n", ) _stub("sudo", "#!/bin/sh\nshift_if_env() { :; }\nexec env \"$@\"\n") _stub( "apt-get", "#!/bin/sh\nprintf '%s\\n' \"$*\" >> \"$APT_LOG\"\nexit 0\n", ) env = os.environ.copy() env.update( { "FIXTURE": str(fixture), "APT_LOG": str(apt_log), "PATH": f"{bin_dir}:{env['PATH']}", } ) driver = tmp_path / "driver.sh" driver.write_text( "#!/usr/bin/env bash\n" "set -euo pipefail\n" "OS=linux\n" "DISTRO=ubuntu\n" "NODE_VERSION=26\n" f"HERMES_HOME={home}\n" "HAS_NODE=maybe\n" "log_info() { printf 'INFO %s\\n' \"$*\"; }\n" "log_success() { printf 'OK %s\\n' \"$*\"; }\n" "log_warn() { printf 'WARN %s\\n' \"$*\"; }\n" "log_error() { printf 'ERROR %s\\n' \"$*\" >&2; }\n" f"get_command_link_dir() {{ echo {link_dir}; }}\n" "configure_managed_node_npm_prefix() { return 0; }\n" "node_satisfies_build() {\n" f"{_extract_node_satisfies_build()}" "}\n" "install_node_line() {\n" f"{_extract_install_node_line()}" "}\n" "install_node() {\n" f"{_extract_install_node()}" "}\n" "install_node\n" 'printf "HAS_NODE=%s\\n" "$HAS_NODE"\n', encoding="utf-8", ) proc = subprocess.run( ["bash", str(driver)], env=env, capture_output=True, text=True, check=False, ) apt_calls = ( apt_log.read_text(encoding="utf-8").splitlines() if apt_log.exists() else [] ) return proc.returncode, proc.stdout, proc.stderr, apt_calls BROKEN_NODE = ( "#!/bin/sh\n" "echo 'node: error while loading shared libraries: libatomic.so.1: " "cannot open shared object file: No such file or directory' >&2\n" "exit 127\n" ) HEALTHY_NODE = "#!/bin/sh\necho v26.7.0\n" def test_broken_node_degrades_with_clear_error(tmp_path: Path) -> None: code, stdout, stderr, _ = _run_install_node(tmp_path, BROKEN_NODE) # Old behavior: silent abort at exit 127 with no output (#87460). # With the pre-release line-walk (#96601 salvage), a binary that cannot # start now fails the pre-adoption probe (node_satisfies_build on the # extracted tree), so it is rejected BEFORE replacing anything and the # walker reports no usable line — strictly stronger than the old # post-adoption cleanup, which this test previously pinned. assert code == 0, stderr combined = stdout + stderr assert "cannot build native modules" in combined assert "No usable Node.js release line found" in combined assert "HAS_NODE=false" in stdout assert "HAS_NODE=true" not in stdout # The broken candidate must never be adopted: no managed tree, no links. home = tmp_path / "home" link_dir = tmp_path / "links" assert not (home / "node").exists(), "broken managed Node tree left behind" assert not (link_dir / "node").exists(), "broken node symlink left behind" assert not (link_dir / "npm").exists(), "broken npm symlink left behind" # AI-review follow-up: the broken tree and bin links must not linger # — retries and later installer steps resolve `node` cleanly. assert not (home / "node").exists(), "broken managed Node tree left behind" assert not (link_dir / "node").exists(), "broken node symlink left behind" assert not (link_dir / "npm").exists(), "broken npm symlink left behind" def test_healthy_node_reports_success(tmp_path: Path) -> None: code, stdout, stderr, _ = _run_install_node(tmp_path, HEALTHY_NODE) assert code == 0, stderr assert "Node.js v26.7.0 installed" in stdout assert "HAS_NODE=true" in stdout def test_libatomic1_is_preinstalled_on_ubuntu(tmp_path: Path) -> None: _, _, _, apt_calls = _run_install_node(tmp_path, HEALTHY_NODE) assert any("libatomic1" in call for call in apt_calls), apt_calls