diff --git a/tests/tools/test_find_shell.py b/tests/tools/test_find_shell.py index d9ea4680cf5d4..7cbe54e88e157 100644 --- a/tests/tools/test_find_shell.py +++ b/tests/tools/test_find_shell.py @@ -7,12 +7,17 @@ import os import platform +import shutil import subprocess from unittest.mock import patch import pytest -from tools.environments.local import _find_bash, _find_shell +from tools.environments.local import ( + _find_bash, + _find_shell, + _windows_bash_candidates, +) class TestFindShellPrefersUserShell: @@ -275,3 +280,53 @@ def test_find_shell_selects_working_shell_on_this_box(self, tmp_path): stdin=subprocess.DEVNULL, capture_output=True, text=True, ) assert marker.exists(), f"_find_shell()={shell} swallowed the command" + + +class TestWindowsBashCandidatesExcludeWslStub: + """#103398: the bash.exe stub in the Windows system directory is the WSL + launcher. It passes _bash_starts (it is a working bash *inside WSL*), + but /c/... and /d/... do not exist there, so selecting it makes every + snapshot command fail with ENOENT. The which("bash") fallback must skip + it and let the structured "Git Bash not found" error surface instead.""" + + @staticmethod + def _fake_roots(tmp_path, monkeypatch): + """Point every known Git root and %SystemRoot% at an empty tmp tree.""" + monkeypatch.setenv("LOCALAPPDATA", str(tmp_path / "lad")) + monkeypatch.setenv("ProgramFiles", str(tmp_path / "pf")) + monkeypatch.setenv("ProgramFiles(x86)", str(tmp_path / "pf86")) + monkeypatch.setenv("SystemRoot", str(tmp_path / "sysroot")) + monkeypatch.delenv("WINDIR", raising=False) + + def test_which_fallback_wsl_stub_is_excluded(self, tmp_path, monkeypatch): + self._fake_roots(tmp_path, monkeypatch) + stub = tmp_path / "sysroot" / "System32" / "bash.exe" + stub.parent.mkdir(parents=True) + stub.touch() + monkeypatch.setattr(shutil, "which", lambda _: str(stub)) + + assert _windows_bash_candidates(None) == [] + + def test_git_bash_candidate_kept_while_wsl_stub_dropped( + self, tmp_path, monkeypatch + ): + self._fake_roots(tmp_path, monkeypatch) + git_bash = tmp_path / "git" / "bin" / "bash.exe" + git_bash.parent.mkdir(parents=True) + git_bash.touch() + stub = tmp_path / "sysroot" / "System32" / "bash.exe" + stub.parent.mkdir(parents=True) + stub.touch() + monkeypatch.setenv("HERMES_GIT_BASH_PATH", str(git_bash)) + monkeypatch.setattr(shutil, "which", lambda _: str(stub)) + + assert _windows_bash_candidates(str(git_bash)) == [str(git_bash)] + + def test_path_bash_kept_when_not_the_wsl_stub(self, tmp_path, monkeypatch): + self._fake_roots(tmp_path, monkeypatch) + other_bash = tmp_path / "tools" / "bash.exe" + other_bash.parent.mkdir(parents=True) + other_bash.touch() + monkeypatch.setattr(shutil, "which", lambda _: str(other_bash)) + + assert _windows_bash_candidates(None) == [str(other_bash)] diff --git a/tools/environments/local.py b/tools/environments/local.py index 1e9ef7e977d60..d910b28a74f6e 100644 --- a/tools/environments/local.py +++ b/tools/environments/local.py @@ -359,7 +359,19 @@ def _windows_bash_candidates(custom: "str | None") -> list[str]: raw = [custom or "", *(os.path.join(r, "bash.exe") for r in roots if r)] candidates = list(dict.fromkeys(c for c in raw if c and os.path.isfile(c))) found = shutil.which("bash") - if found and found not in candidates: + # The bash.exe stub in the Windows system directory is the WSL launcher: + # it is a working bash *inside WSL*, where /c/... and /d/... do not exist, + # so selecting it makes every command fail with ENOENT even though the + # probe passes. Exclude it and let the structured "Git Bash not found" + # error surface instead (#103398). Resolve via %SystemRoot% (fallback + # %WINDIR%) so non-C:\ Windows installs are covered too. + wsl_stub = os.path.join( + getenv("SystemRoot") or getenv("WINDIR") or r"C:\Windows", + "System32", "bash.exe", + ) + if (found + and os.path.normcase(found) != os.path.normcase(wsl_stub) + and found not in candidates): candidates.append(found) return candidates