Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 56 additions & 1 deletion tests/tools/test_find_shell.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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)]
14 changes: 13 additions & 1 deletion tools/environments/local.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down