Files
aiturk-hermes-ide/tests/hermes_cli/test_managed_uv.py
T

1502 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
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
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):
_make_executable(tmp_path / "bin" / "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(tmp_path / "bin" / "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
# ---------------------------------------------------------------------------
# ensure_uv
# ---------------------------------------------------------------------------
class TestEnsureUv:
def test_installs_if_missing(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") as mock_install:
# Simulate the installer creating the binary
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(tmp_path / "bin" / "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.repair_vulnerable_runtime",
return_value=repair,
):
path = ensure_uv(repair_observer=observed.append)
assert path == str(tmp_path / "bin" / "uv")
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, monkeypatch):
"""A recent success stamp must skip `uv self update` entirely while the
vulnerable-runtime repair probe still runs (CVE repair is never gated)."""
from hermes_cli.managed_uv import RuntimeRepairResult, update_managed_uv
uv = tmp_path / "bin" / "uv"
_make_executable(uv)
# Fresh stamp under the isolated HERMES_HOME.
import hermes_constants
stamp = hermes_constants.get_hermes_home() / "cache" / ".uv_self_update_stamp"
stamp.parent.mkdir(parents=True, exist_ok=True)
stamp.touch()
with patch("hermes_cli.managed_uv.get_hermes_home", return_value=tmp_path), \
patch("hermes_cli.managed_uv.subprocess.run") as mock_run, \
patch(
"hermes_cli.managed_uv.repair_vulnerable_runtime",
return_value=RuntimeRepairResult("skipped"),
) as mock_repair:
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"
_make_executable(uv)
import hermes_constants
stamp = hermes_constants.get_hermes_home() / "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.repair_vulnerable_runtime", return_value=_RRR("not-applicable")), \
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_stage_candidate_sync_keeps_uv_project_config(self, tmp_path):
from hermes_cli.managed_uv import _stage_candidate_venv
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")
calls = []
def fake_run(argv, **kwargs):
calls.append((list(argv), kwargs.get("env")))
return MagicMock(returncode=0)
with patch("hermes_cli.managed_uv.subprocess.run", side_effect=fake_run), \
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(calls) == 2
venv_argv, venv_env = calls[0]
sync_argv, sync_env = calls[1]
assert venv_argv[:2] == ["uv", "venv"]
assert "--no-config" in venv_argv
assert venv_env.get("UV_NO_CONFIG") == "1"
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_env
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",
return_value=None,
):
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}"
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_posix_sets_uv_unmanaged_install(self, tmp_path):
target = tmp_path / "bin" / "uv"
with patch("hermes_cli.managed_uv._install_uv_posix") as mock_posix:
from hermes_cli.managed_uv import _install_uv
_install_uv(target)
mock_posix.assert_called_once()
call_env = mock_posix.call_args[0][0]
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",
return_value=None,
):
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 (mocked to None) is what
# failed — proving the retry result flows into the normal pipeline.
assert result.status == "failed"
assert "replacement environment" in result.detail
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")
for d in dirs:
bin_dir = root / d / "bin"
bin_dir.mkdir(parents=True)
(bin_dir / "python").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
class TestWindowsRuntimeSelfLock:
"""The repair pre-flight must see the ONE holder the generic scan hides:
the updater itself (#93032).
A CLI ``hermes update`` runs from the venv's own python, and
``_detect_venv_python_processes`` excludes the calling process and its
ancestors on purpose (correct for the dependency-sync path). For the
whole-venv park rename that exemption is fatal on Windows: a directory
containing an executable mapped by a running process cannot be renamed,
so the cutover retries burn out against a lock that cannot be released
while the updater lives. The repair must detect the self-lock and defer
with honest guidance instead of provisioning a candidate for a doomed
rename.
"""
def _checkout(self, tmp_path):
root, live, sentinel = _make_runtime_install(tmp_path)
# Windows-layout interpreter so sys.executable can point inside the
# live venv on any host (the detector only string-compares paths).
scripts_python = live / "Scripts" / "python.exe"
scripts_python.parent.mkdir(parents=True, exist_ok=True)
scripts_python.write_text("live interpreter", encoding="utf-8")
return root, live, sentinel, scripts_python
def test_self_lock_defers_repair_before_provisioning(
self, tmp_path, monkeypatch, capsys
):
"""Regression for #93032: pre-fix, the repair walks straight into the
doomed rename (provisioning + cutover) whenever the updater itself
maps the live venv; the park then fails with WinError 5 and the user
gets the misleading 'next update will retry' message forever."""
from hermes_cli import managed_uv
from hermes_cli.managed_uv import repair_vulnerable_runtime
root, live, sentinel, scripts_python = self._checkout(tmp_path)
current = _runtime_info(scripts_python, (3, 50, 4))
monkeypatch.setattr(managed_uv.platform, "system", lambda: "Windows")
monkeypatch.setattr(sys, "executable", str(scripts_python))
with patch(
"hermes_cli.managed_uv._windows_runtime_holders",
return_value=(False, ""),
), \
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 == "skipped"
assert "live venv" in result.detail
assert mock_install.call_count == 0, (
"a self-locked updater must not provision a candidate it can "
"never cut over"
)
assert sentinel.read_text(encoding="utf-8") == "live"
assert not (root / ".hermes-runtime").exists()
out = capsys.readouterr().out
assert "SQLite runtime repair deferred" in out
assert "will retry" not in out, (
"the structural self-lock must not promise that retrying helps"
)
assert "outside" in out, "the deferral must point at an escape hatch"
def test_non_self_locked_repair_proceeds(self, tmp_path, monkeypatch):
"""The guard must fail OPEN when the updater runs from outside the
venv — an always-firing deferral would recreate the never-converging
loop this fix removes (#86735 class)."""
from hermes_cli import managed_uv
from hermes_cli.managed_uv import repair_vulnerable_runtime
root, live, sentinel, scripts_python = self._checkout(tmp_path)
current = _runtime_info(scripts_python, (3, 50, 4))
monkeypatch.setattr(managed_uv.platform, "system", lambda: "Windows")
monkeypatch.setattr(
sys, "executable", str(tmp_path / "outside" / "python.exe")
)
with patch(
"hermes_cli.managed_uv._windows_runtime_holders",
return_value=(False, ""),
), \
patch(
"hermes_cli.managed_uv.probe_sqlite_runtime",
return_value=current,
), \
patch(
"hermes_cli.managed_uv._install_safe_python_generation",
return_value=None,
) as mock_install:
result = repair_vulnerable_runtime("uv", project_root=root)
assert result.status == "failed"
assert "provision" in result.detail
mock_install.assert_called_once()
assert sentinel.read_text(encoding="utf-8") == "live"
def test_self_lock_is_a_noop_off_windows(self, tmp_path, monkeypatch):
"""POSIX renames work while the updater maps the venv, so the guard
must stay Windows-only."""
from hermes_cli import managed_uv
root, live, sentinel, scripts_python = self._checkout(tmp_path)
monkeypatch.setattr(managed_uv.platform, "system", lambda: "Linux")
monkeypatch.setattr(sys, "executable", str(scripts_python))
locked, detail = managed_uv._windows_runtime_self_lock(live)
assert (locked, detail) == (False, "")
def test_venv_launcher_ancestor_is_a_self_lock(self, tmp_path, monkeypatch):
r"""The venv\Scripts\hermes.exe shim stays mapped while it waits for
this child — an ancestor running from the venv blocks the rename too."""
from hermes_cli import managed_uv
root, live, sentinel, scripts_python = self._checkout(tmp_path)
monkeypatch.setattr(managed_uv.platform, "system", lambda: "Windows")
monkeypatch.setattr(
sys, "executable", str(tmp_path / "outside" / "python.exe")
)
class _FakeProc:
def __init__(self, pid, exe):
self.pid = pid
self._exe = exe
def exe(self):
return self._exe
fake_psutil = SimpleNamespace(
Process=lambda: SimpleNamespace(
parents=lambda: [_FakeProc(999, str(scripts_python))],
),
)
with patch.dict(sys.modules, {"psutil": fake_psutil}):
locked, detail = managed_uv._windows_runtime_self_lock(live)
assert locked
assert "999" in detail