mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-26 17:38:36 +00:00
fix(checkpoints): bind an empty orphan preview to an empty deletion allowlist
Follow-up for salvaged PR #69141, addressing the last open review point: cmd_prune() only set orphan_allowlist inside 'if orphans or pre_v2_orphans', so a zero-orphan preview passed the unrestricted None sentinel down to prune_checkpoints(), authorizing deletion of any project that became orphaned between the preview and the rescan — with zero confirmation calls. The allowlist is now bound unconditionally for every non-force run (empty preview => empty allowlist); --force keeps None. Adds the zero-orphan-preview timing regression plus allowlist-identity tests.
This commit is contained in:
parent
c004c74df6
commit
6c14b12d1f
2 changed files with 63 additions and 6 deletions
|
|
@ -148,12 +148,15 @@ def cmd_prune(args: argparse.Namespace) -> int:
|
|||
if not _confirm("Delete these orphan projects?"):
|
||||
print("Aborted.")
|
||||
return 1
|
||||
# Bind the deletion to exactly what was just displayed and
|
||||
# confirmed — a project that becomes orphaned only *after* this
|
||||
# preview (e.g. its workdir disappears while waiting on input())
|
||||
# must not be swept up under this same confirmation.
|
||||
orphan_allowlist = {p["hash"] for p in orphans}
|
||||
orphan_allowlist.update(p["path"] for p in pre_v2_orphans)
|
||||
# Bind the deletion to exactly what was just displayed (and, when
|
||||
# non-empty, confirmed) — a project that becomes orphaned only
|
||||
# *after* this preview (e.g. its workdir disappears while waiting on
|
||||
# input()) must not be swept up under this same run. This is set
|
||||
# unconditionally for every non-force run: an EMPTY preview binds to
|
||||
# an EMPTY allowlist, so a zero-orphan preview can never authorize
|
||||
# deletion of orphans discovered by the later rescan.
|
||||
orphan_allowlist = {p["hash"] for p in orphans}
|
||||
orphan_allowlist.update(p["path"] for p in pre_v2_orphans)
|
||||
|
||||
print("Pruning checkpoint store…")
|
||||
print(f" retention_days: {retention_days}")
|
||||
|
|
|
|||
|
|
@ -206,3 +206,57 @@ def test_no_orphans_skips_prompt(monkeypatch, capsys):
|
|||
|
||||
assert rc == 0
|
||||
assert len(prune_calls) == 1
|
||||
|
||||
|
||||
# ─── allowlist binding: preview set == deletion set, even when empty ───────
|
||||
|
||||
|
||||
def test_empty_preview_binds_empty_allowlist(monkeypatch, capsys):
|
||||
"""Zero-orphan-preview timing regression (PR #69141 review).
|
||||
|
||||
When the non-force preview shows zero orphans, no prompt runs — but the
|
||||
later rescan inside prune_checkpoints() may discover a project that
|
||||
became orphaned *after* the preview. That undisplayed, unconfirmed orphan
|
||||
must not be deletable: the allowlist passed down must be the exact
|
||||
(empty) displayed set, never the unrestricted None sentinel.
|
||||
"""
|
||||
import hermes_cli.checkpoints as checkpoints_cli
|
||||
|
||||
prune_calls: list = []
|
||||
_patch_checkpoint_manager(monkeypatch, _V2_ORPHAN_ONLY_STATUS, prune_calls)
|
||||
|
||||
rc = checkpoints_cli.cmd_prune(_ns())
|
||||
|
||||
assert rc == 0
|
||||
assert len(prune_calls) == 1
|
||||
assert prune_calls[0]["orphan_allowlist"] == set()
|
||||
|
||||
|
||||
def test_nonempty_preview_allowlist_matches_displayed_set(monkeypatch, capsys):
|
||||
import hermes_cli.checkpoints as checkpoints_cli
|
||||
|
||||
prune_calls: list = []
|
||||
_patch_checkpoint_manager(monkeypatch, _MIXED_STATUS, prune_calls)
|
||||
monkeypatch.setattr("builtins.input", lambda _prompt: "y")
|
||||
|
||||
rc = checkpoints_cli.cmd_prune(_ns())
|
||||
|
||||
assert rc == 0
|
||||
assert len(prune_calls) == 1
|
||||
assert prune_calls[0]["orphan_allowlist"] == {
|
||||
"abc123",
|
||||
"/home/user/.hermes/checkpoints/deadbeefcafebabe",
|
||||
}
|
||||
|
||||
|
||||
def test_force_leaves_allowlist_unrestricted(monkeypatch, capsys):
|
||||
import hermes_cli.checkpoints as checkpoints_cli
|
||||
|
||||
prune_calls: list = []
|
||||
_patch_checkpoint_manager(monkeypatch, _MIXED_STATUS, prune_calls)
|
||||
|
||||
rc = checkpoints_cli.cmd_prune(_ns(force=True))
|
||||
|
||||
assert rc == 0
|
||||
assert len(prune_calls) == 1
|
||||
assert prune_calls[0]["orphan_allowlist"] is None
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue