Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions hermes_cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -7834,7 +7834,8 @@ def _update_via_zip(args):
# may point to a Python without FTS5. Rebuild it so the new managed
# uv provides a fresh interpreter with FTS5 guaranteed.
if fresh_bootstrap and uv_bin:
rebuild_venv(uv_bin, PROJECT_ROOT / "venv")
if not rebuild_venv(uv_bin, PROJECT_ROOT / "venv"):
raise RuntimeError("venv rebuild failed; aborting update before dependency install")

pip_cmd = [sys.executable, "-m", "pip"]
if not uv_bin:
Expand Down Expand Up @@ -10036,7 +10037,8 @@ def _cmd_update_impl(args, gateway_mode: bool):
# may point to a Python without FTS5. Rebuild it so the new managed
# uv provides a fresh interpreter with FTS5 guaranteed.
if fresh_bootstrap and uv_bin:
rebuild_venv(uv_bin, PROJECT_ROOT / "venv")
if not rebuild_venv(uv_bin, PROJECT_ROOT / "venv"):
raise RuntimeError("venv rebuild failed; aborting update before dependency install")

pip_cmd = [sys.executable, "-m", "pip"]
if not uv_bin:
Expand Down
6 changes: 5 additions & 1 deletion hermes_cli/managed_uv.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,13 +111,17 @@ def rebuild_venv(uv_bin: str, venv_dir: Path, python_version: str = "3.11") -> b
shutil.rmtree(venv_dir, ignore_errors=True)

result = subprocess.run(
[uv_bin, "venv", str(venv_dir), "--python", python_version],
[uv_bin, "venv", str(venv_dir), "--python", python_version, "--clear"],
capture_output=True,
text=True,
check=False,
)
if result.returncode == 0:
venv_python = venv_dir / ("Scripts" if platform.system() == "Windows" else "bin") / "python"
if not venv_python.exists():
logger.warning("venv rebuild reported success but %s is missing", venv_python)
print(f" ✗ venv rebuild failed: Python interpreter missing at {venv_python}")
return False
py_ver = subprocess.run(
[str(venv_python), "--version"],
capture_output=True,
Expand Down
15 changes: 15 additions & 0 deletions tests/hermes_cli/test_managed_uv.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,8 +114,10 @@ def test_removes_old_venv_and_creates_new(self, tmp_path):
(venv_dir / "old_file").write_text("stale")

uv_bin = str(tmp_path / "bin" / "uv")
commands = []

def fake_run(cmd, **kwargs):
commands.append(cmd)
m = MagicMock(returncode=0)
if cmd[1] == "venv":
# Simulate uv creating the venv dir
Expand All @@ -133,6 +135,19 @@ def fake_run(cmd, **kwargs):
result = rebuild_venv(uv_bin, venv_dir)
assert result is True
mock_rmtree.assert_called_once_with(venv_dir, ignore_errors=True)
assert commands[0] == [uv_bin, "venv", str(venv_dir), "--python", "3.11", "--clear"]

def test_rebuild_success_without_python_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=0, stdout="")
from hermes_cli.managed_uv import rebuild_venv
result = rebuild_venv(uv_bin, venv_dir)
assert result is False
assert mock_run.call_count == 1

def test_rebuild_failure_returns_false(self, tmp_path):
venv_dir = tmp_path / "venv"
Expand Down
31 changes: 31 additions & 0 deletions tests/hermes_cli/test_update_autostash.py
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,37 @@ def fake_run(cmd, **kwargs):
assert ".[all]" in install_cmds[0]


def test_cmd_update_aborts_when_fresh_managed_uv_rebuild_fails(monkeypatch, tmp_path):
"""A failed fresh managed-uv venv rebuild must not continue into pip install."""
_setup_update_mocks(monkeypatch, tmp_path)
monkeypatch.setattr("shutil.which", lambda name: "/usr/bin/uv" if name == "uv" else None)
monkeypatch.setattr(hermes_main, "_is_termux_env", lambda env=None: False)

recorded = []

def fake_run(cmd, **kwargs):
recorded.append(cmd)
if cmd == ["git", "fetch", "origin"]:
return SimpleNamespace(stdout="", stderr="", returncode=0)
if cmd == ["git", "rev-parse", "--abbrev-ref", "HEAD"]:
return SimpleNamespace(stdout="main\n", stderr="", returncode=0)
if cmd == ["git", "rev-list", "HEAD..origin/main", "--count"]:
return SimpleNamespace(stdout="1\n", stderr="", returncode=0)
if cmd == ["git", "pull", "--ff-only", "origin", "main"]:
return SimpleNamespace(stdout="Updating\n", stderr="", returncode=0)
return SimpleNamespace(returncode=0, stdout="", stderr="")

monkeypatch.setattr(hermes_main.subprocess, "run", fake_run)

with patch("hermes_cli.managed_uv.ensure_uv", return_value=("/usr/bin/uv", True)), \
patch("hermes_cli.managed_uv.rebuild_venv", return_value=False), \
pytest.raises(RuntimeError, match="venv rebuild failed"):
hermes_main.cmd_update(SimpleNamespace())

install_cmds = [c for c in recorded if "pip" in c and "install" in c]
assert install_cmds == []


def test_install_with_optional_fallback_honors_custom_group(monkeypatch):
"""Termux update path should target .[termux-all] when requested."""
calls = []
Expand Down