Files
hermes-agent/tests/hermes_cli/test_managed_uv.py
teknium1 290bdc3c76 fix(update): let the Windows repoint run; refuse a minor-line jump
The self-lock/holder preflight (#99711) deferred the repair on the theory
that the updater's own mapped python.exe makes the venv rename impossible.
Live on windows-latest, a process executing from venv\Scripts\python.exe
(and the repo's real .venv with cp311 .pyd extensions loaded) does NOT block
the rename; what blocks it with WinError 5 is any ordinary handle under the
tree: a process cwd, an open file, a sync client. Since sys.executable is
always under the venv on Windows, the deferral fired on every run and no
Windows install could repair from `hermes update`. Retire it and its tests;
the pyvenv.cfg repoint needs no rename and has none of that exposure. The
wine2e lane now runs the cutover probes instead.

The repoint keeps the live venv's site-packages, so provisioning's
fall-forward to the next minor (3.11 -> 3.12, #76106) must not be pointed at
a cp311 tree: refuse before touching pyvenv.cfg. The live Windows test holds
the venv the way the field does (a child with cwd inside it), asserts the
rename path fails (the symptom) and the repoint succeeds; the rollback and
minor-guard tests run on every host.
2026-09-17 08:44:33 -07:00

1482 lines
63 KiB
Python

"""Tests for hermes_cli.managed_uv — one path, no guessing."""
from __future__ import annotations
import os
import stat
import sys
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import MagicMock, patch
import pytest
from hermes_cli.managed_uv import _CandidateStageError
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
# Host-native managed-uv binary name: managed_uv_path() installs `uv` on
# POSIX and `uv.exe` on Windows. Fixtures must build what the real host
# resolves — no platform fake.
_UV_BINARY_NAME = "uv.exe" if sys.platform == "win32" else "uv"
def _make_executable(path: Path) -> None:
"""Create a minimal fake uv binary at *path*."""
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text("#!/bin/sh\necho uv 0.1.2\n")
path.chmod(path.stat().st_mode | stat.S_IEXEC)
def _runtime_info(
executable: Path,
sqlite_version: tuple[int, int, int],
):
from hermes_cli.sqlite_runtime import SQLiteRuntimeInfo
return SQLiteRuntimeInfo(
executable=executable,
base_prefix=executable.parent.parent,
python_version=(3, 11, 15),
sqlite_version=sqlite_version,
sqlite_version_string=".".join(str(part) for part in sqlite_version),
sqlite_source_id=f"source-{sqlite_version}",
)
def _RRR(status):
"""not-applicable RuntimeRepairResult for tests that neutralize the
repair hook. ensure_uv()/update_managed_uv() invoke runtime repair as a
side effect; unmocked, it probes the REAL checkout's venv/.venv — on CI
the repo .venv links vulnerable SQLite, so repair fires for real and
re-invokes _install_uv (uv-refresh retry), breaking call-count asserts.
"""
from hermes_cli.managed_uv import RuntimeRepairResult
return RuntimeRepairResult(status)
def _make_runtime_install(
tmp_path: Path,
*,
windows: bool = False,
) -> tuple[Path, Path, Path]:
root = tmp_path / "checkout"
root.mkdir()
(root / "pyproject.toml").write_text("[project]\n", encoding="utf-8")
live = root / "venv"
bin_dir = live / ("Scripts" if windows else "bin")
bin_dir.mkdir(parents=True)
python = bin_dir / ("python.exe" if windows else "python")
python.write_text("live interpreter", encoding="utf-8")
sentinel = live / "sentinel"
sentinel.write_text("live", encoding="utf-8")
return root, live, sentinel
# ---------------------------------------------------------------------------
# managed_uv_path
# ---------------------------------------------------------------------------
class TestManagedUvPath:
# POSIX arm of the name mapping; the Windows arm (uv.exe) is exercised
# for real by TestEnsureUvWindowsSafe on the Windows lane.
@pytest.mark.skipif(sys.platform == "win32", reason="POSIX-only: bin/uv name")
def test_posix(self, tmp_path):
with patch("hermes_cli.managed_uv.get_hermes_home", return_value=tmp_path):
from hermes_cli.managed_uv import managed_uv_path
assert managed_uv_path() == tmp_path / "bin" / "uv"
class TestMacOSManagedPythonSigning:
def test_signs_with_stable_identifier_and_verifies(self, tmp_path, monkeypatch):
import hermes_cli.managed_uv as managed_uv
python = tmp_path / "generation" / "bin" / "python3.11"
python.parent.mkdir(parents=True)
python.touch()
calls = []
def fake_run(cmd, **kwargs):
calls.append((cmd, kwargs))
return SimpleNamespace(returncode=0, stdout="", stderr="")
monkeypatch.setattr(managed_uv.platform, "system", lambda: "Darwin")
monkeypatch.setattr(managed_uv.shutil, "which", lambda name: "/usr/bin/codesign")
monkeypatch.setattr(managed_uv.subprocess, "run", fake_run)
assert managed_uv._macos_sign_managed_python(python) is True
assert calls[0][0] == [
"/usr/bin/codesign",
"--force",
"--deep",
"--sign",
"-",
"--timestamp=none",
"--identifier",
"com.nousresearch.hermes.managed-python",
"--requirements",
'=designated => identifier "com.nousresearch.hermes.managed-python"',
str(python),
]
assert calls[1][0] == [
"/usr/bin/codesign",
"--verify",
"--deep",
"--strict",
str(python),
]
def test_is_non_blocking_when_signing_fails(self, tmp_path, monkeypatch):
import hermes_cli.managed_uv as managed_uv
python = tmp_path / "python3.11"
monkeypatch.setattr(managed_uv.platform, "system", lambda: "Darwin")
monkeypatch.setattr(managed_uv.shutil, "which", lambda name: "/usr/bin/codesign")
monkeypatch.setattr(
managed_uv.subprocess,
"run",
lambda *args, **kwargs: SimpleNamespace(
returncode=1, stdout="", stderr="not signable"
),
)
assert managed_uv._macos_sign_managed_python(python) is False
def test_skips_non_macos(self, tmp_path, monkeypatch):
import hermes_cli.managed_uv as managed_uv
monkeypatch.setattr(managed_uv.platform, "system", lambda: "Linux")
monkeypatch.setattr(
managed_uv.subprocess,
"run",
lambda *args, **kwargs: pytest.fail("codesign must not run on Linux"),
)
assert managed_uv._macos_sign_managed_python(tmp_path / "python") is False
# ---------------------------------------------------------------------------
# resolve_uv
# ---------------------------------------------------------------------------
class TestResolveUv:
def test_existing_executable(self, tmp_path):
uv = tmp_path / "bin" / _UV_BINARY_NAME
_make_executable(uv)
with patch("hermes_cli.managed_uv.get_hermes_home", return_value=tmp_path):
from hermes_cli.managed_uv import resolve_uv
result = resolve_uv()
assert result == str(uv)
def test_non_executable_file_returns_none(self, tmp_path):
uv = tmp_path / "bin" / "uv"
uv.parent.mkdir(parents=True)
uv.write_text("not a binary")
# Ensure no execute bit
uv.chmod(0o644)
with patch("hermes_cli.managed_uv.get_hermes_home", return_value=tmp_path):
from hermes_cli.managed_uv import resolve_uv
assert resolve_uv() is None
class TestPipInstallHint:
def test_names_the_managed_uv_when_present(self, tmp_path):
uv = tmp_path / "bin" / _UV_BINARY_NAME
_make_executable(uv)
with patch("hermes_cli.managed_uv.get_hermes_home", return_value=tmp_path):
from hermes_cli.managed_uv import pip_install_hint
assert pip_install_hint("qrcode") == f"{uv} pip install --python {sys.executable} qrcode"
def test_falls_back_to_bare_uv_when_absent(self, tmp_path):
with patch("hermes_cli.managed_uv.get_hermes_home", return_value=tmp_path):
from hermes_cli.managed_uv import pip_install_hint
assert pip_install_hint("qrcode") == f"uv pip install --python {sys.executable} qrcode"
# ---------------------------------------------------------------------------
# ensure_uv
# ---------------------------------------------------------------------------
class TestEnsureUv:
def test_installs_if_missing(self, tmp_path):
uv = tmp_path / "bin" / _UV_BINARY_NAME
with patch("hermes_cli.managed_uv.get_hermes_home", return_value=tmp_path), \
patch("hermes_cli.managed_uv.repair_vulnerable_runtime", return_value=_RRR("not-applicable")), \
patch("hermes_cli.managed_uv._uv_version", return_value="uv 0.1.2"), \
patch("hermes_cli.managed_uv._install_uv") as mock_install:
# Simulate the installer creating the binary (host-native name:
# uv.exe on Windows, uv on POSIX).
def fake_install(target):
_make_executable(target)
mock_install.side_effect = fake_install
from hermes_cli.managed_uv import ensure_uv
path = ensure_uv()
assert path == str(uv)
mock_install.assert_called_once()
def test_install_reports_runtime_repair_to_observer(self, tmp_path):
from hermes_cli.managed_uv import (
RuntimeRepairResult,
ensure_uv,
)
repair = RuntimeRepairResult(
"repaired",
sqlite_before="3.50.4",
sqlite_after="3.53.1",
)
def fake_install(target):
_make_executable(target)
observed = []
with patch(
"hermes_cli.managed_uv.get_hermes_home",
return_value=tmp_path,
), patch(
"hermes_cli.managed_uv._install_uv",
side_effect=fake_install,
), patch(
"hermes_cli.managed_uv._uv_version",
return_value="uv 0.1.2",
), patch(
"hermes_cli.managed_uv.repair_vulnerable_runtime",
return_value=repair,
):
path = ensure_uv(repair_observer=observed.append)
assert path == str(tmp_path / "bin" / _UV_BINARY_NAME)
assert observed == [repair]
@pytest.mark.skipif(sys.platform == "win32",
reason="POSIX-only: the _UvResult dual contract is not offered on Windows")
class TestEnsureUvUpdateBoundary:
"""``ensure_uv()`` must answer to both the single-value and the legacy
``(path, fresh_bootstrap)`` call conventions — **on POSIX**.
``hermes update`` runs the call site from the old, already-imported
``hermes_cli.main`` against the freshly pulled ``managed_uv``. A release
parked on a ``(path, fresh)`` tuple runs ``uv_bin, fresh = ensure_uv()``
against the single-value module; the path is an iterable ``str`` so the
2-target unpack walked its characters and raised
``ValueError: too many values to unpack (expected 2)`` (root cause behind
PR #39763), or ``TypeError`` on the ``None`` failure path. On POSIX the
result must therefore be usable as a bare path *and* unpackable as a
2-tuple, in both the success and failure cases.
The dual contract is intentionally **not** offered on Windows — see
``TestEnsureUvWindowsSafe`` for why — so these tests are POSIX-only: the
host's real ``platform.system()`` selects the wrapper branch, nothing is
faked.
"""
def test_success_usable_as_single_value(self, tmp_path):
_make_executable(tmp_path / "bin" / "uv")
with patch("hermes_cli.managed_uv.get_hermes_home", return_value=tmp_path), \
patch("hermes_cli.managed_uv.repair_vulnerable_runtime", return_value=_RRR("not-applicable")):
from hermes_cli.managed_uv import ensure_uv
uv_bin = ensure_uv()
assert uv_bin == str(tmp_path / "bin" / "uv")
assert bool(uv_bin) is True
def test_success_unpacks_as_legacy_two_tuple(self, tmp_path):
_make_executable(tmp_path / "bin" / "uv")
with patch("hermes_cli.managed_uv.get_hermes_home", return_value=tmp_path), \
patch("hermes_cli.managed_uv.repair_vulnerable_runtime", return_value=_RRR("not-applicable")):
from hermes_cli.managed_uv import ensure_uv
uv_bin, fresh = ensure_uv() # old: uv_bin, fresh_bootstrap = ensure_uv()
assert uv_bin == str(tmp_path / "bin" / "uv")
assert fresh is False
def test_failure_unpacks_without_raising(self, tmp_path):
with patch("hermes_cli.managed_uv.get_hermes_home", return_value=tmp_path), \
patch("hermes_cli.managed_uv.repair_vulnerable_runtime", return_value=_RRR("not-applicable")), \
patch("hermes_cli.managed_uv._install_uv", side_effect=RuntimeError("network down")):
from hermes_cli.managed_uv import ensure_uv
uv_bin, fresh = ensure_uv()
assert uv_bin is None
assert fresh is False
class TestEnsureUvWindowsSafe:
"""On Windows ``ensure_uv()`` must return a plain ``str``/``None``.
``subprocess`` on Windows serializes argv through
``subprocess.list2cmdline``, which iterates every entry *as a string*
(``for c in arg``). The dependency installer feeds uv straight into the
command list (``[uv_bin, "pip", "install", ...]``). A ``str`` subclass
whose ``__iter__`` yields ``(path, fresh_bootstrap)`` instead of characters
therefore injects the bool into the command line and crashes the install
with ``TypeError: sequence item 1: expected str instance, bool found``
(a real field report on a 10-commits-behind Windows install). A single
return value cannot serve both the legacy 2-tuple unpack and Windows
char-iteration — both use the iterator protocol — so Windows opts out of
the wrapper entirely.
"""
def test_uvresult_would_break_windows_list2cmdline(self):
# Canary: this is *why* the wrapper is gated off Windows. If a future
# change makes _UvResult char-iterable (and thus list2cmdline-safe),
# the gate may be revisited.
import subprocess
from hermes_cli.managed_uv import _UvResult
with pytest.raises(TypeError):
subprocess.list2cmdline([_UvResult("C:\\hermes\\uv.exe"), "pip"])
@pytest.mark.windows_only
def test_windows_returns_plain_str_safe_for_subprocess(self, tmp_path):
"""``windows_only``: the subject is the real Windows opt-out branch and
``subprocess.list2cmdline`` — the faked ``platform.system`` only ever
proved the branch existed, not that the field crash was fixed on the
host that reported it."""
import subprocess
# On Windows the managed binary is uv.exe.
_make_executable(tmp_path / "bin" / "uv.exe")
with patch("hermes_cli.managed_uv.get_hermes_home", return_value=tmp_path), \
patch("hermes_cli.managed_uv.repair_vulnerable_runtime", return_value=_RRR("not-applicable")):
from hermes_cli.managed_uv import _UvResult, ensure_uv
uv_bin = ensure_uv()
assert type(uv_bin) is str and not isinstance(uv_bin, _UvResult)
# The exact operation that crashed in the field must now succeed.
cmdline = subprocess.list2cmdline([uv_bin, "pip", "install", "-e", "."])
assert "pip" in cmdline and "install" in cmdline
# ---------------------------------------------------------------------------
# update_managed_uv
# ---------------------------------------------------------------------------
class TestUpdateManagedUv:
def test_fresh_stamp_skips_network_self_update_but_not_repair(self, tmp_path):
"""A recent success stamp must skip `uv self update` entirely while the
vulnerable-runtime repair probe still runs (CVE repair is never gated)."""
import time
from hermes_cli.managed_uv import RuntimeRepairResult, update_managed_uv
uv = tmp_path / "bin" / _UV_BINARY_NAME
_make_executable(uv)
# The stamp reader imports get_hermes_home separately from the binary
# resolver. Give both paths the same explicit test root.
stamp = tmp_path / "cache" / ".uv_self_update_stamp"
stamp.parent.mkdir(parents=True, exist_ok=True)
stamp.touch()
# File timestamps can lead time.time() briefly on Windows. Stay well
# inside the freshness window instead of racing its age >= 0 boundary.
recent = time.time() - 60
os.utime(stamp, (recent, recent))
with patch("hermes_cli.managed_uv.get_hermes_home", return_value=tmp_path), \
patch("hermes_cli.managed_uv._uv_self_update_stamp", return_value=stamp), \
patch("hermes_cli.managed_uv.repair_vulnerable_runtime",
return_value=RuntimeRepairResult("skipped")) as mock_repair, \
patch("hermes_cli.managed_uv.subprocess.run") as mock_run:
result = update_managed_uv()
assert result == str(uv)
assert mock_run.call_count == 0, "fresh stamp must skip the network self-update"
mock_repair.assert_called_once_with(str(uv))
def test_stale_stamp_runs_self_update_and_refreshes_stamp(self, tmp_path):
import os as _os
import time as _time
from hermes_cli.managed_uv import UV_SELF_UPDATE_INTERVAL_SECONDS, update_managed_uv
uv = tmp_path / "bin" / _UV_BINARY_NAME
_make_executable(uv)
# Keep the stamp and binary resolver in the same test root.
stamp = tmp_path / "cache" / ".uv_self_update_stamp"
stamp.parent.mkdir(parents=True, exist_ok=True)
stamp.touch()
old = _time.time() - UV_SELF_UPDATE_INTERVAL_SECONDS - 60
_os.utime(stamp, (old, old))
with patch("hermes_cli.managed_uv.get_hermes_home", return_value=tmp_path), \
patch("hermes_cli.managed_uv._uv_self_update_stamp", return_value=stamp), \
patch("hermes_cli.managed_uv.repair_vulnerable_runtime", return_value=_RRR("not-applicable")), \
patch("hermes_cli.managed_uv._uv_version", return_value="uv 0.2.0"), \
patch("hermes_cli.managed_uv.subprocess.run") as mock_run:
mock_run.return_value = MagicMock(returncode=0, stdout="uv 0.2.0")
update_managed_uv()
assert mock_run.call_args_list[0][0][0] == [str(uv), "self", "update"]
assert stamp.stat().st_mtime > old + 30, "successful self-update must refresh the stamp"
class TestManagedPythonStore:
def test_store_is_checkout_scoped_across_profiles(self, tmp_path, monkeypatch):
from hermes_cli.managed_uv import managed_python_install_dir
checkout = tmp_path / "checkout"
monkeypatch.setenv("HERMES_HOME", str(tmp_path / "profiles" / "alpha"))
alpha = managed_python_install_dir(checkout)
monkeypatch.setenv("HERMES_HOME", str(tmp_path / "profiles" / "beta"))
beta = managed_python_install_dir(checkout)
expected = checkout / ".hermes-runtime" / "python"
assert alpha == expected
assert beta == expected
def test_environment_is_private_and_sanitized(self, tmp_path):
from hermes_cli.managed_uv import managed_python_env
checkout = tmp_path / "checkout"
base_env = {
"KEEP_ME": "yes",
"CONDA_DEFAULT_ENV": "poison",
"CONDA_PREFIX": "/poison/conda",
"UV_PROJECT_ENVIRONMENT": "/poison/project",
"UV_NO_MANAGED_PYTHON": "1",
"UV_PYTHON": "/poison/python",
"UV_PYTHON_DOWNLOADS": "never",
"UV_SYSTEM_PYTHON": "1",
"VIRTUAL_ENV": "/poison/venv",
"PYTHONHOME": "/poison/home",
"PYTHONPATH": "/poison/path",
}
env = managed_python_env(checkout, base_env=base_env)
assert env["KEEP_ME"] == "yes"
assert env["UV_MANAGED_PYTHON"] == "1"
assert env["UV_NO_CONFIG"] == "1"
assert env["UV_PYTHON_INSTALL_BIN"] == "0"
assert env["UV_PYTHON_INSTALL_REGISTRY"] == "0"
assert env["UV_PYTHON_INSTALL_DIR"] == str(
checkout / ".hermes-runtime" / "python"
)
for key in (
"CONDA_DEFAULT_ENV",
"CONDA_PREFIX",
"UV_PROJECT_ENVIRONMENT",
"UV_NO_MANAGED_PYTHON",
"UV_PYTHON",
"UV_PYTHON_DOWNLOADS",
"UV_SYSTEM_PYTHON",
"VIRTUAL_ENV",
"PYTHONHOME",
"PYTHONPATH",
):
assert key not in env
assert base_env["PYTHONHOME"] == "/poison/home"
@pytest.mark.skipif(sys.platform == "win32",
reason="POSIX-only: fixtures build the bin/ (not Scripts/) venv layout")
class TestRuntimeRepair:
def test_safe_runtime_is_a_noop(self, tmp_path):
from hermes_cli.managed_uv import repair_vulnerable_runtime
root, live, sentinel = _make_runtime_install(tmp_path)
current = _runtime_info(live / "bin" / "python", (3, 53, 1))
with patch(
"hermes_cli.managed_uv.probe_sqlite_runtime",
return_value=current,
), \
patch(
"hermes_cli.managed_uv._install_safe_python_generation"
) as mock_install:
result = repair_vulnerable_runtime("uv", project_root=root)
assert result.status == "safe"
assert result.sqlite_before == "3.53.1"
assert result.sqlite_after == "3.53.1"
assert sentinel.read_text(encoding="utf-8") == "live"
assert not (root / ".hermes-runtime").exists()
mock_install.assert_not_called()
def test_failed_candidate_preserves_live_venv(self, tmp_path):
from hermes_cli.managed_uv import (
_acquire_repair_lock,
_release_repair_lock,
repair_vulnerable_runtime,
)
root, live, sentinel = _make_runtime_install(tmp_path)
current = _runtime_info(live / "bin" / "python", (3, 50, 4))
generation = root / ".hermes-runtime" / "python" / "generation-test"
candidate_python = generation / "bin" / "python"
candidate_python.parent.mkdir(parents=True)
candidate_python.write_text("candidate interpreter", encoding="utf-8")
fixed = _runtime_info(candidate_python, (3, 53, 1))
with patch(
"hermes_cli.managed_uv.probe_sqlite_runtime",
side_effect=[current, current],
), \
patch(
"hermes_cli.managed_uv._install_safe_python_generation",
return_value=(generation, candidate_python, fixed),
), \
patch(
"hermes_cli.managed_uv._stage_candidate_venv",
side_effect=_CandidateStageError("replacement environment rejected"),
):
result = repair_vulnerable_runtime("uv", project_root=root)
assert result.status == "failed"
assert "replacement environment" in result.detail
assert sentinel.read_text(encoding="utf-8") == "live"
assert (live / "bin" / "python").read_text(encoding="utf-8") == (
"live interpreter"
)
assert not generation.exists()
reacquired = _acquire_repair_lock(root / ".hermes-runtime")
assert reacquired is not None
_release_repair_lock(reacquired)
def test_safe_runtime_sweeps_old_stale_backups(self, tmp_path):
"""A fixed runtime reclaims aged venv.stale.runtime-* leftovers
(issue #73109) but leaves fresh ones (possible in-flight repair)."""
import os
import time as _time
from hermes_cli.managed_uv import repair_vulnerable_runtime
root, live, sentinel = _make_runtime_install(tmp_path)
old_backup = root / f"{live.name}.stale.runtime-1-2-aaaa"
(old_backup / "bin").mkdir(parents=True)
(old_backup / "bin" / "python").write_text("old", encoding="utf-8")
stale_mtime = _time.time() - 7200
os.utime(old_backup, (stale_mtime, stale_mtime))
fresh_backup = root / f"{live.name}.stale.runtime-9-9-bbbb"
(fresh_backup / "bin").mkdir(parents=True)
current = _runtime_info(live / "bin" / "python", (3, 53, 1))
with patch(
"hermes_cli.managed_uv.probe_sqlite_runtime",
return_value=current,
):
result = repair_vulnerable_runtime("uv", project_root=root)
assert result.status == "safe"
assert not old_backup.exists(), "aged stale backup must be reclaimed"
assert fresh_backup.exists(), "fresh backup may be an in-flight repair"
assert sentinel.read_text(encoding="utf-8") == "live"
def test_successful_repair_removes_parked_backup(self, tmp_path):
"""After a successful cutover the parked venv is removed instead of
leaking ~1 GB at the project root forever (issue #73109)."""
from hermes_cli.managed_uv import repair_vulnerable_runtime
root, live, sentinel = _make_runtime_install(tmp_path)
current = _runtime_info(live / "bin" / "python", (3, 50, 4))
generation = root / ".hermes-runtime" / "python" / "generation-test"
candidate_python = generation / "bin" / "python"
candidate_python.parent.mkdir(parents=True)
candidate_python.write_text("candidate interpreter", encoding="utf-8")
fixed = _runtime_info(candidate_python, (3, 53, 1))
candidate_venv = root / ".hermes-runtime" / "venv-candidate"
(candidate_venv / "bin").mkdir(parents=True)
(candidate_venv / "bin" / "python").write_text(
"candidate venv interpreter", encoding="utf-8"
)
with patch(
"hermes_cli.managed_uv.probe_sqlite_runtime",
side_effect=[current, current],
), \
patch(
"hermes_cli.managed_uv._install_safe_python_generation",
return_value=(generation, candidate_python, fixed),
), \
patch(
"hermes_cli.managed_uv._stage_candidate_venv",
return_value=candidate_venv,
), \
patch(
"hermes_cli.managed_uv._smoke_candidate_venv",
return_value=(True, "", fixed),
):
result = repair_vulnerable_runtime("uv", project_root=root)
assert result.status == "repaired"
assert result.backup_venv is not None
assert not result.backup_venv.exists(), (
"parked venv must be removed after a successful repair"
)
leftovers = list(root.glob(f"{live.name}.stale.runtime-*"))
assert leftovers == [], f"no stale markers may remain: {leftovers}"
def _make_candidate_layout(tmp_path):
"""The minimal checkout + pinned generation python `_stage_candidate_venv` needs."""
root = tmp_path / "checkout"
root.mkdir()
(root / "uv.lock").write_text("# lock\n", encoding="utf-8")
generation = root / ".hermes-runtime" / "python" / "gen"
python = generation / "bin" / "python"
python.parent.mkdir(parents=True)
python.write_text("py", encoding="utf-8")
return root, generation, python
def _fake_sync_proc(lines, returncode=0):
"""``subprocess.Popen`` stand-in for the candidate sync: text lines on stdout."""
proc = MagicMock()
proc.stdout = iter(lines)
proc.wait.return_value = returncode
return proc
class TestStageCandidateVenvCrossPlatform:
"""Candidate sync preserves project config and streams progress on every host."""
def test_sync_keeps_uv_project_config_and_merges_stderr(self, tmp_path):
import subprocess
from hermes_cli.managed_uv import _stage_candidate_venv
root, generation, python = _make_candidate_layout(tmp_path)
created = []
synced = []
def fake_run(argv, **kwargs):
created.append((list(argv), kwargs))
return MagicMock(returncode=0)
def fake_popen(argv, **kwargs):
synced.append((list(argv), kwargs))
return _fake_sync_proc([])
with patch("hermes_cli.managed_uv.subprocess.run", side_effect=fake_run), \
patch("hermes_cli.managed_uv.subprocess.Popen", side_effect=fake_popen), \
patch(
"hermes_cli.managed_uv._smoke_candidate_venv",
return_value=(True, "", None),
):
candidate = _stage_candidate_venv(
"uv",
project_root=root,
generation=generation,
python=python,
)
assert candidate is not None
assert len(created) == 1
venv_argv, venv_kwargs = created[0]
assert venv_argv[:2] == ["uv", "venv"]
assert "--no-config" in venv_argv
assert venv_kwargs["env"].get("UV_NO_CONFIG") == "1"
sync_argv, sync_kwargs = synced[0]
assert sync_argv[:2] == ["uv", "sync"]
assert "--locked" in sync_argv
assert "--no-config" not in sync_argv
assert "UV_NO_CONFIG" not in sync_kwargs["env"]
assert sync_kwargs["stderr"] == subprocess.STDOUT
def test_sync_failure_reports_the_child_reason(self, tmp_path, caplog):
"""A rejected candidate must say WHY — the child's own diagnosis, not a bare rc.
The reason rides the rejection into ``RuntimeRepairResult.detail`` (#111417, #111497).
"""
import logging
from hermes_cli.managed_uv import _stage_candidate_venv
root, generation, python = _make_candidate_layout(tmp_path)
lock_error = [
"Resolving despite existing lockfile due to addition of exclude newer exclusion\n",
"error: The lockfile at `uv.lock` needs to be updated, but `--locked` was provided.\n",
"\n",
"hint: To update the lockfile, run `uv lock`.\n",
]
with caplog.at_level(logging.WARNING), \
patch("hermes_cli.managed_uv.subprocess.run", return_value=MagicMock(returncode=0)), \
patch(
"hermes_cli.managed_uv.subprocess.Popen",
return_value=_fake_sync_proc(lock_error, returncode=1),
), \
patch(
"hermes_cli.managed_uv._smoke_candidate_venv",
return_value=(True, "", None),
), \
pytest.raises(_CandidateStageError) as rejected:
_stage_candidate_venv(
"uv",
project_root=root,
generation=generation,
python=python,
)
# The reason is the child's own diagnosis: the error + hint, without the progress noise
# that precedes them. It reaches the rejection the repair returns AND the log.
reason = str(rejected.value)
assert reason.startswith("candidate dependency sync failed (rc=1): ")
assert "error: The lockfile at `uv.lock` needs to be updated" in reason
assert "hint: To update the lockfile, run `uv lock`." in reason
assert "Resolving despite existing lockfile" not in reason
assert "candidate dependency sync failed (rc=1)" in caplog.text
assert "needs to be updated" in caplog.text
assert not list((root / ".hermes-runtime").glob("venv-candidate-*"))
class TestRuntimeCutover:
def test_os_lock_blocks_concurrent_repair_and_releases(self, tmp_path):
from hermes_cli.managed_uv import _acquire_repair_lock, _release_repair_lock
runtime_root = tmp_path / ".hermes-runtime"
first = _acquire_repair_lock(runtime_root)
assert first is not None
assert _acquire_repair_lock(runtime_root) is None
_release_repair_lock(first)
second = _acquire_repair_lock(runtime_root)
assert second is not None
_release_repair_lock(second)
def test_post_swap_smoke_failure_rolls_back_live_venv(self, tmp_path):
from hermes_cli.managed_uv import _cut_over_candidate
root, live, sentinel = _make_runtime_install(tmp_path)
runtime_root = root / ".hermes-runtime"
candidate = runtime_root / "venv-candidate-test"
candidate.mkdir(parents=True)
(candidate / "sentinel").write_text("candidate", encoding="utf-8")
rejected_info = _runtime_info(candidate / "bin" / "python", (3, 50, 4))
with patch(
"hermes_cli.managed_uv._smoke_candidate_venv",
return_value=(False, "core import smoke failed", rejected_info),
):
ok, backup, info, detail = _cut_over_candidate(
candidate,
project_root=root,
)
assert ok is False
assert backup is None
assert info == rejected_info
assert "post-cutover smoke failed" in detail
assert sentinel.read_text(encoding="utf-8") == "live"
assert (live / "bin" / "python").read_text(encoding="utf-8") == (
"live interpreter"
)
assert not candidate.exists()
assert not list(runtime_root.glob("venv-rejected-*"))
# ---------------------------------------------------------------------------
# _install_uv internals
# ---------------------------------------------------------------------------
class TestInstallUvInternals:
def test_installer_uses_host_branch_and_managed_directory(self, tmp_path):
"""The native installer receives the managed directory, not a PATH default."""
import hermes_cli.managed_uv as managed_uv
target = tmp_path / "bin" / _UV_BINARY_NAME
with patch("hermes_cli.managed_uv._install_uv_posix") as mock_posix, \
patch("hermes_cli.managed_uv._install_uv_windows") as mock_windows:
managed_uv._install_uv(target)
host_installer, other_installer = (
(mock_windows, mock_posix) if sys.platform == "win32"
else (mock_posix, mock_windows))
host_installer.assert_called_once()
other_installer.assert_not_called()
call_env = host_installer.call_args[0][0]
assert call_env["UV_INSTALL_DIR"] == str(tmp_path / "bin")
if sys.platform != "win32":
assert call_env["UV_UNMANAGED_INSTALL"] == str(tmp_path / "bin")
class TestRuntimeRequestMinorLine:
"""The repair must request the CPython minor line, not the exact patch.
Real-world constraint (verified live, July 2026): every published
python-build-standalone artifact for 3.11.14 links vulnerable SQLite
3.50.4 — even with --reinstall. The fixed SQLite (3.53.1) only exists
from 3.11.15. An exact-patch pin makes the repair permanently
impossible on such installs.
"""
def test_requests_minor_line(self):
from hermes_cli.managed_uv import _runtime_request
info = _runtime_info(Path("/venv/bin/python"), (3, 50, 4))
assert _runtime_request(info) == "3.11"
@staticmethod
def _run_generation(tmp_path, monkeypatch, current_version, candidate_version):
"""Drive _install_safe_python_generation with fakes; return result."""
import hermes_cli.managed_uv as managed_uv
from hermes_cli.sqlite_runtime import SQLiteRuntimeInfo
state = {}
def fake_run(cmd, **kwargs):
if "install" in cmd:
state["generation"] = Path(kwargs["env"]["UV_PYTHON_INSTALL_DIR"])
return SimpleNamespace(returncode=0, stdout="", stderr="")
# uv python find → a path inside the generation dir
python = state["generation"] / "cpython" / "bin" / "python3"
python.parent.mkdir(parents=True, exist_ok=True)
python.touch()
return SimpleNamespace(returncode=0, stdout=str(python), stderr="")
def fake_probe(python, **kwargs):
return SQLiteRuntimeInfo(
executable=Path(python),
base_prefix=Path(python).parent.parent,
python_version=candidate_version,
sqlite_version=(3, 53, 1),
sqlite_version_string="3.53.1",
sqlite_source_id="fixed",
)
current = SQLiteRuntimeInfo(
executable=Path("/venv/bin/python"),
base_prefix=Path("/venv"),
python_version=current_version,
sqlite_version=(3, 50, 4),
sqlite_version_string="3.50.4",
sqlite_source_id="old",
)
monkeypatch.setattr(managed_uv.subprocess, "run", fake_run)
monkeypatch.setattr(managed_uv, "probe_sqlite_runtime", fake_probe)
return managed_uv._install_safe_python_generation(
"uv", project_root=tmp_path, current=current
)
def test_accepts_newer_patch_same_minor(self, tmp_path, monkeypatch):
result = self._run_generation(
tmp_path, monkeypatch, (3, 11, 14), (3, 11, 15)
)
assert result is not None
_, _, candidate = result
assert candidate.python_version == (3, 11, 15)
class TestPatchRetryOnVulnerableCandidate:
"""Regression tests for issue #71250: when the bare minor-line request
(e.g. "3.11") resolves to a candidate that's still vulnerable -- because
uv's default resolution for that host picked an older cached/indexed
patch even though a newer non-vulnerable one is available -- the
provisioner must query the available patches and retry with explicit
newer versions, rather than giving up after the first attempt.
"""
@staticmethod
def _versioned_probe_run(vulnerable_versions, sqlite_fixed=(3, 53, 1)):
"""Build a fake subprocess.run where the install/find/probe cycle
resolves to a DIFFERENT candidate Python version depending on which
exact version string was requested, so retries with explicit
patches can be distinguished from the initial bare-minor attempt."""
import hermes_cli.managed_uv as managed_uv
from hermes_cli.sqlite_runtime import SQLiteRuntimeInfo
state = {"requested": None}
def fake_run(cmd, **kwargs):
if "install" in cmd:
# cmd = [uv, "python", "install", <request>, ...]
state["requested"] = cmd[3]
state["generation"] = Path(kwargs["env"]["UV_PYTHON_INSTALL_DIR"])
return SimpleNamespace(returncode=0, stdout="", stderr="")
if "list" in cmd:
return SimpleNamespace(returncode=0, stdout="", stderr="")
# uv python find → a path inside the generation dir, tagged with
# which request produced it so the probe below can look it up.
python = state["generation"] / "cpython" / "bin" / "python3"
python.parent.mkdir(parents=True, exist_ok=True)
python.write_text(state["requested"] or "")
return SimpleNamespace(returncode=0, stdout=str(python), stderr="")
def fake_probe(python, **kwargs):
requested = Path(python).read_text()
# Bare minor request ("3.11") always resolves to the FIRST
# (worst-case / already-known-vulnerable) version in the list.
if requested in vulnerable_versions or requested == "3.11":
version = (3, 11, 14) if requested == "3.11" else tuple(
int(p) for p in requested.split(".")
)
return SQLiteRuntimeInfo(
executable=Path(python), base_prefix=Path(python).parent.parent,
python_version=version, sqlite_version=(3, 50, 4),
sqlite_version_string="3.50.4", sqlite_source_id="vulnerable",
)
version = tuple(int(p) for p in requested.split("."))
return SQLiteRuntimeInfo(
executable=Path(python), base_prefix=Path(python).parent.parent,
python_version=version, sqlite_version=sqlite_fixed,
sqlite_version_string=".".join(str(p) for p in sqlite_fixed),
sqlite_source_id="fixed",
)
return fake_run, fake_probe
def _run(self, tmp_path, monkeypatch, *, vulnerable_versions, patch_list):
import hermes_cli.managed_uv as managed_uv
from hermes_cli.sqlite_runtime import SQLiteRuntimeInfo
fake_run, fake_probe = self._versioned_probe_run(vulnerable_versions)
current = SQLiteRuntimeInfo(
executable=Path("/venv/bin/python"), base_prefix=Path("/venv"),
python_version=(3, 11, 14), sqlite_version=(3, 50, 4),
sqlite_version_string="3.50.4", sqlite_source_id="old",
)
monkeypatch.setattr(managed_uv.subprocess, "run", fake_run)
monkeypatch.setattr(managed_uv, "probe_sqlite_runtime", fake_probe)
monkeypatch.setattr(
managed_uv, "_list_available_patches", lambda *a, **kw: patch_list
)
return managed_uv._install_safe_python_generation(
"uv", project_root=tmp_path, current=current
)
def test_retries_and_succeeds_with_explicit_newer_patch(self, tmp_path, monkeypatch):
"""The exact #71250 scenario: bare '3.11' resolves to vulnerable
3.11.14, but 3.11.15 (fixed) is available and gets tried explicitly."""
result = self._run(
tmp_path, monkeypatch,
vulnerable_versions={"3.11"},
patch_list=[(3, 11, 15), (3, 11, 14), (3, 11, 13), (3, 11, 12)],
)
assert result is not None, "Must recover via explicit-patch retry"
_, _, candidate = result
assert candidate.python_version == (3, 11, 15)
assert not candidate.wal_reset_vulnerable
def test_retry_is_bounded_by_max_retries_constant(self, tmp_path, monkeypatch):
"""A very long patch list must not result in unbounded retries -- capped at
_MAX_PATCH_RETRIES attempts. After exhausting same-minor retries the
fallback tries the next minor line, which may succeed."""
import hermes_cli.managed_uv as managed_uv
from hermes_cli.sqlite_runtime import SQLiteRuntimeInfo
current = SQLiteRuntimeInfo(
executable=Path("/venv/bin/python"), base_prefix=Path("/venv"),
python_version=(3, 11, 14), sqlite_version=(3, 50, 4),
sqlite_version_string="3.50.4", sqlite_source_id="old",
)
# 20 vulnerable patches -- far more than _MAX_PATCH_RETRIES.
huge_patch_list = [(3, 11, v) for v in range(30, 10, -1)]
all_vulnerable = {f"3.11.{v}" for v in range(30, 10, -1)} | {"3.11"}
fake_run2, fake_probe2 = self._versioned_probe_run(all_vulnerable)
install_calls = []
def counting_fake_run(cmd, **kwargs):
if "install" in cmd:
install_calls.append(cmd[3])
return fake_run2(cmd, **kwargs)
monkeypatch.setattr(managed_uv.subprocess, "run", counting_fake_run)
monkeypatch.setattr(managed_uv, "probe_sqlite_runtime", fake_probe2)
monkeypatch.setattr(
managed_uv, "_list_available_patches", lambda *a, **kw: huge_patch_list
)
result = managed_uv._install_safe_python_generation(
"uv", project_root=tmp_path, current=current
)
# The same-minor retries are bounded, but the minor-line fallback
# (3.11 → 3.12) succeeds because the mock returns a fixed build.
assert result is not None, (
"Minor-line fallback should find a fixed 3.12 build"
)
# 1 initial bare-minor attempt + at most _MAX_PATCH_RETRIES retries.
assert managed_uv._MAX_PATCH_RETRIES <= 5, (
"sanity: constant should stay small since each attempt is a "
"real download+install+probe cycle"
)
same_minor_explicit = [
call for call in install_calls if call.startswith("3.11.")
]
assert len(same_minor_explicit) <= managed_uv._MAX_PATCH_RETRIES, (
f"same-minor explicit retries must be capped: {same_minor_explicit}"
)
assert install_calls[0] == "3.11"
# The run ends the moment the bare next-minor fallback succeeds.
assert install_calls[-1] == "3.12"
assert install_calls.count("3.12") == 1
class TestMinorLineFallForward:
"""Regression tests for issue #76106: when EVERY build on the current
minor line (e.g. all of 3.11 on Windows) links a vulnerable SQLite,
the provisioner must fall forward to the next supported minor line
(3.12, then 3.13) -- first via a bare minor request, then via explicit
patches on that line -- instead of leaving the user stuck on every
`hermes update` with no path to a fixed runtime.
"""
@staticmethod
def _mapped_run(resolutions, fixed_versions, install_calls):
"""Fake subprocess.run/probe pair driven by explicit tables:
- *resolutions*: request string -> python_version tuple the probe
reports for that request (bare minors resolve like uv would).
- *fixed_versions*: set of version tuples that link FIXED SQLite;
everything else probes as vulnerable 3.50.4.
- *install_calls*: list collecting each `uv python install` request,
in order, so tests can assert the actual request sequence.
"""
from hermes_cli.sqlite_runtime import SQLiteRuntimeInfo
state: dict = {"requested": None}
def fake_run(cmd, **kwargs):
if "install" in cmd:
# cmd = [uv, "python", "install", <request>, ...]
state["requested"] = cmd[3]
state["generation"] = Path(kwargs["env"]["UV_PYTHON_INSTALL_DIR"])
install_calls.append(cmd[3])
return SimpleNamespace(returncode=0, stdout="", stderr="")
if "list" in cmd:
return SimpleNamespace(returncode=0, stdout="", stderr="")
# uv python find → a path inside the generation dir, tagged with
# the request that produced it so the probe can look it up.
python = state["generation"] / "cpython" / "bin" / "python3"
python.parent.mkdir(parents=True, exist_ok=True)
python.write_text(state["requested"] or "")
return SimpleNamespace(returncode=0, stdout=str(python), stderr="")
def fake_probe(python, **kwargs):
requested = Path(python).read_text()
version = resolutions[requested]
if version in fixed_versions:
return SQLiteRuntimeInfo(
executable=Path(python),
base_prefix=Path(python).parent.parent,
python_version=version, sqlite_version=(3, 53, 1),
sqlite_version_string="3.53.1", sqlite_source_id="fixed",
)
return SQLiteRuntimeInfo(
executable=Path(python),
base_prefix=Path(python).parent.parent,
python_version=version, sqlite_version=(3, 50, 4),
sqlite_version_string="3.50.4", sqlite_source_id="vulnerable",
)
return fake_run, fake_probe
@staticmethod
def _current_3_11_14():
from hermes_cli.sqlite_runtime import SQLiteRuntimeInfo
return SQLiteRuntimeInfo(
executable=Path("/venv/bin/python"), base_prefix=Path("/venv"),
python_version=(3, 11, 14), sqlite_version=(3, 50, 4),
sqlite_version_string="3.50.4", sqlite_source_id="old",
)
def test_explicit_patch_fallback_when_bare_next_minor_is_vulnerable(
self, tmp_path, monkeypatch
):
"""The review-gap scenario from #76252: the bare '3.12' request
resolves to a VULNERABLE 3.12 build, but an explicit 3.12 patch
links fixed SQLite -- the `_list_available_patches(..., '3.12', ...)`
fallback branch must run, skip the already-tried bare resolution,
and succeed via the explicit patch."""
import hermes_cli.managed_uv as managed_uv
install_calls = []
fake_run, fake_probe = self._mapped_run(
resolutions={
"3.11": (3, 11, 14), # bare current minor: vulnerable
"3.12": (3, 12, 11), # bare next minor: ALSO vulnerable
"3.12.10": (3, 12, 10), # explicit patch: fixed
},
fixed_versions={(3, 12, 10)},
install_calls=install_calls,
)
patch_lists = {
# No newer 3.11 patch exists (the Windows #76106 reality).
"3.11": [(3, 11, 14), (3, 11, 13)],
# Newest 3.12 is the same build the bare request resolved to.
"3.12": [(3, 12, 11), (3, 12, 10)],
}
monkeypatch.setattr(managed_uv.subprocess, "run", fake_run)
monkeypatch.setattr(managed_uv, "probe_sqlite_runtime", fake_probe)
monkeypatch.setattr(
managed_uv, "_list_available_patches",
lambda uv_bin, minor, **kw: patch_lists[minor],
)
result = managed_uv._install_safe_python_generation(
"uv", project_root=tmp_path, current=self._current_3_11_14()
)
assert result is not None, (
"Explicit-patch fallback on the next minor line must recover"
)
_, _, candidate = result
assert candidate.python_version == (3, 12, 10)
assert not candidate.wal_reset_vulnerable
# The actual uv-install request sequence: bare current minor, then
# bare next minor, then STRAIGHT to the fixed explicit patch --
# 3.12.11 must NOT be re-requested explicitly, because the bare
# '3.12' attempt already resolved to (and rejected) that build.
assert install_calls == ["3.11", "3.12", "3.12.10"]
def test_returns_none_with_bounded_attempts_when_all_minors_exhausted(
self, tmp_path, monkeypatch
):
"""When every build on every supported minor line (3.11-3.13) is
vulnerable, the provisioner must give up with None -- and the total
install workload must stay bounded by _MAX_PATCH_RETRIES per line."""
import hermes_cli.managed_uv as managed_uv
install_calls = []
resolutions = {"3.11": (3, 11, 14), "3.12": (3, 12, 30), "3.13": (3, 13, 30)}
patch_lists = {}
for minor in (11, 12, 13):
versions = [(3, minor, v) for v in range(30, 10, -1)] # 20 patches
patch_lists[f"3.{minor}"] = versions
for version in versions:
resolutions[".".join(str(p) for p in version)] = version
fake_run, fake_probe = self._mapped_run(
resolutions=resolutions, fixed_versions=set(),
install_calls=install_calls,
)
monkeypatch.setattr(managed_uv.subprocess, "run", fake_run)
monkeypatch.setattr(managed_uv, "probe_sqlite_runtime", fake_probe)
monkeypatch.setattr(
managed_uv, "_list_available_patches",
lambda uv_bin, minor, **kw: patch_lists[minor],
)
result = managed_uv._install_safe_python_generation(
"uv", project_root=tmp_path, current=self._current_3_11_14()
)
assert result is None, "Nothing fixed anywhere: must give up cleanly"
cap = managed_uv._MAX_PATCH_RETRIES
# Per line: one bare request + at most _MAX_PATCH_RETRIES explicit
# patches; three lines total (3.11, 3.12, 3.13) and nothing beyond
# 3.13 (requires-python is <3.14).
assert install_calls.count("3.11") == 1
assert install_calls.count("3.12") == 1
assert install_calls.count("3.13") == 1
assert not any(call.startswith("3.14") for call in install_calls)
for minor in (11, 12, 13):
explicit = [
call for call in install_calls
if call.startswith(f"3.{minor}.")
]
assert len(explicit) <= cap, (
f"3.{minor} explicit retries must be capped at {cap}: {explicit}"
)
assert len(install_calls) <= 3 * (1 + cap)
class TestListAvailablePatches:
"""Direct unit tests for _list_available_patches()'s JSON parsing,
against realistic `uv python list --all-versions --output-format json`
output (captured from a real uv 0.11.7 invocation)."""
SAMPLE_OUTPUT = (
'[{"key":"cpython-3.11.15-linux-x86_64-gnu","version":"3.11.15",'
'"version_parts":{"major":3,"minor":11,"patch":15},"path":null,'
'"symlink":null,"url":"https://example/cpython-3.11.15.tar.gz",'
'"os":"linux","variant":"default","implementation":"cpython",'
'"arch":"x86_64","libc":"gnu"},'
'{"key":"cpython-3.11.14-linux-x86_64-gnu","version":"3.11.14",'
'"version_parts":{"major":3,"minor":11,"patch":14},"path":null,'
'"symlink":null,"url":"https://example/cpython-3.11.14.tar.gz",'
'"os":"linux","variant":"default","implementation":"cpython",'
'"arch":"x86_64","libc":"gnu"},'
'{"key":"pypy-3.11.15-linux-x86_64-gnu","version":"3.11.15",'
'"version_parts":{"major":3,"minor":11,"patch":15},"path":null,'
'"symlink":null,"url":"https://example/pypy-3.11.15.tar.gz",'
'"os":"linux","variant":"default","implementation":"pypy",'
'"arch":"x86_64","libc":"gnu"}]'
)
def test_parses_and_sorts_newest_first(self, tmp_path, monkeypatch):
import hermes_cli.managed_uv as managed_uv
def fake_run(cmd, **kwargs):
return SimpleNamespace(returncode=0, stdout=self.SAMPLE_OUTPUT, stderr="")
monkeypatch.setattr(managed_uv.subprocess, "run", fake_run)
result = managed_uv._list_available_patches(
"uv", "3.11", cwd=tmp_path, env={}
)
assert result == [(3, 11, 15), (3, 11, 14)]
def test_subprocess_exception_returns_empty_list(self, tmp_path, monkeypatch):
import hermes_cli.managed_uv as managed_uv
def fake_run(cmd, **kwargs):
raise OSError("uv binary not found")
monkeypatch.setattr(managed_uv.subprocess, "run", fake_run)
assert managed_uv._list_available_patches("uv", "3.11", cwd=tmp_path, env={}) == []
# ---------------------------------------------------------------------------
# _refresh_managed_uv_catalog + provisioning retry (issue #72093)
# ---------------------------------------------------------------------------
class TestRefreshManagedUvCatalog:
"""The managed uv is UV_UNMANAGED_INSTALL'd, so `uv self update` is
disabled and its python-build-standalone catalog freezes at bootstrap
age. python-build-standalone re-releases the same patch versions with
fixed SQLite, so a stale catalog makes provisioning fail forever with
no newer patch number to retry (issue #72093)."""
def test_version_change_reports_true(self, tmp_path):
import hermes_cli.managed_uv as managed_uv
versions = iter(["uv 0.1.0", "uv 0.2.0"])
with patch("hermes_cli.managed_uv.get_hermes_home", return_value=tmp_path), \
patch("hermes_cli.managed_uv._install_uv"), \
patch(
"hermes_cli.managed_uv._uv_version_string",
side_effect=lambda _uv: next(versions),
):
# Host-native path: the refresh only acts on the managed binary,
# so the fixture must live at the real host's managed_uv_path()
# (uv on POSIX, uv.exe on Windows) — no platform fake needed.
uv_path = managed_uv.managed_uv_path()
_make_executable(uv_path)
assert managed_uv._refresh_managed_uv_catalog(str(uv_path)) is True
def test_installer_failure_reports_false(self, tmp_path):
import hermes_cli.managed_uv as managed_uv
with patch("hermes_cli.managed_uv.get_hermes_home", return_value=tmp_path), \
patch(
"hermes_cli.managed_uv._install_uv",
side_effect=RuntimeError("network down"),
):
uv_path = managed_uv.managed_uv_path()
_make_executable(uv_path)
assert managed_uv._refresh_managed_uv_catalog(str(uv_path)) is False
@pytest.mark.skipif(sys.platform == "win32",
reason="POSIX-only: fixtures build the bin/ (not Scripts/) venv layout")
class TestRepairRetriesAfterUvRefresh:
def _run_repair(self, tmp_path, *, refresh_result, second_attempt):
"""Drive repair with the first provisioning attempt failing."""
from hermes_cli.managed_uv import repair_vulnerable_runtime
root, live, sentinel = _make_runtime_install(tmp_path)
current = _runtime_info(live / "bin" / "python", (3, 50, 4))
attempts = []
def fake_install(uv_bin, *, project_root, current):
attempts.append(uv_bin)
if len(attempts) == 1:
return None
return second_attempt(project_root)
with patch(
"hermes_cli.managed_uv.probe_sqlite_runtime",
return_value=current,
), \
patch(
"hermes_cli.managed_uv._install_safe_python_generation",
side_effect=fake_install,
), \
patch(
"hermes_cli.managed_uv._refresh_managed_uv_catalog",
return_value=refresh_result,
) as mock_refresh, \
patch(
"hermes_cli.managed_uv._stage_candidate_venv",
side_effect=_CandidateStageError("candidate dependency sync failed (rc=1)"),
):
result = repair_vulnerable_runtime("uv", project_root=root)
return result, attempts, mock_refresh, sentinel
def test_no_retry_when_refresh_did_not_change_uv(self, tmp_path):
result, attempts, mock_refresh, sentinel = self._run_repair(
tmp_path,
refresh_result=False,
second_attempt=lambda root: None,
)
assert result.status == "failed"
assert len(attempts) == 1
mock_refresh.assert_called_once_with("uv")
assert sentinel.read_text(encoding="utf-8") == "live"
def test_retries_once_after_successful_refresh(self, tmp_path):
result, attempts, mock_refresh, sentinel = self._run_repair(
tmp_path,
refresh_result=True,
second_attempt=lambda root: None,
)
# Second attempt ran (and also failed) — exactly one retry, no loop.
assert result.status == "failed"
assert len(attempts) == 2
mock_refresh.assert_called_once_with("uv")
assert sentinel.read_text(encoding="utf-8") == "live"
def test_retry_success_proceeds_to_staging(self, tmp_path):
def second_attempt(root):
generation = root / ".hermes-runtime" / "python" / "generation-retry"
candidate_python = generation / "bin" / "python"
candidate_python.parent.mkdir(parents=True)
candidate_python.write_text("candidate", encoding="utf-8")
return generation, candidate_python, _runtime_info(
candidate_python, (3, 53, 1)
)
result, attempts, mock_refresh, sentinel = self._run_repair(
tmp_path,
refresh_result=True,
second_attempt=second_attempt,
)
# Provisioning succeeded on retry; staging rejects the candidate,
# proving the retry result flows into the normal pipeline.
assert result.status == "failed"
assert result.detail == "candidate dependency sync failed (rc=1)"
assert len(attempts) == 2
assert sentinel.read_text(encoding="utf-8") == "live"
class TestDefaultLiveVenv:
"""_default_live_venv() must cover BOTH install layouts (venv/ and .venv/).
Historically repair hardcoded venv/, so uv-default/.venv checkouts got
'not-applicable' on every hermes update and stayed on journal_mode=DELETE
(2,600x slower state.db appends) while the WAL warning promised repair.
"""
def _checkout(self, tmp_path, *dirs):
root = tmp_path / "checkout"
root.mkdir()
(root / "pyproject.toml").write_text("[project]\n", encoding="utf-8")
# Host-native venv layout: bin/python on POSIX, Scripts/python.exe on
# Windows — what _venv_python() resolves on the real host.
if sys.platform == "win32":
bin_dir_name, python_name = "Scripts", "python.exe"
else:
bin_dir_name, python_name = "bin", "python"
for d in dirs:
bin_dir = root / d / bin_dir_name
bin_dir.mkdir(parents=True)
(bin_dir / python_name).write_text("py", encoding="utf-8")
return root
def test_dot_venv_only_is_targeted(self, tmp_path):
from hermes_cli.managed_uv import _default_live_venv
root = self._checkout(tmp_path, ".venv")
assert _default_live_venv(root) == root / ".venv"
def test_managed_venv_takes_precedence(self, tmp_path):
from hermes_cli.managed_uv import _default_live_venv
root = self._checkout(tmp_path, "venv", ".venv")
assert _default_live_venv(root) == root / "venv"
def test_neither_layout_keeps_not_applicable(self, tmp_path):
from hermes_cli.managed_uv import (
_default_live_venv,
repair_vulnerable_runtime,
)
root = self._checkout(tmp_path)
# Neither venv nor .venv has an interpreter -> repair is not applicable.
assert _default_live_venv(root) == root / "venv"
result = repair_vulnerable_runtime("uv", project_root=root)
assert result.status == "not-applicable"
class TestVenvPythonUpdateBoundary:
"""``_venv_python`` must survive a hermes_constants predating its symbol.
``hermes update`` imports hermes_constants from the OLD checkout, ``git
pull`` replaces that file, and the freshly-pulled managed_uv then runs its
lazy ``from hermes_constants import venv_python_path`` against the module
object already cached in ``sys.modules``. That cached module has no such
symbol, so the import raises — while naming the NEW file on disk, which
plainly contains it, which is what made the error so confusing:
cannot import name 'venv_python_path' from 'hermes_constants'
(~/.hermes/hermes-agent/hermes_constants.py)
It aborted the managed-Python runtime repair on the first update from any
release older than the symbol. Same class as the ``ensure_uv()`` arity skew
documented on ``_UvResult``.
"""
def test_recovers_when_the_cached_module_predates_the_symbol(self, monkeypatch):
import hermes_constants
from hermes_cli.managed_uv import _venv_python
# The stale in-memory module: the symbol the new code wants is absent,
# exactly as on an install that booted the pre-upgrade checkout. The
# file on disk is the current one, so a reload recovers the real helper.
monkeypatch.delattr(hermes_constants, "venv_python_path", raising=False)
# Host-native: the subject is the reload-recovery seam, not the
# bin/Scripts mapping — assert whatever layout the real host resolves.
expected = Path("/opt/hermes/venv/Scripts/python.exe") \
if sys.platform == "win32" else Path("/opt/hermes/venv/bin/python")
assert _venv_python(Path("/opt/hermes/venv")) == expected
def test_recovery_uses_the_shared_helper_not_a_second_copy(self, monkeypatch):
"""The reload must resolve through hermes_constants, not open-code it.
Hand-rolling `Scripts`/`bin` here is what #76105 deduped away and what
`test_no_open_coded_venv_layout_remains_in_hermes_cli` bans.
"""
import hermes_constants
from hermes_cli.managed_uv import _venv_python
monkeypatch.delattr(hermes_constants, "venv_python_path", raising=False)
sentinel = Path("/sentinel/from/shared/helper")
real_reload = __import__("importlib").reload
def _reload_with_marker(module):
fresh = real_reload(module)
monkeypatch.setattr(
fresh, "venv_python_path", lambda *a, **k: sentinel, raising=False
)
return fresh
monkeypatch.setattr("importlib.reload", _reload_with_marker)
assert _venv_python(Path("/opt/hermes/venv")) == sentinel
def test_uses_the_real_helper_when_it_is_importable(self, monkeypatch):
"""The normal path never reloads — recovery stays a fallback."""
from hermes_cli.managed_uv import _venv_python
def _no_reload(module): # pragma: no cover - must not run
raise AssertionError("reload must not run when the import succeeds")
monkeypatch.setattr("importlib.reload", _no_reload)
expected = Path("/opt/hermes/venv/Scripts/python.exe") \
if sys.platform == "win32" else Path("/opt/hermes/venv/bin/python")
assert _venv_python(Path("/opt/hermes/venv")) == expected