refactor(uv): single managed-uv path, delete fts5 installer escalation

Replace the multi-path UV resolution chain (PATH probing, conda guards,
5-location trust ordering, temp-dir fallback installs) with a single
managed uv binary at $HERMES_HOME/bin/uv. Every code path that needs
uv resolves it from that one location; if missing, ensure_uv()
bootstraps it via the official standalone installer.

Key changes:

- New hermes_cli/managed_uv.py: managed_uv_path(), resolve_uv(),
  ensure_uv() (returns (path, freshly_bootstrapped) tuple),
  update_managed_uv(), rebuild_venv(), installer internals.
- hermes_cli/main.py: replace all shutil.which('uv') with ensure_uv(),
  add venv rebuild on first-time managed uv bootstrap, update_managed_uv
  before dep install on all 3 update paths.
- scripts/install.sh: install_uv() always installs to
  $HERMES_HOME/bin/uv; delete ensure_fts5, _python_has_fts5,
  _reinstall_python_with_fts5, _warn_no_fts5 (61 lines).
  Managed uv always installs current Python with FTS5.
- scripts/install.ps1: Install-Uv always installs to
  $HermesHome\bin\uv.exe; Resolve-UvCmd checks managed location first.
- hermes_state.py: simplified FTS5 warning now suggests 'hermes update'
  as the fix instead of blaming install method.
- tests: 15 tests in test_managed_uv.py, autouse _patch_managed_uv
  fixture in test_cmd_update.py.

Closes #37605, Closes #37622
This commit is contained in:
ethernet
2026-06-02 20:29:54 -04:00
parent a51a7b9b92
commit 4df280d511
7 changed files with 578 additions and 231 deletions
+36
View File
@@ -39,6 +39,42 @@ def mock_args():
return SimpleNamespace()
# ---------------------------------------------------------------------------
# Managed-uv compatibility for tests that patch shutil.which
# ---------------------------------------------------------------------------
# The production code now uses ``ensure_uv()`` / ``update_managed_uv()``
# instead of ``shutil.which("uv")``. Many tests in this file patch
# ``shutil.which`` to control whether uv is "available" — these autouse
# fixtures make the managed_uv functions delegate to the patched
# ``shutil.which`` so the existing test setup keeps working without
# per-test changes.
@pytest.fixture(autouse=True)
def _patch_managed_uv(request):
"""Make managed_uv helpers follow shutil.which mocking in tests."""
import shutil
# resolve_uv delegates to shutil.which("uv") so that test patches
# on shutil.which flow through naturally.
def _fake_resolve_uv():
return shutil.which("uv")
def _fake_ensure_uv():
path = shutil.which("uv")
return (path, False) # never freshly bootstrapped in tests
def _fake_update_managed_uv():
return None # never actually self-update in tests
def _fake_rebuild_venv(*args, **kwargs):
return True # no-op in tests
with patch("hermes_cli.managed_uv.resolve_uv", side_effect=_fake_resolve_uv), \
patch("hermes_cli.managed_uv.ensure_uv", side_effect=_fake_ensure_uv), \
patch("hermes_cli.managed_uv.update_managed_uv", side_effect=_fake_update_managed_uv), \
patch("hermes_cli.managed_uv.rebuild_venv", side_effect=_fake_rebuild_venv):
yield
class TestCmdUpdatePip:
"""Regression tests for pip-install update flows."""