Files
computer-playbook/tests/unit/python/utils/install/test_npm.py
kevinveenbirkenbachandClaude Opus 5.5 30268cf552 fix(install): replace an end-of-life distro Node.js with the current LTS
Debian trixie ships nodejs 20.19, whose release line reached its end of
life on 2026-04-30. `@mermaid-js/mermaid-cli` 12 and its puppeteer
declare `node >=22.13.0`, so `make install-lint` failed in every fresh
Debian stack container with `All providers failed for
chrome-headless-shell` (#1088).

`ensure_node_present()` still installs the distro package first, then
reads the release line of `node` on PATH and looks it up in the
nodejs/Release schedule. A line past its `end` date is replaced by the
newest LTS from nodejs.org/dist/index.json: SHASUMS256.txt is verified
with gpgv against the active Node.js release keys from
nodejs/release-keys, the tarball against its SHA-256 entry, and the
release is unpacked into /usr/local, after which `node` on PATH must be
that release. Nothing pins a major: the next LTS line is picked up the
same way. Ubuntu 26.04's Node.js 22, supported until 2027-04-30, stays.

`ensure_npm_present()` calls it first, so a container that already holds
the end-of-life node is upgraded too rather than skipped because npm
exists. The call runs once per process.

Closes #1088

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-21 16:02:52 +02:00

103 lines
3.8 KiB
Python

"""Unit tests for :mod:`utils.install.npm`."""
from __future__ import annotations
import subprocess
import unittest
import unittest.mock as mock
from utils.install import npm as npm_mod
class TestEnsureNpmPresent(unittest.TestCase):
def setUp(self) -> None:
patcher = mock.patch("utils.install.node.ensure_node_present")
self.ensure_node = patcher.start()
self.addCleanup(patcher.stop)
def test_provides_a_supported_node_first(self) -> None:
with (
mock.patch.object(npm_mod.shutil, "which", return_value="/usr/bin/npm"),
mock.patch("utils.install.system_pkg.install_command_via_pkg"),
):
npm_mod.ensure_npm_present()
self.ensure_node.assert_called_once_with()
def test_present_noop(self) -> None:
with (
mock.patch.object(npm_mod.shutil, "which", return_value="/usr/bin/npm"),
mock.patch(
"utils.install.system_pkg.install_command_via_pkg"
) as install_pkg,
):
npm_mod.ensure_npm_present()
install_pkg.assert_not_called()
def test_installs_via_system_pkg(self) -> None:
whiches = iter([None, "/usr/bin/npm"])
with (
mock.patch.object(
npm_mod.shutil, "which", side_effect=lambda _x: next(whiches)
),
mock.patch(
"utils.install.system_pkg.install_command_via_pkg"
) as install_pkg,
):
npm_mod.ensure_npm_present()
install_pkg.assert_called_once_with("npm")
def test_raises_when_still_missing(self) -> None:
with (
mock.patch.object(npm_mod.shutil, "which", return_value=None),
mock.patch("utils.install.system_pkg.install_command_via_pkg"),
):
self.assertRaises(RuntimeError, npm_mod.ensure_npm_present)
class TestNpmInstallGlobal(unittest.TestCase):
def test_global_success(self) -> None:
with (
mock.patch.object(npm_mod, "ensure_npm_present"),
mock.patch.object(npm_mod, "install_with_optional_sudo") as installer,
):
npm_mod.npm_install_global("markdownlint-cli2")
installer.assert_called_once_with(["npm", "install", "-g", "markdownlint-cli2"])
def test_fallback_to_prefix(self) -> None:
err = subprocess.CalledProcessError(returncode=1, cmd=["npm"])
with (
mock.patch.object(npm_mod, "ensure_npm_present"),
mock.patch.object(npm_mod, "install_with_optional_sudo", side_effect=err),
mock.patch.object(npm_mod.Path, "mkdir"),
mock.patch.object(npm_mod, "ensure_dir_on_path"),
mock.patch.object(npm_mod.subprocess, "run") as run,
):
npm_mod.npm_install_global("markdownlint-cli2")
called_cmd = run.call_args.args[0]
self.assertIn("--prefix", called_cmd)
self.assertEqual(called_cmd[-1], "markdownlint-cli2")
class TestNpmInstallLocalInRepo(unittest.TestCase):
def test_uses_ci_when_lockfile_present(self) -> None:
with (
mock.patch.object(npm_mod, "ensure_npm_present"),
mock.patch.object(npm_mod.Path, "is_file", return_value=True),
mock.patch.object(npm_mod.subprocess, "run") as run,
):
npm_mod.npm_install_local_in_repo("/repo")
self.assertEqual(run.call_args.args[0][:2], ["npm", "ci"])
def test_uses_install_without_lockfile(self) -> None:
with (
mock.patch.object(npm_mod, "ensure_npm_present"),
mock.patch.object(npm_mod.Path, "is_file", return_value=False),
mock.patch.object(npm_mod.subprocess, "run") as run,
):
npm_mod.npm_install_local_in_repo("/repo")
self.assertEqual(run.call_args.args[0][:2], ["npm", "install"])
if __name__ == "__main__":
unittest.main()