117 lines
4.5 KiB
Python
117 lines
4.5 KiB
Python
"""Uninstall must not leave a dangling ``hermes`` command on Windows.
|
|
|
|
Every uninstall mode deletes the code checkout, but the launchers install.ps1
|
|
staged in the managed binary dir (the default Hermes root's ``bin``, shared
|
|
with the managed uv) live outside it. A surviving launcher makes ``hermes``
|
|
in a new terminal resolve and then error on its missing venv target — worse
|
|
than command-not-found. The managed uv next to them must survive keep-data
|
|
uninstalls, so the PATH sweep takes the ``bin`` entry only on a full wipe.
|
|
|
|
Platform verdicts are injected parameters (input→output, not host fakes).
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from hermes_cli import uninstall
|
|
from hermes_cli._install_repair import _WINDOWS_BIN_LAUNCHERS
|
|
|
|
|
|
@pytest.fixture
|
|
def managed_bin(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
|
|
"""Default-root ``bin`` holding launchers of both forms plus managed uv."""
|
|
home = tmp_path / "hermes"
|
|
bin_dir = home / "bin"
|
|
bin_dir.mkdir(parents=True)
|
|
(bin_dir / "hermes.exe").write_bytes(b"MZ launcher")
|
|
(bin_dir / "hermes-acp.cmd").write_text("@echo off\r\n", encoding="ascii")
|
|
(bin_dir / "uv.exe").write_bytes(b"MZ managed uv")
|
|
(bin_dir / "uvx.exe").write_bytes(b"MZ managed uvx")
|
|
monkeypatch.setenv("HERMES_HOME", str(home))
|
|
return bin_dir
|
|
|
|
|
|
def test_removes_both_launcher_forms_and_keeps_managed_uv(managed_bin: Path):
|
|
removed = uninstall.remove_windows_bin_launchers(windows=True)
|
|
|
|
assert sorted(p.name for p in removed) == ["hermes-acp.cmd", "hermes.exe"]
|
|
assert not (managed_bin / "hermes.exe").exists()
|
|
assert not (managed_bin / "hermes-acp.cmd").exists()
|
|
# The managed uv stays — keep-data reinstalls still need it.
|
|
assert (managed_bin / "uv.exe").exists()
|
|
assert (managed_bin / "uvx.exe").exists()
|
|
|
|
|
|
def test_anchors_on_default_root_not_profile_home(
|
|
managed_bin: Path, monkeypatch: pytest.MonkeyPatch
|
|
):
|
|
"""The launcher dir is per-machine; a profile HERMES_HOME must not
|
|
redirect the sweep into ``profiles/<name>/bin``."""
|
|
home = managed_bin.parent
|
|
monkeypatch.setenv("HERMES_HOME", str(home / "profiles" / "work"))
|
|
|
|
removed = uninstall.remove_windows_bin_launchers(windows=True)
|
|
|
|
assert sorted(p.name for p in removed) == ["hermes-acp.cmd", "hermes.exe"]
|
|
assert not (managed_bin / "hermes.exe").exists()
|
|
|
|
|
|
def test_noop_on_posix(managed_bin: Path):
|
|
assert uninstall.remove_windows_bin_launchers(windows=False) == []
|
|
assert (managed_bin / "hermes.exe").exists()
|
|
|
|
|
|
def test_noop_when_no_launchers_staged(tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
|
|
home = tmp_path / "hermes"
|
|
home.mkdir()
|
|
monkeypatch.setenv("HERMES_HOME", str(home))
|
|
|
|
assert uninstall.remove_windows_bin_launchers(windows=True) == []
|
|
|
|
|
|
def test_launcher_names_stay_in_lockstep_with_install_ps1():
|
|
"""The sweep must cover exactly the names install.ps1 stages, and no
|
|
generic name it could clobber. Reads the real installer list so the two
|
|
sides cannot drift apart silently."""
|
|
import re
|
|
|
|
install_ps1 = (
|
|
Path(uninstall.__file__).resolve().parents[1] / "scripts" / "install.ps1"
|
|
).read_text(encoding="ascii")
|
|
match = re.search(r"foreach \(\$launcher in @\(([^)]*)\)\)", install_ps1)
|
|
assert match, "launcher staging loop not found in install.ps1"
|
|
staged = set(re.findall(r'"([^"]+)"', match.group(1)))
|
|
|
|
assert staged == set(_WINDOWS_BIN_LAUNCHERS)
|
|
for name in _WINDOWS_BIN_LAUNCHERS:
|
|
assert name.startswith("hermes") # never a generic name it could clobber
|
|
|
|
|
|
class TestManagedBinPathMarker:
|
|
"""The managed ``bin`` PATH entry goes only when the dir itself goes.
|
|
|
|
Markers match against Windows registry PATH entries, so the inputs here
|
|
are Windows-shaped path strings regardless of the host — feeding
|
|
``tmp_path`` would make the test pass only on Windows hosts.
|
|
"""
|
|
|
|
HOME = r"C:\Users\me\AppData\Local\hermes"
|
|
BIN_ENTRY = r"C:\Users\me\AppData\Local\hermes\bin"
|
|
|
|
def test_keep_data_markers_spare_the_managed_bin(self):
|
|
markers = [m.lower() for m in uninstall._hermes_path_markers(Path(self.HOME))]
|
|
|
|
assert not any(self.BIN_ENTRY.lower().startswith(m) for m in markers)
|
|
|
|
def test_full_wipe_markers_take_the_managed_bin(self):
|
|
markers = [
|
|
m.lower()
|
|
for m in uninstall._hermes_path_markers(
|
|
Path(self.HOME), include_managed_bin=True
|
|
)
|
|
]
|
|
|
|
assert any(self.BIN_ENTRY.lower().startswith(m) for m in markers)
|