From a9a8ba2acf8360bf62fa1f8fc246fd38328b1dea Mon Sep 17 00:00:00 2001 From: HexLab98 Date: Sat, 4 Jul 2026 08:43:38 +0700 Subject: [PATCH] test(update): cover lazy refresh venv repair after failed installs Add repair/probe/quarantine regression tests and update autostash mocks for the new lazy-refresh signature. --- .../test_lazy_refresh_venv_repair.py | 219 ++++++++++++++++++ tests/hermes_cli/test_update_autostash.py | 3 +- 2 files changed, 221 insertions(+), 1 deletion(-) create mode 100644 tests/hermes_cli/test_lazy_refresh_venv_repair.py diff --git a/tests/hermes_cli/test_lazy_refresh_venv_repair.py b/tests/hermes_cli/test_lazy_refresh_venv_repair.py new file mode 100644 index 000000000000..817c6c65a338 --- /dev/null +++ b/tests/hermes_cli/test_lazy_refresh_venv_repair.py @@ -0,0 +1,219 @@ +"""Tests for lazy-backend refresh venv repair (#57828).""" + +from __future__ import annotations + +import textwrap +from pathlib import Path +from unittest.mock import MagicMock, patch + +import hermes_cli.main as m + + +def test_detect_broken_imports_returns_repair_package_names( + tmp_path, monkeypatch +): + venv_bin = tmp_path / "bin" + venv_bin.mkdir(parents=True) + python = venv_bin / "python" + python.write_text("", encoding="utf-8") + + monkeypatch.setattr( + m, + "_resolve_install_target_python", + lambda prefix, env: python, + ) + + def fake_run(cmd, **kwargs): + result = MagicMock() + result.stdout = "yaml\nclick\n" + result.returncode = 0 + return result + + monkeypatch.setattr(m.subprocess, "run", fake_run) + + broken = m._detect_broken_lazy_refresh_imports( + ["python", "-m", "pip"], env={"VIRTUAL_ENV": str(tmp_path)} + ) + assert broken == ["PyYAML", "click"] + + +def test_repair_runs_force_reinstall_with_pyproject_pins( + tmp_path, monkeypatch +): + pyproject = tmp_path / "pyproject.toml" + pyproject.write_text( + textwrap.dedent( + """\ + [project] + name = "fake" + version = "0.0.0" + dependencies = [ + "pyyaml==6.0.3", + "click==8.2.1", + ] + """ + ) + ) + monkeypatch.setattr(m, "PROJECT_ROOT", tmp_path) + + calls: list[list[str]] = [] + + def fake_install(cmd, **kwargs): + calls.append(cmd) + + detect_calls = {"count": 0} + + def fake_detect(prefix, *, env=None): + detect_calls["count"] += 1 + return [] + + monkeypatch.setattr(m, "_run_package_only_install", fake_install) + monkeypatch.setattr(m, "_detect_broken_lazy_refresh_imports", fake_detect) + + ok = m._repair_broken_lazy_refresh_imports( + ["uv", "pip"], + ["PyYAML", "click"], + env={"VIRTUAL_ENV": str(tmp_path)}, + ) + assert ok is True + assert calls == [ + [ + "uv", + "pip", + "install", + "--force-reinstall", + "pyyaml==6.0.3", + "click==8.2.1", + ] + ] + assert detect_calls["count"] == 1 + + +def test_refresh_repairs_venv_after_lazy_failure(tmp_path, monkeypatch, capsys): + import tools.lazy_deps as lazy_deps_mod + + monkeypatch.setattr(lazy_deps_mod, "active_features", lambda: ["platform.matrix"]) + monkeypatch.setattr( + lazy_deps_mod, + "refresh_active_features", + lambda **kw: {"platform.matrix": "failed: pip install failed"}, + ) + + repair_calls: list[list[str]] = [] + + def fake_repair(prefix, packages, *, env=None): + repair_calls.append(packages) + return True + + monkeypatch.setattr(m, "_detect_broken_lazy_refresh_imports", lambda *a, **k: ["PyYAML"]) + monkeypatch.setattr(m, "_repair_broken_lazy_refresh_imports", fake_repair) + + ok = m._refresh_active_lazy_features(["uv", "pip"], env={"VIRTUAL_ENV": str(tmp_path)}) + out = capsys.readouterr().out + + assert ok is True + assert repair_calls == [["PyYAML"]] + assert "Venv repair succeeded" in out + assert "Backends keep their previously-installed version" not in out + + +def test_refresh_returns_false_when_repair_fails(tmp_path, monkeypatch, capsys): + import tools.lazy_deps as lazy_deps_mod + + monkeypatch.setattr(lazy_deps_mod, "active_features", lambda: ["platform.matrix"]) + monkeypatch.setattr( + lazy_deps_mod, + "refresh_active_features", + lambda **kw: {"platform.matrix": "failed: pip install failed"}, + ) + + monkeypatch.setattr(m, "_detect_broken_lazy_refresh_imports", lambda *a, **k: ["PyYAML"]) + monkeypatch.setattr( + m, "_repair_broken_lazy_refresh_imports", lambda *a, **k: False + ) + + ok = m._refresh_active_lazy_features(["uv", "pip"], env={"VIRTUAL_ENV": str(tmp_path)}) + out = capsys.readouterr().out + + assert ok is False + assert "Venv repair incomplete" in out + + +def test_upgrade_pip_before_lazy_refresh_never_raises(monkeypatch): + monkeypatch.setattr( + m, + "_run_package_only_install", + MagicMock(side_effect=m.subprocess.CalledProcessError(1, "pip")), + ) + m._upgrade_pip_before_lazy_refresh(["uv", "pip"]) + + +def test_package_only_repair_does_not_quarantine_shims_on_windows( + tmp_path, monkeypatch +): + """Regression: package-only repairs must not rename hermes.exe on Windows.""" + fake_scripts = tmp_path / "venv" / "Scripts" + fake_scripts.mkdir(parents=True) + + install_calls: list[list[str]] = [] + + def fake_install(cmd, **kwargs): + install_calls.append(cmd) + + monkeypatch.setattr(m, "_is_windows", lambda: True) + monkeypatch.setattr(m, "_venv_scripts_dir", lambda: fake_scripts) + monkeypatch.setattr(m, "_run_package_only_install", fake_install) + monkeypatch.setattr( + m, "_detect_broken_lazy_refresh_imports", lambda *a, **k: [] + ) + + with patch("hermes_cli.main._quarantine_running_hermes_exe") as mock_quar: + m._repair_broken_lazy_refresh_imports( + ["uv", "pip"], + ["PyYAML"], + env={"VIRTUAL_ENV": str(tmp_path / "venv")}, + ) + + mock_quar.assert_not_called() + assert install_calls + + +def test_upgrade_pip_does_not_quarantine_shims_on_windows(tmp_path, monkeypatch): + fake_scripts = tmp_path / "venv" / "Scripts" + fake_scripts.mkdir(parents=True) + + install_calls: list[list[str]] = [] + + def fake_install(cmd, **kwargs): + install_calls.append(cmd) + + monkeypatch.setattr(m, "_is_windows", lambda: True) + monkeypatch.setattr(m, "_venv_scripts_dir", lambda: fake_scripts) + monkeypatch.setattr(m, "_run_package_only_install", fake_install) + + with patch("hermes_cli.main._quarantine_running_hermes_exe") as mock_quar: + m._upgrade_pip_before_lazy_refresh(["uv", "pip"]) + + mock_quar.assert_not_called() + assert install_calls == [["uv", "pip", "install", "--upgrade", "pip"]] + + +def test_lazy_refresh_repair_specs_resolves_extras(tmp_path, monkeypatch): + pyproject = tmp_path / "pyproject.toml" + pyproject.write_text( + textwrap.dedent( + """\ + [project] + name = "fake" + version = "0.0.0" + dependencies = [ + "PyJWT[crypto]==2.13.0", + "cryptography==46.0.7", + ] + """ + ) + ) + monkeypatch.setattr(m, "PROJECT_ROOT", tmp_path) + + specs = m._lazy_refresh_repair_specs(["PyJWT", "cryptography"]) + assert specs == ["PyJWT[crypto]==2.13.0", "cryptography==46.0.7"] diff --git a/tests/hermes_cli/test_update_autostash.py b/tests/hermes_cli/test_update_autostash.py index 514632048218..2797ca19f8fa 100644 --- a/tests/hermes_cli/test_update_autostash.py +++ b/tests/hermes_cli/test_update_autostash.py @@ -393,7 +393,8 @@ def _setup_update_mocks(monkeypatch, tmp_path): monkeypatch.setattr(hermes_config, "get_missing_config_fields", lambda: []) monkeypatch.setattr(hermes_config, "check_config_version", lambda: (5, 5)) monkeypatch.setattr(hermes_config, "migrate_config", lambda **kw: {"env_added": [], "config_added": []}) - monkeypatch.setattr(hermes_main, "_refresh_active_lazy_features", lambda: None) + monkeypatch.setattr(hermes_main, "_upgrade_pip_before_lazy_refresh", lambda *a, **kw: None) + monkeypatch.setattr(hermes_main, "_refresh_active_lazy_features", lambda *a, **kw: True) def test_cmd_update_retries_optional_extras_individually_when_all_fails(monkeypatch, tmp_path, capsys):