Files
hermes-agent/tests/pm/test_uv_python.py
ethernet 8b7eae99ef fix(pm): own interpreter selection and dependency recovery
Pin uv and uvx to the PM interpreter instead of ambient Python discovery.
A matching dependency stamp cannot prove that installed files still exist.
Repair now rebuilds the recorded workspace and lock in a fresh generation,
checks startup imports, and publishes the selection only after success.

Run startup recovery before dependency activation. Keep manual PM repair
reachable when the selected environment is damaged. Preserve plugin
selection, retry ownership, and the previous generation on failure.
Remove the separate pip, ensurepip, per-extra, and install-time quarantine
ladders. Keep orphan launcher restoration.

Verification: 717 targeted tests passed on native Windows ARM64, with
56 skipped. Ruff, diff checks, and the source-scoped compat check passed.
A disposable real Hermes install recovered deleted YAML and dotenv files,
then printed CLI help with exit 0. Its lock and stamp stayed unchanged.
The full suite and a release build were not run for this change.
2026-09-08 23:39:55 -04:00

126 lines
5.5 KiB
Python

"""PM-owned uv commands use the installed interpreter, never host discovery."""
from __future__ import annotations
import importlib
import os
from pathlib import Path
import shutil
import subprocess
import sys
import pytest
from pm.lock import Facts, Lockfile
from pm.package import InstallError
from pm.packages import Python, Uv
from pm.store import current_target
@pytest.fixture
def installed_uv(tmp_path, monkeypatch):
import pm.paths as paths
import pm.registry as registry
uv = shutil.which("uv")
assert uv, "the interpreter selection contract requires real uv"
store = tmp_path / "store"
lock = Lockfile(tmp_path / "lock.json")
target = current_target()
digest = "1" * 64
entry = store / "uv"
entry.mkdir(parents=True)
binary = entry / ("uv.exe" if os.name == "nt" else "uv")
shutil.copy2(uv, binary)
uvx = Path(uv).with_name("uvx" + binary.suffix)
assert uvx.is_file(), "the real uv distribution must include uvx"
shutil.copy2(uvx, entry / uvx.name)
lock.set_pin("uv", "test", {target: {"url": "https://test.invalid/uv", "sha256": digest}})
lock.set_pin("python", "test", {target: {"url": "https://test.invalid/python", "sha256": digest}})
lock.save()
facts = Facts(store / "facts.json")
facts.record("uv", "test", entry.name, {}, store, target=target, artifacts=[digest])
monkeypatch.setattr(paths, "lockfile_path", lambda: lock.path)
monkeypatch.setattr(paths, "store_root", lambda: store)
monkeypatch.setattr(paths, "writable_store_root", lambda: store)
monkeypatch.setitem(registry._packages, "uv", Uv())
return tmp_path, binary, facts, target, digest
def test_all_uv_commands_keep_the_pm_interpreter(installed_uv, monkeypatch):
import pm.registry as registry
root, uv, facts, target, digest = installed_uv
selected = root / "store" / "selected-python"
clean = {key: value for key, value in os.environ.items() if not key.startswith("UV_")}
clean.update({"UV_NO_CONFIG": "1", "UV_OFFLINE": "1", "UV_PYTHON_DOWNLOADS": "never"})
subprocess.run([str(uv), "venv", "--python", sys.executable, str(selected)],
cwd=root, env=clean, check=True, capture_output=True, timeout=60)
python = selected / ("Scripts/python.exe" if os.name == "nt" else "bin/python")
class FixturePython(Python):
binary_rel = {"win32": "Scripts/python.exe", "posix": "bin/python"}
monkeypatch.setitem(registry._packages, "python", FixturePython())
facts.record("python", "test", selected.name, {}, selected.parent,
target=target, artifacts=[digest])
monkeypatch.setenv("UV_PYTHON", str(root / "ambient-python"))
monkeypatch.setenv("UV_PROJECT_ENVIRONMENT", str(root / "ambient-venv"))
before = dict(os.environ)
managed_uv, env = importlib.import_module("pm.ensure").uv(realize=False)
assert managed_uv == str(uv)
assert env.get("UV_PYTHON") == str(python)
assert "UV_PROJECT_ENVIRONMENT" not in env
assert dict(os.environ) == before
uvx, uvx_env = importlib.import_module("pm.ensure").uv("uvx", realize=False)
assert Path(uvx) == uv.with_name("uvx" + uv.suffix)
assert uvx_env["UV_PYTHON"] == str(python)
subprocess.run([uvx, "--version"], cwd=root, env=uvx_env, check=True, capture_output=True, timeout=30)
project = root / "project"
project.mkdir()
(project / "pyproject.toml").write_text(
'[project]\nname="pm-python-proof"\nversion="1"\nrequires-python=">=3.11"\n'
'[tool.uv]\npackage=false\n', encoding="utf-8",
)
(project / ".python-version").write_text(str(root / "host-only-python"), encoding="utf-8")
env.pop("UV_NO_CONFIG")
env.update({"UV_OFFLINE": "1", "UV_PYTHON_DOWNLOADS": "never"})
environment = root / "candidate"
env["VIRTUAL_ENV"] = str(environment)
for args in (["venv", str(environment)], ["lock"], ["sync", "--frozen", "--active"],
["run", "--active", "--no-sync", "python", "-c", "import sys; print(sys.prefix)"]):
result = subprocess.run([managed_uv, *args], cwd=project, env=env,
capture_output=True, text=True, check=True, timeout=60)
assert Path(result.stdout.strip()).resolve() == environment.resolve()
incomplete = root / "store" / "uv-without-uvx"
incomplete.mkdir()
shutil.copy2(uv, incomplete / uv.name)
facts.record("uv", "test", incomplete.name, {}, incomplete.parent,
target=target, artifacts=[digest])
ensure = importlib.import_module("pm.ensure")
assert ensure.uv("uvx", realize=False)[0] is None
with pytest.raises(InstallError, match="binary is missing"):
ensure.uv("uvx")
def test_uv_refuses_discovery_when_pm_python_is_missing(installed_uv, monkeypatch):
root, _, facts, target, digest = installed_uv
ensure = importlib.import_module("pm.ensure")
monkeypatch.setattr(ensure, "lazy_installs_allowed", lambda: False)
monkeypatch.setenv("UV_PYTHON", str(root / "ambient-python"))
managed_uv, env = ensure.uv(realize=False)
assert managed_uv is None
assert "UV_PYTHON" not in env
assert not (root / "store" / "python-test").exists()
with pytest.raises(InstallError, match="python"):
ensure.uv()
entry = root / "store" / "missing-binary"
entry.mkdir()
facts.record("python", "test", entry.name, {}, entry.parent,
target=target, artifacts=[digest])
assert ensure.uv(realize=False)[0] is None
with pytest.raises(InstallError, match="binary is missing"):
ensure.uv()