Files
hermes-agent/tests/hermes_cli/test_gui_uninstall_retention.py
itsflownium 80efaa56ee fix(gui): detect leftover desktop install records
On macOS, `hermes update` and `hermes desktop` reinstall any Hermes.app
listed in desktop-installed-apps.json that has gone missing, and the GUI
uninstall is what deletes that record. gui_is_installed() ignored the
record, so after the app was removed by hand `hermes uninstall --gui`
reported that no GUI was installed and returned without deleting it, and
the next update put the app back.

Count the install record in gui_is_installed() so the uninstall previews
and removes it. No new removal targets: uninstall_gui() already deletes
the record. The retention tests now also run with the record as the only
remaining GUI artifact.

Refs: #128974
Salvaged-from: #131147
2026-10-02 20:51:34 +00:00

84 lines
3.4 KiB
Python

"""Regression for #128974: GUI dry-run and GUI removal preserve shared state."""
import sys
from pathlib import Path
import pytest
from hermes_cli import gui_uninstall, main
from hermes_constants import get_hermes_home
@pytest.fixture(params=("complete", "record_only"))
def installation(request, tmp_path, monkeypatch):
monkeypatch.setattr(Path, "home", lambda: tmp_path)
monkeypatch.setenv("HERMES_HOME", str(tmp_path / ".hermes"))
home = get_hermes_home()
assert home.resolve().is_relative_to(tmp_path.resolve())
root = home / "hermes-agent"
protected = {
root / "hermes_cli" / "__init__.py": b"# agent source\n",
root / "venv" / "runtime": b"agent runtime",
root / "node_modules" / "shared-package" / "index.js": b"shared workspace dependency",
home / "config.yaml": b"model: fixture-model\n",
home / ".env": b"FIXTURE_TOKEN=fake\n",
home / "sessions" / "saved.json": b'{"saved": true}',
}
record = tmp_path / "desktop-installed-apps.json"
bundle = tmp_path / "apps" / "Hermes.app"
userdata = tmp_path / "desktop-data"
gui_files = {
root / "apps" / "desktop" / "dist" / "index.html": b"renderer",
root / "apps" / "desktop" / "release" / "artifact": b"release",
root / "apps" / "desktop" / "node_modules" / "private-module": b"desktop dependency",
home / "desktop-build-stamp.json": b'{"fixture": true}',
bundle / "app": b"desktop app",
userdata / "connections.json": b'{"fixture": true}',
record: b'{"fixture": true}',
}
if request.param == "record_only":
gui_files = {record: gui_files[record]}
for path, data in {**protected, **gui_files}.items():
path.parent.mkdir(parents=True, exist_ok=True)
path.write_bytes(data)
monkeypatch.setattr(gui_uninstall, "packaged_gui_app_paths", lambda: [bundle])
monkeypatch.setattr(gui_uninstall, "desktop_userdata_dir", lambda: userdata)
monkeypatch.setattr(gui_uninstall, "desktop_install_record", lambda: record)
return protected, gui_files, record
def _run(monkeypatch, *args):
monkeypatch.setattr(sys, "argv", ["hermes", "uninstall", "--gui", *args])
return main.main()
def test_gui_dry_run_never_confirms_or_removes_installed_state(installation, monkeypatch, capsys):
protected, gui_files, record = installation
def unexpected_input(*args):
pytest.fail("A dry-run must not request removal confirmation")
monkeypatch.setattr("builtins.input", unexpected_input)
for flags in (("--yes",), ()):
_run(monkeypatch, "--dry-run", *flags)
for path, data in {**protected, **gui_files}.items():
assert path.read_bytes() == data
output = capsys.readouterr().out
assert "Dry run" in output
assert str(record) in output
assert "Uninstalled!" not in output
# The sibling full-agent preview must also work without an interactive terminal.
monkeypatch.setattr(sys, "argv", ["hermes", "uninstall", "--dry-run"])
main.main()
for path, data in {**protected, **gui_files}.items():
assert path.read_bytes() == data
def test_gui_removal_keeps_agent_data_and_shared_workspace_dependencies(installation, monkeypatch):
protected, gui_files, _ = installation
_run(monkeypatch, "--yes")
for path, data in protected.items():
assert path.read_bytes() == data
assert all(not path.exists() for path in gui_files)