"""The manifest's ``engines`` must be satisfiable by a toolchain we can actually ship. `engine-strict=true` in `.npmrc` makes `engines` a hard gate on every `npm ci` / `npm install` — the installer's workspace step, `hermes update`'s dependency refresh, and CI alike. So a floor nobody's toolchain can meet is not a strict-hygiene win; it is a total install outage. That is exactly what happened: `engines.npm` was raised to `>=12.0.0` while **no Node release bundles npm 12** (Node 26 ships 11.17.0, 24 ships 11.16.0, 22 ships 10.9.8). Every fresh install died at the first `npm ci`, and `hermes update` left installs in a mixed state. These tests encode the invariants that would have caught it. Deliberately behavioral, not a snapshot: nothing here pins a version we expect to change. Each test asserts a *relationship* — between the floor we declare and the toolchain that has to satisfy it. """ from __future__ import annotations import json from pathlib import Path import pytest REPO_ROOT = Path(__file__).resolve().parents[1] # npm releases bundled with a Node major, newest-per-major. Not a catalog # snapshot: the point is that *some* real, shipping toolchain must clear the # floor, and these are the ones users actually arrive with. _STOCK_NPM_BY_NODE_MAJOR = { 20: "10.8.2", 22: "10.9.8", 24: "11.16.0", 26: "11.17.0", } def _root_manifest() -> dict: return json.loads((REPO_ROOT / "package.json").read_text()) def _parse_major_minor_patch(version: str) -> tuple[int, int, int]: parts = version.split("-", 1)[0].split(".") nums = [int(p) for p in parts[:3]] while len(nums) < 3: nums.append(0) return nums[0], nums[1], nums[2] def _satisfies_clause(version: str, clause: str) -> bool: """Evaluate one `>=x.y.z` / `=x.y.z within the same major (x > 0). return have[0] == want[0] and have >= want for op in (">=", "<=", "<", ">", "="): if clause.startswith(op): bound = clause[len(op) :].strip() break else: op, bound = "=", clause have = _parse_major_minor_patch(version) want = _parse_major_minor_patch(bound) if op == ">=": return have >= want if op == "<=": return have <= want if op == "<": return have < want if op == ">": return have > want return have == want def _satisfies_range(version: str, spec: str) -> bool: """Evaluate the `A || B` / space-joined-AND subset of semver we author.""" for alternative in spec.split("||"): clauses = [c for c in alternative.strip().split() if c] if clauses and all(_satisfies_clause(version, c) for c in clauses): return True return False class TestEnginesAreSatisfiable: def test_npm_floor_is_met_by_a_shipping_node(self): """Some stock Node must bundle an npm our floor accepts. Without this, a fresh install cannot run `npm ci` at all: the installer provisions a Node from nodejs.org and immediately uses the npm that came with it. """ npm_range = _root_manifest()["engines"]["npm"] satisfying = { major: npm for major, npm in _STOCK_NPM_BY_NODE_MAJOR.items() if _satisfies_range(npm, npm_range) } assert satisfying, ( f"engines.npm is {npm_range!r}, which no shipping Node bundles " f"(checked {_STOCK_NPM_BY_NODE_MAJOR}). With engine-strict=true " "every fresh install fails at the first `npm ci`." ) def test_node_floor_is_met_by_the_managed_runtime(self): """The Node major the installers provision must clear engines.node.""" node_range = _root_manifest()["engines"]["node"] install_sh = (REPO_ROOT / "scripts" / "install.sh").read_text() for line in install_sh.splitlines(): if line.startswith("NODE_VERSION="): managed_major = int(line.split("=", 1)[1].strip().strip('"').strip("'")) break else: # pragma: no cover - install.sh always defines it pytest.fail("install.sh does not define NODE_VERSION") # install.sh fetches latest-v{major}.x, not {major}.0.0. Use a high # representative release from that major so ranges that enumerate LTS # lines (rather than one continuous floor) are checked correctly. managed_release = f"{managed_major}.999.999" assert _satisfies_range(managed_release, node_range), ( f"engines.node is {node_range!r} but install.sh provisions Node " f"{managed_major}.x. The runtime we ship must satisfy the floor we " "declare, or the install we just performed cannot install deps." ) def test_managed_node_bundles_an_npm_the_engines_accept(self): """The Node major install.sh fetches must ship an npm that clears engines.npm. Node 22 bundles 11.16.0, which is in the excluded 11.10–11.16 band — fresh Hermes-managed installs then die at `npm ci` with EBADENGINE (#80769). """ npm_range = _root_manifest()["engines"]["npm"] install_sh = (REPO_ROOT / "scripts" / "install.sh").read_text() for line in install_sh.splitlines(): if line.startswith("NODE_VERSION="): managed_major = int(line.split("=", 1)[1].strip().strip('"').strip("'")) break else: # pragma: no cover pytest.fail("install.sh does not define NODE_VERSION") stock_npm = _STOCK_NPM_BY_NODE_MAJOR.get(managed_major) assert stock_npm is not None, ( f"install.sh NODE_VERSION={managed_major} is not in the known " f"stock map {_STOCK_NPM_BY_NODE_MAJOR}" ) assert _satisfies_range(stock_npm, npm_range), ( f"install.sh provisions Node {managed_major}.x (stock npm " f"{stock_npm}), but engines.npm is {npm_range!r}. A fresh " "Hermes-managed install cannot run npm ci." ) def test_desktop_node_floor_is_not_stricter_than_its_toolchain(self): """apps/desktop must not demand more Node than its own build tools do. Vite is the real constraint (it needs `node:util.styleText`). Raising the desktop floor beyond it silently force-migrates every user's toolchain for no dependency reason. """ desktop = json.loads((REPO_ROOT / "apps" / "desktop" / "package.json").read_text()) node_range = desktop["engines"]["node"] # The tightest floor any dependency actually declares (react-router # 8.3.0 -> >=22.22.0). If this legitimately rises, the assertion # documents the reason for the bump rather than blocking it. assert _satisfies_range("22.22.0", node_range), ( f"apps/desktop engines.node is {node_range!r}, which rejects Node " "22.12 — stricter than Vite requires. A desktop floor above the " "build toolchain's own floor replaces working user toolchains for " "nothing." ) class TestExcludedNpmBand: """npm 11.10–11.16 honor `min-release-age` but ignore `min-release-age-exclude`. `.npmrc` sets both, so that band applies the 14-day age gate to packages we deliberately exempted and installs fail with ETARGET. The floor must keep excluding them. """ @pytest.mark.parametrize("bad_npm", ["11.10.0", "11.12.1", "11.16.0"]) def test_band_that_ignores_the_exclude_list_is_rejected(self, bad_npm): npm_range = _root_manifest()["engines"]["npm"] assert not _satisfies_range(bad_npm, npm_range), ( f"engines.npm {npm_range!r} accepts npm {bad_npm}, which supports " "min-release-age but not min-release-age-exclude — it will fail " "ETARGET on any freshly published dependency in .npmrc's exclude list." ) @pytest.mark.parametrize("good_npm", ["10.9.8", "11.17.0", "12.0.2"]) def test_versions_handling_the_exclude_list_are_accepted(self, good_npm): npm_range = _root_manifest()["engines"]["npm"] assert _satisfies_range(good_npm, npm_range), ( f"engines.npm {npm_range!r} rejects npm {good_npm}, which handles " ".npmrc correctly and should be usable." ) class TestManifestMirrors: def test_lockfile_engines_match_the_manifest(self): """A stale lockfile mirror re-imposes the old floor on `npm ci`.""" manifest = _root_manifest()["engines"] lock = json.loads((REPO_ROOT / "package-lock.json").read_text()) assert lock["packages"][""]["engines"] == manifest def _normalize_range(spec: str) -> str: """Normalize the wilder styles real deps publish so our tiny evaluator can read them: collapse space after operators (``">= 10"``), drop ``v`` prefixes (``">=v12.22.7"``), and rewrite ``x``/``*`` wildcards to floors. """ import re spec = re.sub(r"(>=|<=|>|<|\^|~|=)\s+", r"\1", spec) spec = re.sub(r"(>=|<=|>|<|\^|~|=)v", r"\1", spec) # "6.x" / "10.*" -> "^6.0.0"-ish floor within the major; ">= 10.*" -> ">=10.0.0" spec = re.sub(r"(\d+)\.[x*](?:\.[x*])?", r"\1.0.0", spec) return spec class TestDeclaredFloorsClearTheLockedTree: """Every Node version our own gates accept must survive `npm ci`. The class of outage this pins: the installers' version gates (node_satisfies_build in install.sh, Test-NodeVersionOk in install.ps1) and `engines.node` are hand-maintained, while the *real* floor is whatever the strictest locked dependency demands. When they drift, a user's system Node clears every gate we own and then dies at `npm install` with EBADENGINE under engine-strict=true. Aug 2026 instance: @babel/* 8.x requires `^22.18.0 || >=24.11.0`; our engines arm said `^24.0.0`, so Node 24.4 passed the installer and the manifest and failed on 28 babel packages. """ def _arm_floors(self, node_range: str) -> list[str]: floors = [] for arm in node_range.split("||"): arm = arm.strip() for op in ("^", ">=", "="): if arm.startswith(op): floors.append(arm[len(op):].strip()) break else: floors.append(arm) return floors def _locked_node_ranges(self) -> dict[str, str]: lock = json.loads((REPO_ROOT / "package-lock.json").read_text()) ranges: dict[str, str] = {} for path, meta in lock["packages"].items(): engines = meta.get("engines") if not isinstance(engines, dict): continue node_range = engines.get("node") if isinstance(node_range, str) and node_range.strip() not in ("", "*"): ranges.setdefault(node_range, path) return ranges def test_every_engines_arm_floor_clears_every_locked_dependency(self): node_range = _root_manifest()["engines"]["node"] violations = [] for floor in self._arm_floors(node_range): for dep_range, example in self._locked_node_ranges().items(): if not _satisfies_range(floor, _normalize_range(dep_range)): violations.append((floor, dep_range, example)) assert not violations, ( "engines.node arms admit Node versions the locked dependency " "tree rejects — those users pass every install gate and then " "die at `npm install` with EBADENGINE (engine-strict=true). " "Raise the arm floor (and the installer gates: " "node_satisfies_build in scripts/install.sh, Test-NodeVersionOk " f"in scripts/install.ps1) or relax the dep. Violations: {violations}" ) def test_installer_gates_match_the_manifest_arms(self): """install.sh's node_satisfies_build must encode the same floors as engines.node — a laxer gate accepts a Node that npm then rejects.""" node_range = _root_manifest()["engines"]["node"] install_sh = (REPO_ROOT / "scripts" / "install.sh").read_text() install_ps1 = (REPO_ROOT / "scripts" / "install.ps1").read_text() for arm in node_range.split("||"): arm = arm.strip() major, minor = _parse_major_minor_patch(arm.lstrip("^>="))[:2] if arm.startswith("^") and minor > 0: sh_gate = f'[ "$major" -eq {major} ] && [ "$minor" -ge {minor} ]' ps1_gate = f"if ($v.Major -eq {major}) {{ return ($v.Minor -ge {minor}) }}" assert sh_gate in install_sh, ( f"engines.node arm {arm!r} has no matching gate in " f"install.sh node_satisfies_build (expected: {sh_gate})" ) assert ps1_gate in install_ps1, ( f"engines.node arm {arm!r} has no matching gate in " f"install.ps1 Test-NodeVersionOk (expected: {ps1_gate})" )