diff --git a/hermes_cli/main_install_repair.py b/hermes_cli/main_install_repair.py index 303e447ddc..e4626ae008 100644 --- a/hermes_cli/main_install_repair.py +++ b/hermes_cli/main_install_repair.py @@ -1206,12 +1206,17 @@ def _is_termux_env(env: dict[str, str] | None = None) -> bool: def _is_windows_npm_path(npm_path: str) -> bool: - """True if ``npm_path`` points at a Windows npm shim (WSL ``/mnt/c`` interop, ``.cmd``/``.exe``, UNC). + """True if ``npm_path`` points at a Windows npm shim (WSL drive interop, ``.cmd``/``.exe``, UNC). Callers use this only on a POSIX host — on native Windows ``npm.cmd`` is correct. """ low = npm_path.lower() - return low.endswith((".exe", ".cmd", ".bat")) or low.startswith("/mnt/") or "\\" in npm_path + mount = low.split("/", 3)[2] if low.startswith("/mnt/") else "" + return ( + low.endswith((".exe", ".cmd", ".bat")) + or (len(mount) == 1 and mount.isalpha()) + or "\\" in npm_path + ) def _resolve_node_runtime_npm() -> str | None: @@ -1219,7 +1224,7 @@ def _resolve_node_runtime_npm() -> str | None: On WSL, PATH interop can hand back a Windows npm that fails with EISDIR / symlink errors over ``\\\\wsl.localhost\\...`` UNC paths. Refuse it on a POSIX host and re-scan PATH minus the - ``/mnt/*`` drive mounts. ``None`` when no suitable npm is reachable. + Windows drive mounts. ``None`` when no suitable npm is reachable. On WSL/Linux ``shutil.which("npm")`` may resolve a Windows npm exposed through PATH interop. See #30271. """ @@ -1232,7 +1237,7 @@ def _resolve_node_runtime_npm() -> str | None: if not _is_windows_npm_path(npm): return npm for directory in os.environ.get("PATH", "").split(os.pathsep): - if not directory or directory.lower().startswith("/mnt/"): + if not directory or _is_windows_npm_path(directory): continue candidate = shutil.which("npm", path=directory) if candidate and not _is_windows_npm_path(candidate): diff --git a/tests/hermes_cli/test_node_runtime_npm_resolution.py b/tests/hermes_cli/test_node_runtime_npm_resolution.py new file mode 100644 index 0000000000..e2532701eb --- /dev/null +++ b/tests/hermes_cli/test_node_runtime_npm_resolution.py @@ -0,0 +1,50 @@ +"""Regression coverage for POSIX npm path classification.""" + +from __future__ import annotations + +from unittest.mock import patch + +from hermes_cli.main_install_repair import ( + _is_windows_npm_path, + _resolve_node_runtime_npm, +) + + +def test_windows_npm_path_classifies_only_windows_drive_mounts(): + assert _is_windows_npm_path("/mnt/c/nodejs/npm") + assert not _is_windows_npm_path("/mnt/data/node/bin/npm") + + +def test_resolve_node_runtime_npm_accepts_native_mnt_data_path(): + native_npm = "/mnt/data/node/bin/npm" + with ( + patch("hermes_cli.main._is_windows", return_value=False), + patch("hermes_constants.find_node_executable", return_value=native_npm), + ): + assert _resolve_node_runtime_npm() == native_npm + + +def test_resolve_node_runtime_npm_rejects_windows_drive_mount(monkeypatch): + monkeypatch.setenv("PATH", "/mnt/c/Program Files/nodejs") + with ( + patch("hermes_cli.main._is_windows", return_value=False), + patch( + "hermes_constants.find_node_executable", + return_value="/mnt/c/nodejs/npm", + ), + ): + assert _resolve_node_runtime_npm() is None + + +def test_resolve_node_runtime_npm_rescans_native_mnt_data_path(monkeypatch): + native_npm = "/mnt/data/node/bin/npm" + monkeypatch.setenv("PATH", "/mnt/data/node/bin") + with ( + patch("hermes_cli.main._is_windows", return_value=False), + patch( + "hermes_constants.find_node_executable", + return_value="/mnt/c/nodejs/npm", + ), + patch("hermes_cli.main_install_repair.shutil.which", return_value=native_npm), + ): + assert _resolve_node_runtime_npm() == native_npm