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
84 lines
3.4 KiB
Python
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)
|