fix(install): reinstall a managed uv that no longer runs
`_ensure_uv_path` accepted `$HERMES_HOME/bin/uv[.exe]` on is_file()+X_OK alone, so a dead relocated launcher a pre-fix installer had salvaged there was handed to `hermes update` and doctor/repair forever; the only self-heal lived in install.ps1. Probe it with `uv --version` (exit code, OSError = dead) and fall through to the existing `_install_uv` when it fails. Invariant test: an executable exit-1 stand-in is replaced (red before: `_install_uv` called 0 times; green after). The Windows plain-str test answers the probe for its shell-script `uv.exe` stand-in.
This commit is contained in:
@@ -181,7 +181,7 @@ def _ensure_uv_path(
|
||||
*, repair_observer: Callable[[RuntimeRepairResult], None] | None = None) -> Optional[str]:
|
||||
"""Resolve the managed uv path, installing it if necessary (plain ``str``/``None``)."""
|
||||
existing = resolve_uv()
|
||||
if existing:
|
||||
if existing and _uv_runs(existing):
|
||||
return existing
|
||||
target = managed_uv_path()
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
@@ -203,6 +203,16 @@ def _ensure_uv_path(
|
||||
return result
|
||||
|
||||
|
||||
def _uv_runs(uv_bin: str) -> bool:
|
||||
"""``uv --version`` exits 0. A pre-fix installer could salvage a relocated Chocolatey/Scoop shim into
|
||||
``$HERMES_HOME/bin``: it is a file with the executable bit that never runs, so is_file()+X_OK
|
||||
alone would keep handing it out forever instead of reinstalling."""
|
||||
try:
|
||||
return subprocess.run([uv_bin, "--version"], capture_output=True, check=False).returncode == 0
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
|
||||
def _uv_version(uv_bin: str) -> str:
|
||||
return subprocess.run(
|
||||
[uv_bin, "--version"],
|
||||
|
||||
@@ -27,7 +27,7 @@ _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.write_text("#!/bin/sh\necho uv 0.1.2\n", encoding="utf-8")
|
||||
path.chmod(path.stat().st_mode | stat.S_IEXEC)
|
||||
|
||||
|
||||
@@ -176,7 +176,7 @@ class TestResolveUv:
|
||||
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")
|
||||
uv.write_text("not a binary", encoding="utf-8")
|
||||
# Ensure no execute bit
|
||||
uv.chmod(0o644)
|
||||
with patch("hermes_cli.managed_uv.get_hermes_home", return_value=tmp_path):
|
||||
@@ -222,6 +222,21 @@ class TestEnsureUv:
|
||||
assert path == str(uv)
|
||||
mock_install.assert_called_once()
|
||||
|
||||
def test_dead_managed_launcher_is_reinstalled(self, tmp_path):
|
||||
"""A managed uv that exists, is executable, and exits non-zero (a relocated shim a pre-fix
|
||||
installer copied in) must be replaced, not handed out."""
|
||||
uv = tmp_path / "bin" / _UV_BINARY_NAME
|
||||
uv.parent.mkdir(parents=True)
|
||||
uv.write_text("#!/bin/sh\necho 'launcher target missing' >&2\nexit 1\n", encoding="utf-8")
|
||||
uv.chmod(uv.stat().st_mode | stat.S_IEXEC)
|
||||
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", side_effect=_make_executable) as mock_install:
|
||||
from hermes_cli.managed_uv import ensure_uv
|
||||
assert ensure_uv() == str(uv)
|
||||
mock_install.assert_called_once_with(uv)
|
||||
|
||||
def test_install_reports_runtime_repair_to_observer(self, tmp_path):
|
||||
from hermes_cli.managed_uv import (
|
||||
RuntimeRepairResult,
|
||||
@@ -339,9 +354,11 @@ class TestEnsureUvWindowsSafe:
|
||||
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.
|
||||
# On Windows the managed binary is uv.exe (a shell-script stand-in cannot run there, so the
|
||||
# launcher probe is answered for it).
|
||||
_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._uv_runs", return_value=True), \
|
||||
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()
|
||||
@@ -902,11 +919,11 @@ class TestPatchRetryOnVulnerableCandidate:
|
||||
# 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 "")
|
||||
python.write_text(state["requested"] or "", encoding="utf-8")
|
||||
return SimpleNamespace(returncode=0, stdout=str(python), stderr="")
|
||||
|
||||
def fake_probe(python, **kwargs):
|
||||
requested = Path(python).read_text()
|
||||
requested = Path(python).read_text(encoding="utf-8")
|
||||
# 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":
|
||||
@@ -1056,11 +1073,11 @@ class TestMinorLineFallForward:
|
||||
# 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 "")
|
||||
python.write_text(state["requested"] or "", encoding="utf-8")
|
||||
return SimpleNamespace(returncode=0, stdout=str(python), stderr="")
|
||||
|
||||
def fake_probe(python, **kwargs):
|
||||
requested = Path(python).read_text()
|
||||
requested = Path(python).read_text(encoding="utf-8")
|
||||
version = resolutions[requested]
|
||||
if version in fixed_versions:
|
||||
return SQLiteRuntimeInfo(
|
||||
|
||||
Reference in New Issue
Block a user