Files
hermes-agent/tests/hermes_cli/test_lazy_refresh_venv_repair.py
Teknium ba030bc0db fix(test-seams): monkeypatch.setattr facade aliases — also patch the defining module (57 files)
Tests did monkeypatch.setattr(<facade module>, name) where name is now defined in a
sibling module and the production path reads the sibling's binding. Where production
reads through BOTH bindings the setattr is duplicated onto the defining module (import
added next to the existing alias import); where only the sibling reads it the target is
repointed. Seams whose production readers go through the facade are left alone.
2026-09-03 19:33:28 -07:00

278 lines
8.6 KiB
Python

"""Tests for lazy-backend refresh venv repair (#57828 / #58004)."""
from __future__ import annotations
import textwrap
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import MagicMock, patch
import hermes_cli.main as m
import hermes_cli.main_install_repair as hermes_cli_main_install_repair
from hermes_cli import main_install_repair
from hermes_cli import update_cmd
import pytest
def test_detect_returns_none_when_probe_subprocess_fails(tmp_path, monkeypatch):
python = tmp_path / "python"
python.write_text("", encoding="utf-8")
monkeypatch.setattr(
m, "_resolve_install_target_python", lambda *a, **k: python
)
monkeypatch.setattr(
hermes_cli_main_install_repair, "_resolve_install_target_python", lambda *a, **k: python
)
monkeypatch.setattr(
m.subprocess,
"run",
MagicMock(side_effect=OSError("exec failed")),
)
assert main_install_repair._detect_broken_lazy_refresh_imports(["uv", "pip"]) is None
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(main_install_repair, "_run_package_only_install", fake_install)
monkeypatch.setattr(main_install_repair, "_detect_broken_lazy_refresh_imports", fake_detect)
ok = main_install_repair._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(main_install_repair, "_detect_broken_lazy_refresh_imports", lambda *a, **k: ["PyYAML"])
monkeypatch.setattr(main_install_repair, "_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 "import probes" in out
assert "Backends keep their previously-installed version" not in out
def test_refresh_uses_pre_rebuild_snapshot_when_provided(monkeypatch):
"""Replacement runtimes must not re-detect features after packages vanish."""
import tools.lazy_deps as lazy_deps_mod
monkeypatch.setattr(
lazy_deps_mod,
"active_features",
lambda: pytest.fail("post-rebuild detection must not run"),
)
restored = []
monkeypatch.setattr(
lazy_deps_mod,
"restore_features",
lambda features: restored.append(features) or {"platform.telegram": "restored"},
)
assert m._refresh_active_lazy_features(
["uv", "pip"], features=["platform.telegram"]
) is True
assert restored == [["platform.telegram"]]
def test_capture_active_tool_dependencies_uses_tools_status_probes(monkeypatch):
from hermes_cli import tools_config_post_setup
monkeypatch.setattr(
tools_config_post_setup,
"_module_installed",
lambda module: module in {"langfuse", "ddgs"},
)
assert m._capture_active_tool_dependencies() == ["ddgs", "langfuse"]
def test_restore_active_tool_dependencies_uses_static_allowlist(monkeypatch):
calls = []
monkeypatch.setattr(
m,
"_run_package_only_install",
lambda cmd, *, env=None: calls.append((cmd, env)),
)
monkeypatch.setattr(
hermes_cli_main_install_repair,
"_run_package_only_install",
lambda cmd, *, env=None: calls.append((cmd, env)),
)
env = {"VIRTUAL_ENV": "/tmp/venv"}
m._restore_active_tool_dependencies(
["langfuse", "not-allowlisted"],
["uv", "pip"],
env=env,
)
assert calls == [(["uv", "pip", "install", "langfuse", "--quiet"], env)]
def test_cmd_update_captures_and_propagates_pre_rebuild_snapshot(
tmp_path, monkeypatch
):
"""The updater must carry pre-rebuild state into its repair refresh."""
from hermes_cli import managed_uv, update_cmd
(tmp_path / ".git").mkdir()
snapshot = ["platform.telegram"]
tool_snapshot = ["langfuse"]
refresh_calls = []
restore_calls = []
class RestoreReached(Exception):
pass
def fake_run(cmd, **kwargs):
if "rev-parse" in cmd:
return SimpleNamespace(returncode=0, stdout="main\n", stderr="")
if "rev-list" in cmd:
return SimpleNamespace(returncode=0, stdout="0\n", stderr="")
return SimpleNamespace(returncode=0, stdout="", stderr="")
def fake_refresh(prefix, *, env=None, features=None):
refresh_calls.append((prefix, env, features))
return True
def fake_restore(dependencies, prefix, *, env=None):
restore_calls.append((dependencies, prefix, env))
raise RestoreReached
monkeypatch.setattr(m, "PROJECT_ROOT", tmp_path)
monkeypatch.setattr(m, "_capture_active_lazy_features", lambda: snapshot.copy())
monkeypatch.setattr(
m, "_capture_active_tool_dependencies", lambda: tool_snapshot.copy()
)
monkeypatch.setattr(m, "_is_windows", lambda: False)
monkeypatch.setattr(hermes_cli_main_install_repair, "_is_windows", lambda: False)
monkeypatch.setattr(m, "_run_pre_update_backup", lambda args: None)
monkeypatch.setattr(m, "_pause_windows_gateways_for_update", lambda: None)
monkeypatch.setattr(m, "_resume_windows_gateways_after_update", lambda state: None)
monkeypatch.setattr(update_cmd, "_discard_lockfile_churn", lambda *args: None)
monkeypatch.setattr(m, "_get_origin_url", lambda *args: "https://github.com/NousResearch/hermes-agent.git")
monkeypatch.setattr(m, "_resolve_update_branch", lambda args: "main")
monkeypatch.setattr(m, "_stash_local_changes_if_needed", lambda *args: None)
monkeypatch.setattr(update_cmd, "_invalidate_update_cache", lambda: None)
monkeypatch.setattr(
update_cmd, "_venv_core_imports_healthy", lambda: (False, "broken")
)
monkeypatch.setattr(update_cmd, "_write_update_incomplete_marker", lambda: None)
monkeypatch.setattr(
m, "_install_python_dependencies_with_optional_fallback", lambda *a, **k: None
)
monkeypatch.setattr(m, "_refresh_active_lazy_features", fake_refresh)
monkeypatch.setattr(m, "_restore_active_tool_dependencies", fake_restore)
monkeypatch.setattr(m.subprocess, "run", fake_run)
monkeypatch.setattr(managed_uv, "update_managed_uv", lambda **kwargs: None)
monkeypatch.setattr(managed_uv, "ensure_uv", lambda **kwargs: "uv")
args = SimpleNamespace(
yes=True,
force=False,
force_venv=False,
no_backup=True,
backup=False,
branch=None,
)
with pytest.raises(RestoreReached):
update_cmd._cmd_update_impl(args, gateway_mode=False)
# The repair env is now built via managed_python_env (#83914): third-party
# UV vars are stripped, managed pins set, then VIRTUAL_ENV re-pointed at
# the install's venv. Assert the CONTRACT, not the raw environ copy.
from hermes_cli.managed_uv import managed_python_env
expected_env = managed_python_env()
expected_env["VIRTUAL_ENV"] = str(tmp_path / "venv")
assert refresh_calls == [
(
["uv", "pip"],
expected_env,
snapshot,
)
]
assert restore_calls == [
(
tool_snapshot,
["uv", "pip"],
expected_env,
)
]