diff --git a/hermes_cli/config.py b/hermes_cli/config.py index 6d5356890a..49ee1e4730 100644 --- a/hermes_cli/config.py +++ b/hermes_cli/config.py @@ -1055,10 +1055,11 @@ DEFAULT_CONFIG = { "updates": { # Run a full ``hermes backup``-style zip of HERMES_HOME before every # ``hermes update``. Backups land in ``/backups/`` and - # can be restored with ``hermes import ``. Set to false to - # skip the backup entirely; use the ``--no-backup`` flag on a single - # update invocation to override just that run. - "pre_update_backup": True, + # can be restored with ``hermes import ``. Off by default — + # on large HERMES_HOME directories the zip can add minutes to every + # update. Set to true to re-enable, or pass ``--backup`` to opt in + # for a single update run. + "pre_update_backup": False, # How many pre-update backup zips to retain. Older ones are pruned # automatically after each successful backup. "backup_keep": 5, diff --git a/hermes_cli/main.py b/hermes_cli/main.py index b838fb9e0e..7bc77bb320 100644 --- a/hermes_cli/main.py +++ b/hermes_cli/main.py @@ -6145,16 +6145,21 @@ def _ensure_fhs_path_guard() -> None: def _run_pre_update_backup(args) -> None: """Create a full zip backup of HERMES_HOME before running the update. - Gated on ``updates.pre_update_backup`` in config (default true). The - ``--no-backup`` flag on ``hermes update`` overrides it for one run. - Never raises — a backup failure should not block the update itself. + Gated on ``updates.pre_update_backup`` in config (default false). Off + by default because the zip can add minutes to every update on large + HERMES_HOME directories. The ``--backup`` flag on ``hermes update`` + opts in for a single run; ``--no-backup`` forces it off when config + has it enabled. Never raises — a backup failure should not block the + update itself. """ - # CLI flag wins over config + # CLI flags win over config. --no-backup beats --backup if both are set. if getattr(args, "no_backup", False): print("◆ Pre-update backup: skipped (--no-backup)") print() return + force_backup = bool(getattr(args, "backup", False)) + try: from hermes_cli.config import load_config cfg = load_config() @@ -6163,12 +6168,13 @@ def _run_pre_update_backup(args) -> None: cfg = {} updates_cfg = cfg.get("updates", {}) if isinstance(cfg, dict) else {} - enabled = updates_cfg.get("pre_update_backup", True) + enabled = updates_cfg.get("pre_update_backup", False) keep = updates_cfg.get("backup_keep", 5) - if not enabled: - print("◆ Pre-update backup: disabled (updates.pre_update_backup=false in config.yaml)") - print() + if not enabled and not force_backup: + # Silent by default — the backup is off, most users don't need to + # hear about it on every update. They can opt in via --backup + # or by flipping the config knob. return try: @@ -6221,8 +6227,8 @@ def _run_pre_update_backup(args) -> None: print(f" Saved: {display_path} ({size_str}, {elapsed:.1f}s)") print(f" Restore: hermes import {out_path}") - print(f" Disable: set updates.pre_update_backup: false in config.yaml") - print(f" (or pass --no-backup on a single update)") + print(f" Disable: omit --backup (backups are off by default)") + print(f" set updates.pre_update_backup: false in config.yaml") print() @@ -9671,6 +9677,12 @@ Examples: default=False, help="Skip the pre-update backup for this run (overrides updates.pre_update_backup)", ) + update_parser.add_argument( + "--backup", + action="store_true", + default=False, + help="Force a pre-update backup for this run (off by default; overrides updates.pre_update_backup)", + ) update_parser.set_defaults(func=cmd_update) # ========================================================================= diff --git a/tests/hermes_cli/test_backup.py b/tests/hermes_cli/test_backup.py index f12e4757b2..fe57f3c6b8 100644 --- a/tests/hermes_cli/test_backup.py +++ b/tests/hermes_cli/test_backup.py @@ -1344,9 +1344,10 @@ class TestRunPreUpdateBackup: del __import__("sys").modules[mod] return root - def test_default_enabled_creates_backup(self, hermes_home, capsys): + def test_backup_flag_creates_backup(self, hermes_home, capsys): + """--backup forces the pre-update backup for one run even when config is off.""" from hermes_cli.main import _run_pre_update_backup - _run_pre_update_backup(Namespace(no_backup=False)) + _run_pre_update_backup(Namespace(no_backup=False, backup=True)) out = capsys.readouterr().out assert "Creating pre-update backup" in out assert "Saved:" in out @@ -1357,9 +1358,20 @@ class TestRunPreUpdateBackup: backups = list((hermes_home / "backups").glob("pre-update-*.zip")) assert len(backups) == 1 + def test_default_disabled_is_silent(self, hermes_home, capsys): + """With the default-off config and no --backup flag, the hook is silent + and creates no backup. This is the common case for every update.""" + from hermes_cli.main import _run_pre_update_backup + _run_pre_update_backup(Namespace(no_backup=False, backup=False)) + out = capsys.readouterr().out + assert out == "" + assert not (hermes_home / "backups").exists() or not list( + (hermes_home / "backups").glob("pre-update-*.zip") + ) + def test_no_backup_flag_skips(self, hermes_home, capsys): from hermes_cli.main import _run_pre_update_backup - _run_pre_update_backup(Namespace(no_backup=True)) + _run_pre_update_backup(Namespace(no_backup=True, backup=False)) out = capsys.readouterr().out assert "skipped (--no-backup)" in out assert "Creating pre-update backup" not in out @@ -1368,7 +1380,30 @@ class TestRunPreUpdateBackup: (hermes_home / "backups").glob("pre-update-*.zip") ) - def test_config_disabled_skips(self, hermes_home, capsys): + def test_config_enabled_creates_backup(self, hermes_home, capsys): + """Users who explicitly set updates.pre_update_backup: true still get + a backup on every update — this is the opt-in legacy behavior.""" + import yaml + (hermes_home / "config.yaml").write_text(yaml.safe_dump({ + "_config_version": 22, + "updates": {"pre_update_backup": True}, + })) + import sys as _sys + for mod in list(_sys.modules.keys()): + if mod.startswith("hermes_cli.config"): + del _sys.modules[mod] + + from hermes_cli.main import _run_pre_update_backup + _run_pre_update_backup(Namespace(no_backup=False, backup=False)) + out = capsys.readouterr().out + assert "Creating pre-update backup" in out + assert "Saved:" in out + backups = list((hermes_home / "backups").glob("pre-update-*.zip")) + assert len(backups) == 1 + + def test_config_disabled_is_silent(self, hermes_home, capsys): + """Explicit pre_update_backup: false behaves the same as the default — + silent no-op, no message spam.""" import yaml (hermes_home / "config.yaml").write_text(yaml.safe_dump({ "_config_version": 22, @@ -1381,10 +1416,9 @@ class TestRunPreUpdateBackup: del _sys.modules[mod] from hermes_cli.main import _run_pre_update_backup - _run_pre_update_backup(Namespace(no_backup=False)) + _run_pre_update_backup(Namespace(no_backup=False, backup=False)) out = capsys.readouterr().out - assert "disabled" in out - assert "updates.pre_update_backup=false" in out + assert out == "" assert not list((hermes_home / "backups").glob("pre-update-*.zip")) \ if (hermes_home / "backups").exists() else True @@ -1401,6 +1435,6 @@ class TestRunPreUpdateBackup: del _sys.modules[mod] from hermes_cli.main import _run_pre_update_backup - _run_pre_update_backup(Namespace(no_backup=True)) + _run_pre_update_backup(Namespace(no_backup=True, backup=False)) out = capsys.readouterr().out assert "skipped (--no-backup)" in out