"""Portable node test-runner gate for the HUX browser suites. Runs ``node --test --experimental-test-coverage`` without the version-gated threshold/include flags (Node >= 22.8 only) and enforces the per-source thresholds by parsing the coverage table, so the same gate passes on the CI image's Node 20 and on newer local Nodes. TypeScript suites need ``--experimental-strip-types`` (Node >= 22.6); on older runtimes they skip with an explicit reason instead of failing on a missing capability. """ from __future__ import annotations import re import subprocess from pathlib import Path import pytest ROOT = Path(__file__).resolve().parents[2] STRIP_TYPES_MINIMUM = (22, 6) ROW = re.compile(r"([^|\s][^|]*?)\s*\|\s*([0-9.]+)\s*\|\s*([0-9.]+)\s*\|\s*([0-9.]+)") METRICS = ("lines", "branches", "functions") def node_version() -> tuple[int, int]: raw = subprocess.run( ["node", "--version"], check=True, capture_output=True, text=True, timeout=15 ).stdout.strip().lstrip("v") major, minor = raw.split(".")[:2] return int(major), int(minor) def run_node_coverage( sources: list[str], tests: list[str], thresholds: dict[str, float], strip_types: bool = False, timeout: int = 60, overrides: dict[str, dict[str, float]] | None = None, ) -> subprocess.CompletedProcess: """Run the node suite and enforce coverage thresholds per source file. ``overrides`` maps a source basename to explicit per-metric floors for documented, pre-existing coverage debt (the old aggregate node gate hid per-file shortfalls); an override still enforces its stated floor. """ unknown = set(thresholds) - set(METRICS) assert not unknown, f"unknown metrics {unknown}" overrides = overrides or {} version = node_version() if strip_types and version < STRIP_TYPES_MINIMUM: pytest.skip( f"node {version[0]}.{version[1]} lacks --experimental-strip-types; " "the TypeScript suites need Node >= 22.6" ) command = ["node", "--test"] if strip_types: command.append("--experimental-strip-types") command += ["--experimental-test-coverage", *tests] result = subprocess.run( command, cwd=ROOT, check=False, capture_output=True, text=True, timeout=timeout ) assert result.returncode == 0, result.stdout + result.stderr rows: dict[str, tuple[float, float, float]] = {} for line in result.stdout.splitlines(): match = ROW.search(line) if match: label = match.group(1).replace("\u2139", " ").strip() name = label.split("/")[-1] rows[name] = tuple(float(match.group(i)) for i in (2, 3, 4)) for source in sources: name = Path(source).name assert name in rows, ( f"no coverage row for {source}\n" + result.stdout + result.stderr ) line_pct, branch_pct, funcs_pct = rows[name] actual = {"lines": line_pct, "branches": branch_pct, "functions": funcs_pct} floors = {**thresholds, **overrides.get(name, {})} for metric, minimum in floors.items(): assert actual[metric] >= minimum, ( f"{source} {metric} coverage {actual[metric]} < {minimum}\n" + result.stdout ) return result