diff --git a/.github/workflows/windows-venv-e2e.yml b/.github/workflows/windows-venv-e2e.yml index f3ed1cd15d..29dcbdea9a 100644 --- a/.github/workflows/windows-venv-e2e.yml +++ b/.github/workflows/windows-venv-e2e.yml @@ -59,4 +59,5 @@ jobs: uv run --no-sync python -m pytest \ tests/hermes_cli/test_venv_holder_windows_live.py \ tests/hermes_cli/test_taskkill_identity_windows_live.py \ + tests/hermes_cli/test_git_trampoline_windows_live.py \ -o addopts= -v -p no:cacheprovider diff --git a/contributors/emails/852938468@qq.com b/contributors/emails/852938468@qq.com new file mode 100644 index 0000000000..8c498e2b43 --- /dev/null +++ b/contributors/emails/852938468@qq.com @@ -0,0 +1 @@ +SayHell0W0rld diff --git a/tests/hermes_cli/test_git_trampoline_windows_live.py b/tests/hermes_cli/test_git_trampoline_windows_live.py new file mode 100644 index 0000000000..e0ace04f0f --- /dev/null +++ b/tests/hermes_cli/test_git_trampoline_windows_live.py @@ -0,0 +1,95 @@ +"""Live Windows E2E for the Git-for-Windows trampoline self-heal (#87876). + +Runs ONLY on a real windows-latest runner (wine2e/** on-demand lane). No +mocked subprocess anywhere: the probes drive the REAL +``_git_is_trampoline`` / ``_locate_real_git`` / ``_ensure_non_trampoline_git`` +helpers against the runner's genuine Git-for-Windows install plus a real +broken-trampoline stand-in (a .bat that reproduces the launcher's +"BUG (fork bomb)" guard output and refuses to run). + +windows-latest ships full Git for Windows at ``C:\\Program Files\\Git`` with +the real ``mingw64\\libexec\\git-core\\git.exe`` — exactly the layout the +locator targets — so the swap path is exercised end to end. +""" + +from __future__ import annotations + +import subprocess +import sys +from pathlib import Path + +import pytest + +pytestmark = pytest.mark.skipif( + sys.platform != "win32", reason="live Windows trampoline E2E" +) + +_FORK_BOMB_BAT = ( + "@echo off\r\n" + "echo BUG (fork bomb): tried to spawn itself, check your PATH 1>&2\r\n" + "exit /b 1\r\n" +) + + +def _make_broken_trampoline(tmp_path: Path) -> Path: + bat = tmp_path / "git.bat" + bat.write_text(_FORK_BOMB_BAT, encoding="utf-8") + return bat + + +class TestGitTrampolineLive: + def test_runner_git_is_not_a_trampoline(self): + from hermes_cli import update_cmd + + # Healthy PATH git on the runner: probe must report False and the + # probe itself must actually run git (sanity: git exists here). + version = subprocess.run( + ["git", "--version"], capture_output=True, text=True, timeout=30 + ) + assert version.returncode == 0, version.stderr + assert update_cmd._git_is_trampoline(["git"]) is False + + def test_broken_trampoline_detected_live(self, tmp_path): + from hermes_cli import update_cmd + + bat = _make_broken_trampoline(tmp_path) + assert update_cmd._git_is_trampoline([str(bat)]) is True + + def test_locate_real_git_finds_runner_git_core(self): + from hermes_cli import update_cmd + + real = update_cmd._locate_real_git() + # windows-latest ships Git for Windows under Program Files with the + # canonical git-core layout the locator searches first. + assert real is not None, "expected git-core git.exe on windows-latest" + assert real.exists() + assert real.name == "git.exe" + probe = subprocess.run( + [str(real), "--version"], capture_output=True, text=True, timeout=30 + ) + assert probe.returncode == 0 + assert "git version" in probe.stdout.lower() + + def test_ensure_non_trampoline_git_swaps_live(self, tmp_path, capsys): + from hermes_cli import update_cmd + + bat = _make_broken_trampoline(tmp_path) + broken_cmd = [str(bat), "-c", "windows.appendAtomically=false"] + healed = update_cmd._ensure_non_trampoline_git(broken_cmd) + + assert healed != broken_cmd, "broken trampoline must be swapped" + assert healed[1:] == broken_cmd[1:], "git config args must survive" + real = Path(healed[0]) + assert real.exists() and real.name == "git.exe" + # The healed command must actually work. + result = subprocess.run( + healed[:1] + ["--version"], capture_output=True, text=True, timeout=30 + ) + assert result.returncode == 0 + assert "switching to real git" in capsys.readouterr().out + + def test_healthy_git_command_untouched_live(self): + from hermes_cli import update_cmd + + git_cmd = ["git", "-c", "windows.appendAtomically=false"] + assert update_cmd._ensure_non_trampoline_git(git_cmd) == git_cmd