fix(cli): let uninstall dry-runs run without a TTY and list the install record
A dry run never prompts, but plain `hermes uninstall --dry-run` still went through the interactive-terminal gate and was refused when stdin was not a TTY. Exempt --dry-run from that gate, matching --gui and --data. The GUI removal plan also omitted desktop-installed-apps.json, which uninstall_gui() deletes. List it under "Will remove" so the preview matches what a real run removes. Adds a CLI-level regression test covering `--gui --dry-run` with and without --yes, and plain `uninstall --dry-run`, asserting nothing is prompted or removed. Refs: #128974 Salvaged-from: #131147
This commit is contained in:
committed by
unsupportedpastels
parent
049028e0f1
commit
2eadea653a
@@ -2369,7 +2369,7 @@ def cmd_uninstall(args):
|
||||
run_gui_uninstall(args)
|
||||
return
|
||||
|
||||
if not getattr(args, "yes", False):
|
||||
if not getattr(args, "yes", False) and not getattr(args, "dry_run", False):
|
||||
_require_tty("uninstall")
|
||||
from hermes_cli.uninstall import run_uninstall
|
||||
|
||||
|
||||
@@ -695,6 +695,7 @@ def run_gui_uninstall(args):
|
||||
"""
|
||||
from hermes_cli.gui_uninstall import (
|
||||
agent_is_installed,
|
||||
desktop_install_record,
|
||||
gui_install_summary,
|
||||
uninstall_gui,
|
||||
)
|
||||
@@ -719,6 +720,8 @@ def run_gui_uninstall(args):
|
||||
print(color("Will remove:", Colors.YELLOW, Colors.BOLD))
|
||||
for p in (*summary["source_built_artifacts"], *summary["packaged_app_paths"]):
|
||||
print(f" • {p}")
|
||||
if (install_record := desktop_install_record()).exists():
|
||||
print(f" • {install_record} (desktop install record)")
|
||||
if summary["userdata_exists"]:
|
||||
print(f" • {summary['userdata_dir']} (desktop app data)")
|
||||
print()
|
||||
|
||||
72
tests/hermes_cli/test_gui_uninstall_retention.py
Normal file
72
tests/hermes_cli/test_gui_uninstall_retention.py
Normal file
@@ -0,0 +1,72 @@
|
||||
"""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
|
||||
def installation(tmp_path, monkeypatch):
|
||||
monkeypatch.setattr(Path, "home", lambda: tmp_path)
|
||||
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}',
|
||||
}
|
||||
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
|
||||
@@ -388,6 +388,8 @@ These controls are hidden for Nix, bundled/Light packages, and other externally
|
||||
|
||||
For self-managed installations, you can do the same from the terminal — `hermes uninstall --gui` for the GUI alone, or `hermes uninstall` / `hermes uninstall --full` for the agent too.
|
||||
|
||||
Preview GUI cleanup with `hermes uninstall --gui --dry-run`. It lists the removal targets and returns without prompting or removing anything, including when `--yes` is also supplied.
|
||||
|
||||
:::note
|
||||
Running `hermes uninstall --gui` from a **source checkout** (a `hermes desktop` dev build) also removes the workspace `node_modules` and `apps/desktop/{dist,release}` build output, since those are GUI build artifacts. They're recoverable with `hermes desktop` (or `npm install` + a rebuild) — but if you're actively hacking on the desktop app, expect to reinstall dependencies afterward.
|
||||
:::
|
||||
|
||||
Reference in New Issue
Block a user