"""Tests for hermes_cli.managed_uv — one path, no guessing.""" from __future__ import annotations import os import stat from pathlib import Path 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) # --------------------------------------------------------------------------- # managed_uv_path # --------------------------------------------------------------------------- class TestManagedUvPath: def test_posix(self, tmp_path): with patch("hermes_cli.managed_uv.get_hermes_home", return_value=tmp_path), \ patch("hermes_cli.managed_uv.platform.system", return_value="Linux"): from hermes_cli.managed_uv import managed_uv_path assert managed_uv_path() == tmp_path / "bin" / "uv" def test_windows(self, tmp_path): with patch("hermes_cli.managed_uv.get_hermes_home", return_value=tmp_path), \ patch("hermes_cli.managed_uv.platform.system", return_value="Windows"): from hermes_cli.managed_uv import managed_uv_path assert managed_uv_path() == tmp_path / "bin" / "uv.exe" # --------------------------------------------------------------------------- # resolve_uv # --------------------------------------------------------------------------- class TestResolveUv: def test_missing_returns_none(self, tmp_path): 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 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_already_installed_no_bootstrap(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 ensure_uv path, fresh = ensure_uv() assert path == str(tmp_path / "bin" / "uv") assert fresh is False def test_installs_if_missing_sets_bootstrap_flag(self, tmp_path): with patch("hermes_cli.managed_uv.get_hermes_home", return_value=tmp_path), \ 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, fresh = ensure_uv() assert path == str(tmp_path / "bin" / "uv") assert fresh is True mock_install.assert_called_once() def test_install_failure_returns_none_false(self, tmp_path): 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")): from hermes_cli.managed_uv import ensure_uv path, fresh = ensure_uv() assert path is None assert fresh is False # --------------------------------------------------------------------------- # rebuild_venv # --------------------------------------------------------------------------- class TestRebuildVenv: def test_removes_old_venv_and_creates_new(self, tmp_path): venv_dir = tmp_path / "venv" venv_dir.mkdir() (venv_dir / "old_file").write_text("stale") uv_bin = str(tmp_path / "bin" / "uv") def fake_run(cmd, **kwargs): m = MagicMock(returncode=0) if cmd[1] == "venv": # Simulate uv creating the venv dir venv_dir.mkdir(exist_ok=True) bin_dir = venv_dir / "bin" bin_dir.mkdir(parents=True, exist_ok=True) (bin_dir / "python").write_text("#!/bin/sh\necho Python 3.11.0") elif "--version" in cmd: m.stdout = "Python 3.11.0" return m with patch("hermes_cli.managed_uv.subprocess.run", side_effect=fake_run), \ patch("hermes_cli.managed_uv.shutil.rmtree") as mock_rmtree: from hermes_cli.managed_uv import rebuild_venv result = rebuild_venv(uv_bin, venv_dir) assert result is True mock_rmtree.assert_called_once_with(venv_dir, ignore_errors=True) def test_rebuild_failure_returns_false(self, tmp_path): venv_dir = tmp_path / "venv" uv_bin = str(tmp_path / "bin" / "uv") with patch("hermes_cli.managed_uv.subprocess.run") as mock_run, \ patch("hermes_cli.managed_uv.shutil.rmtree"): mock_run.return_value = MagicMock(returncode=1, stderr="nope") from hermes_cli.managed_uv import rebuild_venv result = rebuild_venv(uv_bin, venv_dir) assert result is False def test_retries_with_clear_when_dir_already_exists(self, tmp_path): """On Windows, rmtree can silently fail when an open handle holds a file in the venv (running hermes.exe, gateway, AV scanner). uv then refuses with ``Caused by: A directory already exists at: venv``. Make sure we don't give up — retry with ``--clear`` to force uv past the stale directory and rebuild successfully.""" venv_dir = tmp_path / "venv" venv_dir.mkdir() (venv_dir / "stale_open_handle").write_text("rmtree couldn't delete me") uv_bin = str(tmp_path / "bin" / "uv") call_log: list[list[str]] = [] def fake_run(cmd, **kwargs): call_log.append(list(cmd)) m = MagicMock() if cmd[1] == "venv" and "--clear" not in cmd: # First attempt: uv refuses because dir still exists m.returncode = 1 m.stderr = ( "error: Failed to create virtual environment\n" " Caused by: A directory already exists at: venv\n" "hint: Use the `--clear` flag or set `UV_VENV_CLEAR=1` to replace the existing directory\n" ) m.stdout = "" return m if cmd[1] == "venv" and "--clear" in cmd: # Retry: succeeds. Simulate uv writing the python shim. m.returncode = 0 m.stderr = "" m.stdout = "" bin_dir = venv_dir / ("Scripts" if os.name == "nt" else "bin") bin_dir.mkdir(parents=True, exist_ok=True) python_name = "python.exe" if os.name == "nt" else "python" (bin_dir / python_name).write_text("#!/bin/sh\necho Python 3.11.0") return m if "--version" in cmd: m.returncode = 0 m.stdout = "Python 3.11.0" m.stderr = "" return m m.returncode = 0 return m with patch("hermes_cli.managed_uv.subprocess.run", side_effect=fake_run), \ patch("hermes_cli.managed_uv.shutil.rmtree"): from hermes_cli.managed_uv import rebuild_venv result = rebuild_venv(uv_bin, venv_dir) assert result is True, "rebuild should succeed after --clear retry" # We expect exactly two ``uv venv`` calls: one without --clear, one with. venv_calls = [c for c in call_log if len(c) >= 2 and c[1] == "venv"] assert len(venv_calls) == 2, f"expected 2 venv calls, got {venv_calls}" assert "--clear" not in venv_calls[0], "first call should not pass --clear" assert "--clear" in venv_calls[1], "retry must pass --clear" def test_does_not_retry_when_first_failure_is_not_dir_exists(self, tmp_path): """If uv venv fails for some other reason (e.g. interpreter download failed, disk full), we should NOT silently retry with --clear — that would mask a real problem. Just surface the original failure.""" venv_dir = tmp_path / "venv" uv_bin = str(tmp_path / "bin" / "uv") call_log: list[list[str]] = [] def fake_run(cmd, **kwargs): call_log.append(list(cmd)) m = MagicMock(returncode=1, stderr="error: No space left on device", stdout="") return m with patch("hermes_cli.managed_uv.subprocess.run", side_effect=fake_run), \ patch("hermes_cli.managed_uv.shutil.rmtree"): from hermes_cli.managed_uv import rebuild_venv result = rebuild_venv(uv_bin, venv_dir) assert result is False venv_calls = [c for c in call_log if len(c) >= 2 and c[1] == "venv"] assert len(venv_calls) == 1, "should not retry on non-dir-exists failures" assert "--clear" not in venv_calls[0] # --------------------------------------------------------------------------- # update_managed_uv # --------------------------------------------------------------------------- class TestUpdateManagedUv: def test_no_uv_returns_none(self, tmp_path): with patch("hermes_cli.managed_uv.get_hermes_home", return_value=tmp_path): from hermes_cli.managed_uv import update_managed_uv assert update_managed_uv() is None def test_self_update_success(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.subprocess.run") as mock_run: # uv self update succeeds mock_run.return_value = MagicMock(returncode=0, stdout="uv 0.2.0") from hermes_cli.managed_uv import update_managed_uv result = update_managed_uv() assert result == str(tmp_path / "bin" / "uv") # First call is self update, second is --version assert mock_run.call_count == 2 assert mock_run.call_args_list[0][0][0] == [str(tmp_path / "bin" / "uv"), "self", "update"] def test_self_update_failure_non_fatal(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.subprocess.run") as mock_run: mock_run.return_value = MagicMock(returncode=1, stderr="nope") from hermes_cli.managed_uv import update_managed_uv result = update_managed_uv() # Still returns the path — failure is non-fatal assert result == str(tmp_path / "bin" / "uv") # --------------------------------------------------------------------------- # _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") def test_windows_sets_uv_install_dir(self, tmp_path): target = tmp_path / "bin" / "uv.exe" with patch("hermes_cli.managed_uv.platform.system", return_value="Windows"), \ patch("hermes_cli.managed_uv._install_uv_windows") as mock_windows: from hermes_cli.managed_uv import _install_uv _install_uv(target) mock_windows.assert_called_once() call_env = mock_windows.call_args[0][0] assert call_env["UV_INSTALL_DIR"] == str(tmp_path / "bin")