890bbbda1fresolved modify/delete conflicts as main's side, bringing back tests pm-clean had deleted or trimmed.427d4936c2cleaned up most of that fallout; these were still broken: - test_update_yes_flag.py, test_update_venv_ownership_preflight.py: import hermes_cli.update_cmd_deps, which PM removed (the first was also cut by the pm-test-audit mergef8bdc96033). - test_ssl_ca_guard.py: imports SSLConfigurationError, removed with the TLS refactor2f20ceb6f7. - test_cmd_update.py: main's 37-test version needs main's isolated_update_runtime fixture and drives the old updater (lazy tool restore, Termux uv bootstrap, npm lockfile cache); back to pm-clean's audited 5 tests. - test_tools_config.py: one import of the retired AGENT_BROWSER_NPX_SPEC failed the whole file, and main's twelve npx/Chromium post-setup tests reached a real `pm.ensure("agent-browser")` download. TestAgentBrowserPostSetup is pm-clean's own class again (pm.ensure stubbed); the Browser Use CLI contract test stays, with pm.ensure stubbed too.
130 lines
4.8 KiB
Python
130 lines
4.8 KiB
Python
"""Git trampoline recovery; branch updates use the real target-identity suite."""
|
|
|
|
import subprocess
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
|
|
from hermes_cli import update_cmd
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _isolate_venv_holders(monkeypatch):
|
|
"""The update flow's venv-holder guard sees the live gateway processes on
|
|
a dev machine and aborts with SystemExit 2 before reaching the branch
|
|
logic under test. Isolate it so the test exercises the intended path."""
|
|
monkeypatch.setattr("hermes_cli.update_cmd_windows._detect_venv_python_processes", lambda: [])
|
|
|
|
|
|
class TestGitTrampolineSelfHeal:
|
|
"""Proactive Git-for-Windows trampoline self-heal (#87876).
|
|
|
|
A broken bin\\git.exe / cmd\\git.exe shim (~46KB) refuses every git call
|
|
with a "BUG (fork bomb)" guard instead of re-execing the real git-core
|
|
binary. _ensure_non_trampoline_git detects this up front and swaps in a
|
|
real git binary when one can be located, so the normal git update path
|
|
survives instead of degrading to the ZIP fallback.
|
|
"""
|
|
|
|
@staticmethod
|
|
def _fake_run_healthy(command, **_kwargs):
|
|
return subprocess.CompletedProcess(
|
|
command, 0, stdout="git version 2.50.0.windows.1\n", stderr=""
|
|
)
|
|
|
|
@staticmethod
|
|
def _fake_run_trampoline(command, **_kwargs):
|
|
return subprocess.CompletedProcess(
|
|
command,
|
|
1,
|
|
stdout="",
|
|
stderr="BUG (fork bomb): tried to spawn itself, check your PATH\n",
|
|
)
|
|
|
|
@pytest.mark.platforms("windows")
|
|
def test_healthy_git_command_unchanged(self):
|
|
from hermes_cli import update_cmd
|
|
|
|
git_cmd = ["git", "-c", "windows.appendAtomically=false"]
|
|
with (
|
|
patch(
|
|
"hermes_cli.update_cmd.subprocess.run",
|
|
side_effect=self._fake_run_healthy,
|
|
),
|
|
patch("hermes_cli.update_cmd._locate_real_git") as locate,
|
|
):
|
|
result = update_cmd._ensure_non_trampoline_git(git_cmd)
|
|
assert result == git_cmd
|
|
locate.assert_not_called()
|
|
|
|
@pytest.mark.platforms("windows")
|
|
def test_trampoline_swaps_to_real_git(self, capsys):
|
|
from pathlib import Path
|
|
|
|
from hermes_cli import update_cmd
|
|
|
|
git_cmd = ["git", "-c", "windows.appendAtomically=false"]
|
|
real = Path(r"C:\Program Files\Git\mingw64\libexec\git-core\git.exe")
|
|
with (
|
|
patch(
|
|
"hermes_cli.update_cmd.subprocess.run",
|
|
side_effect=self._fake_run_trampoline,
|
|
),
|
|
patch(
|
|
"hermes_cli.update_cmd._locate_real_git", return_value=real
|
|
),
|
|
):
|
|
result = update_cmd._ensure_non_trampoline_git(git_cmd)
|
|
assert result == [str(real), "-c", "windows.appendAtomically=false"]
|
|
out = capsys.readouterr().out
|
|
assert "switching to real git" in out
|
|
|
|
@pytest.mark.platforms("windows")
|
|
def test_trampoline_no_real_git_keeps_command(self, capsys):
|
|
from hermes_cli import update_cmd
|
|
|
|
git_cmd = ["git", "-c", "windows.appendAtomically=false"]
|
|
with (
|
|
patch(
|
|
"hermes_cli.update_cmd.subprocess.run",
|
|
side_effect=self._fake_run_trampoline,
|
|
),
|
|
patch("hermes_cli.update_cmd._locate_real_git", return_value=None),
|
|
):
|
|
result = update_cmd._ensure_non_trampoline_git(git_cmd)
|
|
assert result == git_cmd
|
|
out = capsys.readouterr().out
|
|
assert "ZIP path" in out
|
|
|
|
@pytest.mark.platforms("not windows")
|
|
def test_off_windows_noop(self):
|
|
from hermes_cli import update_cmd
|
|
|
|
git_cmd = ["git"]
|
|
with patch("hermes_cli.update_cmd.subprocess.run") as run:
|
|
result = update_cmd._ensure_non_trampoline_git(git_cmd)
|
|
assert result == git_cmd
|
|
run.assert_not_called()
|
|
|
|
def test_portable_git_candidates_check_shared_root_first(self, tmp_path, monkeypatch):
|
|
# Profile-scoped layout: HERMES_HOME = <root>/profiles/foo, but the
|
|
# PortableGit tree lives under the SHARED root (monerostar review on
|
|
# #88136). The candidate list must check get_default_hermes_root()
|
|
# before the profile home.
|
|
import hermes_constants
|
|
from hermes_cli.update_cmd_git import _portable_git_candidates
|
|
|
|
root = tmp_path / "root"
|
|
profile_home = root / "profiles" / "foo"
|
|
|
|
monkeypatch.setattr(hermes_constants, "get_default_hermes_root", lambda: root)
|
|
monkeypatch.setattr(hermes_constants, "get_hermes_home", lambda: profile_home)
|
|
|
|
candidates = _portable_git_candidates()
|
|
assert candidates[0] == (
|
|
root / "git" / "mingw64" / "libexec" / "git-core" / "git.exe"
|
|
)
|
|
assert candidates[1] == (
|
|
profile_home / "git" / "mingw64" / "libexec" / "git-core" / "git.exe"
|
|
)
|