Files
aiturk-hermes-ide/tests/hermes_cli/test_uninstall_windows_bin_launchers.py
T

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)