test(scripts): resolve Git Bash instead of the WSL launcher for Windows shell-script tests (#5404)

* test(scripts): resolve Git Bash instead of the WSL launcher for Windows shell-script tests

* test(scripts): pin Windows shell-discovery rules in unit tests and prefer Git's sh.exe for POSIX-sh tests
This commit is contained in:
GGbond 2026-09-14 06:55:57 +08:00 committed by GitHub
parent f66cb8e376
commit b1dad2480a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 381 additions and 35 deletions

View File

@ -0,0 +1,122 @@
"""Locate a shell that can actually run the repo's POSIX shell scripts.
On POSIX hosts ``bash``/``sh`` from PATH are fine. On Windows the suite must
run Git Bash (MSYS2): the WSL launcher at ``%SystemRoot%\\System32\\bash.exe``
and the Microsoft Store alias stubs under ``WindowsApps`` also answer to the
name ``bash`` and CreateProcess searches System32 before PATH, so even a
literal ``["bash", ...]`` argv with Git Bash first on PATH still reaches
WSL but neither can run repo scripts against Windows checkout paths. The
discovery below mirrors what ``scripts/run-with-git-bash.cmd`` already does
for the Makefile.
"""
from __future__ import annotations
import os
import shutil
from pathlib import Path
import pytest
def _script_bash_candidates(git: str | None, program_files: str | None, path_bash: str | None) -> list[Path]:
"""Build the ordered bash candidate list for Windows hosts."""
candidates: list[Path] = []
if git is not None:
# Git for Windows layout: <root>/cmd/git.exe (or <root>/bin/git.exe)
# both resolve to <root>/bin/bash.exe two levels up from git's parent.
candidates.append(Path(git).resolve().parent.parent / "bin" / "bash.exe")
if program_files:
candidates.append(Path(program_files) / "Git" / "bin" / "bash.exe")
if path_bash is not None:
candidates.append(Path(path_bash))
return candidates
def _script_sh_candidates(git: str | None, program_files: str | None) -> list[Path]:
"""Build the ordered POSIX-sh candidate list for Windows hosts.
Git for Windows ships a real ``sh.exe`` at ``<root>/bin/sh.exe`` (and
``<root>/usr/bin/sh.exe``); preferring it over bash lets ``#!/bin/sh``
scripts run under an actual sh, like the POSIX CI legs.
"""
candidates: list[Path] = []
if git is not None:
git_root = Path(git).resolve().parent.parent
candidates.append(git_root / "bin" / "sh.exe")
candidates.append(git_root / "usr" / "bin" / "sh.exe")
if program_files:
program_files_git = Path(program_files) / "Git"
candidates.append(program_files_git / "bin" / "sh.exe")
candidates.append(program_files_git / "usr" / "bin" / "sh.exe")
return candidates
def _first_runnable_shell(candidates: list[Path], system_root: Path) -> str | None:
"""Return the first candidate that is a real shell, rejecting stub launchers.
The WSL launcher lives in System32/SysWOW64 and the Microsoft Store alias
stubs live under WindowsApps; neither is an MSYS2 shell that can run repo
scripts against Windows checkout paths.
"""
rejected_parents = (system_root / "System32", system_root / "SysWOW64")
for candidate in candidates:
try:
resolved = candidate.resolve()
except OSError:
continue
if not resolved.is_file():
continue
if any(parent in resolved.parents for parent in rejected_parents):
continue
if "WindowsApps" in resolved.parts:
continue
return str(resolved)
return None
def find_script_bash() -> str | None:
"""Return a bash able to run the repo's shell scripts, or ``None``."""
if os.name != "nt":
return shutil.which("bash")
candidates = _script_bash_candidates(
git=shutil.which("git"),
program_files=os.environ.get("ProgramFiles"),
path_bash=shutil.which("bash"),
)
system_root = Path(os.environ.get("SystemRoot", r"C:\Windows"))
return _first_runnable_shell(candidates, system_root)
def require_script_bash() -> str:
"""Return :func:`find_script_bash`'s result, skipping the test when absent."""
bash = find_script_bash()
if bash is None:
pytest.skip("repo shell-script tests need Git Bash on Windows")
return bash
def find_posix_sh() -> str | None:
"""Return a shell able to run the repo's POSIX-sh scripts.
Plain ``sh`` on POSIX hosts. On Windows no ``sh`` exists on PATH outside
an MSYS2 installation, so prefer the real ``sh.exe`` shipped with Git for
Windows and fall back to Git Bash when it is absent.
"""
if os.name != "nt":
return shutil.which("sh")
candidates = _script_sh_candidates(
git=shutil.which("git"),
program_files=os.environ.get("ProgramFiles"),
)
system_root = Path(os.environ.get("SystemRoot", r"C:\Windows"))
return _first_runnable_shell(candidates, system_root) or find_script_bash()
def require_posix_sh() -> str:
"""Return :func:`find_posix_sh`'s result, skipping the test when absent."""
sh = find_posix_sh()
if sh is None:
pytest.skip("repo shell-script tests need Git Bash on Windows")
return sh

View File

@ -7,10 +7,16 @@ import os
import tempfile
from pathlib import Path
import pytest
import yaml
from support.shell import find_script_bash
from deerflow.config.app_config import AppConfig
# Only the upgrade-script test shells out; it needs Git Bash on Windows (the
# WSL launcher and Store alias stubs cannot run the repo scripts).
SCRIPT_BASH = find_script_bash()
def _make_config_files(tmpdir: Path, user_config: dict, example_config: dict) -> Path:
"""Write user config.yaml and config.example.yaml to a temp dir, return config path."""
@ -126,6 +132,7 @@ def test_newer_user_version_no_warning(caplog):
assert "outdated" not in caplog.text
@pytest.mark.skipif(SCRIPT_BASH is None, reason="repo shell-script tests need Git Bash on Windows")
def test_version_26_config_upgrades_to_checkpoint_channel_mode(tmp_path, caplog):
"""A v26 user config must be flagged outdated and merge the new persisted field.
@ -158,7 +165,7 @@ def test_version_26_config_upgrades_to_checkpoint_channel_mode(tmp_path, caplog)
env = {**os.environ, "DEER_FLOW_CONFIG_PATH": str(config_path)}
result = subprocess.run(
["bash", str(repo_root / "scripts" / "config-upgrade.sh")],
[SCRIPT_BASH, str(repo_root / "scripts" / "config-upgrade.sh")],
env=env,
capture_output=True,
text=True,

View File

@ -10,8 +10,16 @@ import subprocess
import sys
from pathlib import Path
import pytest
from support.shell import find_script_bash
REPO_ROOT = Path(__file__).resolve().parents[2]
# Every test here shells out to sh/bash; on Windows that must be Git Bash —
# the WSL launcher and Store alias stubs cannot run the repo scripts.
BASH = find_script_bash()
pytestmark = pytest.mark.skipif(BASH is None, reason="repo shell-script tests need Git Bash on Windows")
def _backend_dockerfile_uv_sync_script() -> str:
dockerfile = (REPO_ROOT / "backend" / "Dockerfile").read_text(encoding="utf-8")
@ -41,7 +49,7 @@ def test_backend_dockerfile_expands_multiple_uv_extras(tmp_path):
env["UV_EXTRAS"] = "discord,postgres"
subprocess.run(
["sh", "-c", _backend_dockerfile_uv_sync_script()],
[BASH, "-c", _backend_dockerfile_uv_sync_script()],
cwd=workdir,
env=env,
check=True,
@ -81,7 +89,7 @@ def test_backend_dockerfile_rejects_glob_uv_extra(tmp_path):
env["UV_EXTRAS"] = "postgres,*"
result = subprocess.run(
["sh", "-c", _backend_dockerfile_uv_sync_script()],
[BASH, "-c", _backend_dockerfile_uv_sync_script()],
cwd=workdir,
env=env,
check=False,
@ -121,7 +129,7 @@ def test_deploy_build_auto_detects_postgres_extra_when_other_extras_are_enabled(
env["PATH"] = f"{bin_dir}{os.pathsep}{env['PATH']}"
subprocess.run(
["bash", str(worktree / "scripts" / "deploy.sh"), "build"],
[BASH, str(worktree / "scripts" / "deploy.sh"), "build"],
cwd=worktree,
env=env,
check=True,
@ -167,7 +175,7 @@ def test_deploy_uses_dotenv_without_sourcing_shell_syntax(tmp_path):
env["PATH"] = f"{bin_dir}{os.pathsep}{env['PATH']}"
subprocess.run(
["bash", str(worktree / "scripts" / "deploy.sh"), "build"],
[BASH, str(worktree / "scripts" / "deploy.sh"), "build"],
cwd=worktree,
env=env,
check=True,
@ -179,7 +187,18 @@ def test_deploy_uses_dotenv_without_sourcing_shell_syntax(tmp_path):
assert capture_extras.read_text(encoding="utf-8") == "discord"
args = capture_args.read_text(encoding="utf-8").splitlines()
assert "--env-file" in args
assert str(worktree / ".env") in args
env_file_arg = args[args.index("--env-file") + 1]
# The flag must reference the worktree's .env, but its spelling depends on
# the shell that produced it: Git Bash renders host paths in MSYS form
# (/tmp/... for %TEMP%, /c/... otherwise). Resolve through that same bash
# instead of comparing against this platform's literal path string.
assert env_file_arg.replace("\\", "/").endswith("/.env")
probe = subprocess.run(
[BASH, "-c", 'test -f "$1" && cmp -s "$1" "$2"', "--", env_file_arg, str(worktree / ".env")],
check=False,
capture_output=True,
)
assert probe.returncode == 0, f"--env-file target {env_file_arg!r} is not the worktree .env"
def test_deploy_build_auto_detects_postgres_extra_with_python_fallback(tmp_path):
@ -219,7 +238,7 @@ def test_deploy_build_auto_detects_postgres_extra_with_python_fallback(tmp_path)
env["PATH"] = f"{bin_dir}{os.pathsep}{env['PATH']}"
subprocess.run(
["bash", str(worktree / "scripts" / "deploy.sh"), "build"],
[BASH, str(worktree / "scripts" / "deploy.sh"), "build"],
cwd=worktree,
env=env,
check=True,

View File

@ -9,15 +9,58 @@ same shape — see PR #2767 / Issue #2754.
from __future__ import annotations
import os
import shlex
import shutil
import subprocess
import sys
import tempfile
from collections.abc import Iterator
from pathlib import Path
import pytest
from support.shell import require_posix_sh
REPO_ROOT = Path(__file__).resolve().parents[2]
ENTRYPOINT = REPO_ROOT / "docker" / "dev-entrypoint.sh"
_PYTHON_SHIM_DIR: str | None = None
def _windows_python_shim_dir() -> str | None:
"""Return a bin dir with working python3/python shims (Windows only).
The Microsoft Store alias stubs answer `command -v python3` but exit 49
when exec'd — the failure mode #5179 hardened serve.sh against — which
would send the entrypoint's detector probe to a broken interpreter.
"""
global _PYTHON_SHIM_DIR
if os.name != "nt":
return None
if _PYTHON_SHIM_DIR is None:
shim_dir = tempfile.mkdtemp(prefix="dev-entrypoint-python-shim-")
for name in ("python3", "python"):
shim = Path(shim_dir) / name
# newline="\n": the default newline=None would write CRLF on
# Windows, gluing a stray \r onto the shim's last argument.
shim.write_text(
f'#!/bin/sh\nexec {shlex.quote(sys.executable)} "$@"\n',
encoding="utf-8",
newline="\n",
)
shim.chmod(0o755)
_PYTHON_SHIM_DIR = shim_dir
return _PYTHON_SHIM_DIR
@pytest.fixture(scope="module", autouse=True)
def _cleanup_python_shim_dir() -> Iterator[None]:
"""Remove the session-scoped python shim directory after the module."""
yield
if _PYTHON_SHIM_DIR is not None:
shutil.rmtree(_PYTHON_SHIM_DIR, ignore_errors=True)
def _run(
uv_extras: str | None,
*,
@ -35,8 +78,12 @@ def _run(
env["DEER_FLOW_CONFIG_PATH"] = str(config_path)
if stream_bridge_redis_url is not None:
env["DEER_FLOW_STREAM_BRIDGE_REDIS_URL"] = stream_bridge_redis_url
python_shim_dir = _windows_python_shim_dir()
if python_shim_dir is not None:
env["PATH"] = f"{python_shim_dir}{os.pathsep}{env['PATH']}"
sh = require_posix_sh()
return subprocess.run(
["sh", str(ENTRYPOINT), "--print-extras"],
[sh, str(ENTRYPOINT), "--print-extras"],
cwd=ENTRYPOINT.parent,
env=env,
capture_output=True,
@ -48,7 +95,8 @@ def _run(
def test_entrypoint_script_exists_and_is_posix_sh():
assert ENTRYPOINT.is_file()
# Catch syntax errors before runtime — `sh -n` is a parse-only check.
proc = subprocess.run(["sh", "-n", str(ENTRYPOINT)], capture_output=True, text=True, check=False)
sh = require_posix_sh()
proc = subprocess.run([sh, "-n", str(ENTRYPOINT)], capture_output=True, text=True, check=False)
assert proc.returncode == 0, proc.stderr
@ -218,7 +266,8 @@ def _run_sync_block(tmp_path: Path, stub_uv: str) -> subprocess.CompletedProcess
bin_dir = tmp_path / "bin"
bin_dir.mkdir()
uv_stub = bin_dir / "uv"
uv_stub.write_text(stub_uv, encoding="utf-8")
# newline="\n": keep the stub POSIX-sh clean on Windows (no stray \r).
uv_stub.write_text(stub_uv, encoding="utf-8", newline="\n")
uv_stub.chmod(0o755)
state_dir = tmp_path / "state"
@ -231,7 +280,7 @@ def _run_sync_block(tmp_path: Path, stub_uv: str) -> subprocess.CompletedProcess
env = os.environ.copy()
env["PATH"] = f"{bin_dir}{os.pathsep}{env['PATH']}"
env["STUB_UV_STATE"] = str(state_dir)
return subprocess.run(["sh", "-c", script], capture_output=True, text=True, check=False, env=env, cwd=tmp_path)
return subprocess.run([require_posix_sh(), "-c", script], capture_output=True, text=True, check=False, env=env, cwd=tmp_path)
def test_successful_sync_reaches_the_uvicorn_handoff(tmp_path: Path):

View File

@ -7,23 +7,16 @@ import shutil
import subprocess
import tempfile
from pathlib import Path
from shutil import which
import pytest
from support.shell import find_script_bash
REPO_ROOT = Path(__file__).resolve().parents[2]
SCRIPT_PATH = REPO_ROOT / "scripts" / "docker.sh"
BASH_CANDIDATES = [
Path(r"C:\Program Files\Git\bin\bash.exe"),
Path(which("bash")) if which("bash") else None,
]
BASH_EXECUTABLE = next(
(str(path) for path in BASH_CANDIDATES if path is not None and path.exists() and "WindowsApps" not in str(path)),
None,
)
BASH_EXECUTABLE = find_script_bash()
if BASH_EXECUTABLE is None:
pytestmark = pytest.mark.skip(reason="bash is required for docker.sh detection tests")
pytestmark = pytest.mark.skip(reason="Git Bash is required for docker.sh detection tests")
def _detect_mode_with_config(config_content: str) -> str:

View File

@ -7,8 +7,16 @@ import shutil
import subprocess
from pathlib import Path
import pytest
from support.shell import find_script_bash
REPO_ROOT = Path(__file__).resolve().parents[2]
# Only the two deploy.sh tests shell out; they need Git Bash on Windows (the
# WSL launcher and Store alias stubs cannot run the repo scripts).
SCRIPT_BASH = find_script_bash()
requires_script_bash = pytest.mark.skipif(SCRIPT_BASH is None, reason="repo shell-script tests need Git Bash on Windows")
def _read(path: str) -> str:
return (REPO_ROOT / path).read_text(encoding="utf-8")
@ -62,6 +70,7 @@ def test_production_gateway_has_a_real_readiness_probe() -> None:
assert "gateway:\n condition: service_healthy" in compose
@requires_script_bash
def test_deploy_waits_for_gateway_readiness_before_success(tmp_path: Path) -> None:
capture = tmp_path / "docker-args.txt"
worktree, env = _deploy_fixture(
@ -71,7 +80,7 @@ def test_deploy_waits_for_gateway_readiness_before_success(tmp_path: Path) -> No
env["CAPTURE_DOCKER_ARGS"] = str(capture)
result = subprocess.run(
["bash", str(worktree / "scripts" / "deploy.sh"), "start"],
[SCRIPT_BASH, str(worktree / "scripts" / "deploy.sh"), "start"],
cwd=worktree,
env=env,
check=False,
@ -86,6 +95,7 @@ def test_deploy_waits_for_gateway_readiness_before_success(tmp_path: Path) -> No
assert "DeerFlow is running!" in result.stdout
@requires_script_bash
def test_deploy_failure_prints_gateway_diagnostics_and_never_claims_success(tmp_path: Path) -> None:
capture = tmp_path / "docker-calls.txt"
worktree, env = _deploy_fixture(
@ -95,7 +105,7 @@ def test_deploy_failure_prints_gateway_diagnostics_and_never_claims_success(tmp_
env["CAPTURE_DOCKER_CALLS"] = str(capture)
result = subprocess.run(
["bash", str(worktree / "scripts" / "deploy.sh"), "start"],
[SCRIPT_BASH, str(worktree / "scripts" / "deploy.sh"), "start"],
cwd=worktree,
env=env,
check=False,

View File

@ -3,11 +3,11 @@
from __future__ import annotations
import shlex
import shutil
import subprocess
from pathlib import Path
import pytest
from support.shell import require_script_bash
REPO_ROOT = Path(__file__).resolve().parents[2]
SERVE_SH = REPO_ROOT / "scripts" / "serve.sh"
@ -33,12 +33,10 @@ def _is_repo_nginx_pid(
*,
command: str,
args: str,
repo_root: Path,
repo_root: Path | str,
deerflow_pid: bool = False,
) -> bool:
bash = shutil.which("bash")
if bash is None:
pytest.skip("bash is required to exercise serve.sh helpers")
bash = require_script_bash()
function = _extract_shell_function("_is_repo_nginx_pid")
script = f"""
@ -69,8 +67,12 @@ _is_repo_nginx_pid 12345
def test_repo_nginx_pid_accepts_macos_rewritten_master_command(tmp_path):
repo_root = tmp_path / "deer-flow"
nginx_conf = repo_root / "docker" / "nginx" / "nginx.local.conf"
# The simulated macOS ps line embeds POSIX-form paths no matter which host
# runs this test, so build the fixture with forward slashes explicitly;
# on Windows a Path would render with backslashes and never match the
# "$root"/docker/nginx/... pattern the shell function greps for.
repo_root = (tmp_path / "deer-flow").as_posix()
nginx_conf = f"{repo_root}/docker/nginx/nginx.local.conf"
assert _is_repo_nginx_pid(
command=f"nginx: master process /opt/homebrew/bin/nginx -c {nginx_conf}",

View File

@ -5,11 +5,10 @@ python alias stubs pass Bash's own PATH lookup but cannot be exec'd through
from __future__ import annotations
import shlex
import shutil
import subprocess
from pathlib import Path
import pytest
from support.shell import require_script_bash
REPO_ROOT = Path(__file__).resolve().parents[2]
SERVE_SH = REPO_ROOT / "scripts" / "serve.sh"
@ -69,9 +68,7 @@ def _to_bash_path(path: Path) -> str:
def _run_pick_python(tmp_path: Path, *, env_mock: str = "") -> subprocess.CompletedProcess:
bash = shutil.which("bash")
if bash is None:
pytest.skip("bash is required to exercise serve.sh helpers")
bash = require_script_bash()
script = _SCRIPT_TEMPLATE.replace("__BIN__", shlex.quote(_to_bash_path(tmp_path / "bin"))).replace("__STUBS__", "python3 python py").replace("__ENV_MOCK__", env_mock).replace("__FUNCTION__", _extract_shell_function("_pick_python"))
# errors="replace": bash's diagnostics may arrive in the console's code

View File

@ -0,0 +1,147 @@
"""Unit tests for the Windows shell discovery in tests/support/shell.py.
``find_script_bash``/``find_posix_sh`` return at the ``os.name != "nt"``
early-out on POSIX CI, so the candidate-rejection rules would otherwise only
ever run on contributors' Windows machines -- exactly the failure mode the
discovery exists to fix. The candidate construction and the rejection scan are
pinned here against real files (``tmp_path``) with faked inputs, so the logic
is exercised on every CI leg.
"""
from __future__ import annotations
import os
from pathlib import Path
import pytest
from support import shell
def _touch(path: Path) -> Path:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text("", encoding="utf-8")
return path
@pytest.fixture()
def fake_windows(tmp_path: Path) -> Path:
"""A fake Windows layout: git root, ProgramFiles Git, WSL + Store stubs."""
_touch(tmp_path / "git" / "cmd" / "git.exe")
_touch(tmp_path / "git" / "bin" / "bash.exe")
_touch(tmp_path / "git" / "bin" / "sh.exe")
_touch(tmp_path / "git" / "usr" / "bin" / "sh.exe")
_touch(tmp_path / "ProgramFiles" / "Git" / "bin" / "bash.exe")
_touch(tmp_path / "ProgramFiles" / "Git" / "usr" / "bin" / "sh.exe")
_touch(tmp_path / "Windows" / "System32" / "bash.exe") # WSL launcher
_touch(tmp_path / "Windows" / "SysWOW64" / "bash.exe") # 32-bit WSL launcher
_touch(tmp_path / "WindowsApps" / "bash.exe") # Microsoft Store alias stub
return tmp_path
def _system_root(root: Path) -> Path:
return root / "Windows"
def test_system32_bash_rejected_even_when_only_path_hit(fake_windows: Path):
wsl_launcher = fake_windows / "Windows" / "System32" / "bash.exe"
result = shell._first_runnable_shell([wsl_launcher], _system_root(fake_windows))
assert result is None
def test_syswow64_bash_rejected(fake_windows: Path):
syswow64 = fake_windows / "Windows" / "SysWOW64" / "bash.exe"
result = shell._first_runnable_shell([syswow64], _system_root(fake_windows))
assert result is None
def test_windowsapps_store_stub_rejected(fake_windows: Path):
store_stub = fake_windows / "WindowsApps" / "bash.exe"
result = shell._first_runnable_shell([store_stub], _system_root(fake_windows))
assert result is None
def test_rejected_candidates_fall_through_to_real_shell(fake_windows: Path):
wsl_launcher = fake_windows / "Windows" / "System32" / "bash.exe"
git_bash = fake_windows / "git" / "bin" / "bash.exe"
result = shell._first_runnable_shell([wsl_launcher, git_bash], _system_root(fake_windows))
assert result == str(git_bash.resolve())
def test_missing_candidates_are_skipped(fake_windows: Path):
missing = fake_windows / "no-such-dir" / "bash.exe"
git_bash = fake_windows / "git" / "bin" / "bash.exe"
result = shell._first_runnable_shell([missing, git_bash], _system_root(fake_windows))
assert result == str(git_bash.resolve())
def test_no_runnable_candidate_returns_none(fake_windows: Path):
missing = fake_windows / "no-such-dir" / "bash.exe"
wsl_launcher = fake_windows / "Windows" / "System32" / "bash.exe"
result = shell._first_runnable_shell([missing, wsl_launcher], _system_root(fake_windows))
assert result is None
def test_git_derived_bash_candidate_comes_first(fake_windows: Path):
git_exe = str(fake_windows / "git" / "cmd" / "git.exe")
path_bash = str(fake_windows / "elsewhere" / "bash.exe")
candidates = shell._script_bash_candidates(git=git_exe, program_files=None, path_bash=path_bash)
git_root = (fake_windows / "git" / "cmd" / "git.exe").resolve().parent.parent
assert candidates == [git_root / "bin" / "bash.exe", Path(path_bash)]
def test_program_files_bash_used_when_git_absent(fake_windows: Path):
program_files = fake_windows / "ProgramFiles"
candidates = shell._script_bash_candidates(git=None, program_files=str(program_files), path_bash=None)
assert candidates == [program_files / "Git" / "bin" / "bash.exe"]
def test_posix_sh_prefers_git_sh_exe(fake_windows: Path):
git_exe = str(fake_windows / "git" / "cmd" / "git.exe")
candidates = shell._script_sh_candidates(git=git_exe, program_files=None)
git_root = (fake_windows / "git" / "cmd" / "git.exe").resolve().parent.parent
assert candidates == [
git_root / "bin" / "sh.exe",
git_root / "usr" / "bin" / "sh.exe",
]
def test_posix_sh_falls_back_to_program_files(fake_windows: Path):
program_files = fake_windows / "ProgramFiles"
candidates = shell._script_sh_candidates(git=None, program_files=str(program_files))
assert candidates == [
program_files / "Git" / "bin" / "sh.exe",
program_files / "Git" / "usr" / "bin" / "sh.exe",
]
@pytest.mark.skipif(os.name != "nt", reason="real Windows environment reads")
def test_find_script_bash_never_returns_wsl_or_store_stubs():
"""On a real Windows host the result must not be a stub launcher."""
result = shell.find_script_bash()
if result is None:
pytest.skip("no Git Bash installed on this machine")
assert "WindowsApps" not in Path(result).parts
system_root = Path(os.environ.get("SystemRoot", r"C:\Windows"))
assert system_root / "System32" not in Path(result).parents
assert system_root / "SysWOW64" not in Path(result).parents