fix(console): skip the checkpoints prune confirmation the console already took

Hermes Console registers `checkpoints prune`, `clear` and `clear-legacy` as
mutating, so it takes a console-level confirmation before dispatching any of
them. `_apply_confirmed_defaults` then exists to keep the CLI layer from
asking a second time — its docstring says so — but it only force-defaults
`clear` and `clear-legacy`. `prune` was left out, even though `cmd_prune`
gates its orphan preview on the identical `not args.force` shape.

`_capture_output` redirects stdout and stderr but never stdin, so the
unskipped `_confirm()` call hits `input()` with no terminal behind it:
`EOFError` propagates into `_confirm`, which returns False, and `cmd_prune`
prints "Aborted." and returns 1. The console turns that non-zero exit into a
ConsoleCommandError, so `checkpoints prune` fails outright for any user who
has at least one orphan checkpoint project — after that user already
confirmed. When the server does happen to inherit a foreground terminal, the
same call instead blocks a console worker thread and eats the operator's
keystrokes.

Forcing the flag is the documented behavior here rather than a weakening of
the recent orphan-allowlist hardening. `orphan_allowlist` binds a deletion to
the identities shown in the preview, guarding the window where a workdir
disappears while the command waits on `input()`. Under the console there is
no preview and no wait, which is exactly the `--force` case the comment on
`cmd_prune` describes as "no restriction".
This commit is contained in:
briandevans
2026-08-08 08:29:18 -07:00
committed by Teknium
parent e6708af1f2
commit 4c24629bc9
2 changed files with 86 additions and 1 deletions
+4 -1
View File
@@ -1254,7 +1254,10 @@ def _apply_confirmed_defaults(args: argparse.Namespace) -> None:
setattr(args, attr, True)
if getattr(args, "_console_command", None) == "import":
setattr(args, "force", True)
if getattr(args, "checkpoints_command", None) in {"clear", "clear-legacy"}:
# Every checkpoints subcommand the console registers as mutating gates its
# own confirmation on --force, so all three belong here. `prune` reaches
# _confirm() for its orphan preview, and the console never redirects stdin.
if getattr(args, "checkpoints_command", None) in {"prune", "clear", "clear-legacy"}:
setattr(args, "force", True)
if getattr(args, "plugins_action", None) == "install":
if not getattr(args, "enable", False) and not getattr(args, "no_enable", False):
+82
View File
@@ -504,3 +504,85 @@ def test_execute_handler_string_exit_returns_error_not_crash(_isolate_hermes_hom
assert result.status == "error"
assert result.output
_ORPHAN_STORE_STATUS = {
"projects": [
{"hash": "abc123", "workdir": "/gone/v2-project", "exists": False, "commits": 4},
],
"pre_v2_projects": [],
}
def _patch_checkpoint_manager(monkeypatch, prune_calls: list) -> None:
"""Report one orphan project and record the resulting prune call."""
import tools.checkpoint_manager as ckpt_mgr
monkeypatch.setattr(ckpt_mgr, "store_status", lambda *a, **k: _ORPHAN_STORE_STATUS)
def _fake_prune(**kwargs):
prune_calls.append(kwargs)
return {
"scanned": 1,
"deleted_orphan": 1,
"deleted_stale": 0,
"errors": 0,
"bytes_freed": 0,
}
monkeypatch.setattr(ckpt_mgr, "prune_checkpoints", _fake_prune)
def test_console_checkpoints_prune_does_not_reprompt_for_orphans(
_isolate_hermes_home, monkeypatch
):
"""`checkpoints prune` is console-mutating, so the nested prompt must be skipped.
The console asks for confirmation itself before dispatching any command in the
`checkpoints` mutating set, and `_apply_confirmed_defaults` exists to keep the
CLI layer from asking a second time. `clear` and `clear-legacy` are force
defaulted; `prune` was not, so its orphan confirmation still called `input()`.
"""
prune_calls: list = []
_patch_checkpoint_manager(monkeypatch, prune_calls)
def _unexpected_input(_prompt):
raise AssertionError(
"input() must not be called: the console already confirmed `checkpoints prune`"
)
monkeypatch.setattr("builtins.input", _unexpected_input)
result = HermesConsoleEngine().execute("checkpoints prune", confirmed=True)
assert result.status == "ok"
assert len(prune_calls) == 1
assert prune_calls[0]["delete_orphans"] is True
# No preview was shown, so there is nothing to bind the deletion to — the
# documented `--force` case for `orphan_allowlist`.
assert prune_calls[0]["orphan_allowlist"] is None
def test_console_checkpoints_prune_succeeds_without_a_tty(
_isolate_hermes_home, monkeypatch
):
"""The dashboard console has no stdin, so an unskipped prompt aborts the command.
`_capture_output` redirects stdout/stderr but never stdin, so `input()` raises
`EOFError`, `_confirm` returns False, and `cmd_prune` returns 1 — which the
console surfaces as a failed command for every user with an orphan project.
"""
prune_calls: list = []
_patch_checkpoint_manager(monkeypatch, prune_calls)
def _eof_input(_prompt):
raise EOFError
monkeypatch.setattr("builtins.input", _eof_input)
result = HermesConsoleEngine().execute("checkpoints prune", confirmed=True)
assert result.status == "ok"
assert "Aborted." not in result.output
assert len(prune_calls) == 1
assert prune_calls[0]["orphan_allowlist"] is None