From 4ae28a685a252c0780b38dd2cdd7b9757ee5e2a5 Mon Sep 17 00:00:00 2001 From: Igor Tarasenko Date: Thu, 7 May 2026 18:35:00 +0200 Subject: [PATCH] fix(bootstrap): note Windows fallback + add symlinks regression test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses review feedback on PR #1815: 1. Extend the inline comment to note that CPython's venv falls back to copy mode when symlink creation fails (e.g. older Windows without SeCreateSymbolicLinkPrivilege), so symlinks=True is safe to set unconditionally — no platform branching needed. 2. Add a regression test that asserts EnvBuilder is called with symlinks=True. Cheap insurance against a future "simplify" pass removing the flag without realising it's load-bearing on macOS. --- bootstrap.py | 3 +++ tests/test_bootstrap_python_selection.py | 34 ++++++++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/bootstrap.py b/bootstrap.py index 3dfcd05e..fde976b0 100644 --- a/bootstrap.py +++ b/bootstrap.py @@ -185,6 +185,9 @@ def ensure_python_has_webui_deps(python_exe: str, agent_dir: Path | None = None) # so the venv binary aborts with SIGABRT on first import because the # dylib never gets copied into .venv/lib. Symlinking the interpreter # keeps @executable_path resolving back to the original install. + # CPython's venv falls back to copy mode automatically when symlink + # creation fails (e.g. older Windows without SeCreateSymbolicLinkPrivilege), + # so this is safe to set unconditionally. venv.EnvBuilder(with_pip=True, symlinks=True).create(venv_dir) info("Installing WebUI dependencies into local virtualenv") diff --git a/tests/test_bootstrap_python_selection.py b/tests/test_bootstrap_python_selection.py index 72d24732..4fc8e44e 100644 --- a/tests/test_bootstrap_python_selection.py +++ b/tests/test_bootstrap_python_selection.py @@ -1,4 +1,5 @@ import pathlib +from unittest.mock import patch import bootstrap @@ -61,3 +62,36 @@ def test_ensure_python_fails_loudly_when_no_interpreter_can_import_agent(monkeyp assert "cannot import both WebUI dependencies and Hermes Agent" in str(exc) else: raise AssertionError("expected RuntimeError") + + +def test_local_venv_is_created_with_symlinks(monkeypatch, tmp_path): + """Regression: mise/asdf macOS Pythons need symlinks=True to avoid SIGABRT. + + Their copy-mode venv produces a python binary referencing + @executable_path/../lib/libpython3.X.dylib that never gets copied into the + new .venv. Symlinking keeps @executable_path resolving back to the original + install. CPython's venv falls back to copy mode if symlink creation fails, + so this is safe to set unconditionally. + """ + local_python = tmp_path / "webui" / ".venv" / "bin" / "python" + monkeypatch.setattr(bootstrap, "REPO_ROOT", tmp_path) + monkeypatch.setattr(bootstrap, "_python_can_run_webui_and_agent", lambda *a, **k: False) + monkeypatch.setattr(bootstrap.subprocess, "run", lambda *a, **k: None) + + with patch.object(bootstrap.venv, "EnvBuilder") as mock_builder: + # Make EnvBuilder().create() materialize the venv python so the post-create + # `_python_can_run_webui_and_agent` retry path doesn't trip on a missing file. + venv_python = tmp_path / ".venv" / "bin" / "python" + + def fake_create(target): + venv_python.parent.mkdir(parents=True, exist_ok=True) + venv_python.write_text("", encoding="utf-8") + + mock_builder.return_value.create.side_effect = fake_create + + try: + bootstrap.ensure_python_has_webui_deps(str(local_python), None) + except RuntimeError: + pass # expected — fake _python_can_run_webui_and_agent always returns False + + mock_builder.assert_called_once_with(with_pip=True, symlinks=True)