Files
hermes-agent/tests/hermes_cli/test_uninstall_gui_userdata.py
kshitijk4poor 34b48add56 test(uninstall): record stray rmtree/subprocess so the guard can fail the test
The homes fixture's confined_rmtree / no_subprocess guards raised
AssertionError, but _rmtree_step, gui_uninstall._remove_path, the try/except
around uninstall_gui in _perform_uninstall and the Linux menu-cache refresh
all catch and log exceptions. An out-of-tmp delete was blocked but the test
stayed green, so the docstring's "fail loudly" was false.

The guards now append to a violations list (and still refuse to act), and
the test asserts violations == [] after _perform_uninstall. Verified by
temporarily adding an out-of-tmp _rmtree_step to _perform_uninstall: both
params go red and the canary dir survives; restored -> green.
2026-09-27 00:57:22 +05:30

73 lines
3.1 KiB
Python

"""Keep-data uninstall must not delete the desktop app's Electron userData dir.
Regression for #122548: ``hermes uninstall`` option 1 (Keep data) wiped
``desktop_userdata_dir()`` (connections.json, OAuth partitions, renderer state)
because ``_perform_uninstall`` never forwarded ``remove_userdata``.
"""
from __future__ import annotations
import shutil
from pathlib import Path
import pytest
import hermes_cli.gui_uninstall as gui_uninstall
import hermes_cli.uninstall as uninstall
@pytest.fixture
def homes(tmp_path, monkeypatch):
"""Temp home/checkout/userData; every step that reaches outside tmp_path is stubbed.
The full wipe otherwise boots out real launchd jobs, rmtrees real
~/Library/Caches dirs and /Applications/Hermes.app, and (on Windows) edits
the registry — so stub those helpers and refuse any stray rmtree or
subprocess instead of silently touching the developer's machine. Refusals
are recorded in ``violations`` because the uninstall steps catch and log
exceptions, so a raised AssertionError alone would leave the test green.
"""
home = tmp_path / "hermes-home"
home.mkdir()
project_root = tmp_path / "checkout"
project_root.mkdir()
userdata = tmp_path / "userdata"
userdata.mkdir()
(userdata / "connections.json").write_text("{}", encoding="utf-8")
monkeypatch.setattr(uninstall, "_is_windows", lambda: False)
monkeypatch.setattr(uninstall, "uninstall_gateway_service", lambda: True)
for name in ("remove_path_from_shell_configs", "remove_wrapper_script",
"remove_node_symlinks", "remove_legacy_runtime_trees",
"remove_dashboard_launchd_jobs", "_macos_cache_leftover_dirs"):
monkeypatch.setattr(uninstall, name, lambda *a, **k: [])
monkeypatch.setattr(gui_uninstall, "packaged_gui_app_paths", lambda: [])
monkeypatch.setattr(gui_uninstall, "desktop_userdata_dir", lambda: userdata)
real_rmtree = shutil.rmtree
violations: list[str] = []
def confined_rmtree(path, *args, **kwargs):
if not Path(path).is_relative_to(tmp_path):
violations.append(f"rmtree outside tmp: {path}")
raise AssertionError(violations[-1])
return real_rmtree(path, *args, **kwargs)
def no_subprocess(*args, **kwargs):
violations.append(f"unexpected subprocess: {args}")
raise AssertionError(violations[-1])
monkeypatch.setattr(shutil, "rmtree", confined_rmtree)
monkeypatch.setattr(uninstall.subprocess, "run", no_subprocess)
return home, project_root, userdata, violations
@pytest.mark.parametrize("full_uninstall", [False, True])
def test_uninstall_desktop_userdata_kept_unless_full(homes, full_uninstall):
"""Keep-data preserves Electron userData; only the full wipe removes it."""
home, project_root, userdata, violations = homes
uninstall._perform_uninstall(
project_root=project_root, hermes_home=home, full_uninstall=full_uninstall,
remove_profiles=False, named_profiles=[])
assert violations == []
assert (userdata / "connections.json").exists() is not full_uninstall