fix(cli): handle EOFError in sessions delete/prune confirmation prompts (#3101)

sessions delete and prune call input() for confirmation without
catching EOFError. When stdin isn't a TTY (piped input, CI/CD, cron),
input() throws EOFError and the command crashes.

Extract a _confirm_prompt() helper that handles EOFError and
KeyboardInterrupt, defaulting to cancel. Both call sites now use it.

Salvaged from PR #2622 by dieutx (improved from duplicated try/except
to shared helper). Closes #2565.
This commit is contained in:
Teknium 2026-03-25 18:06:04 -07:00 committed by GitHub
parent 432ba3b709
commit bd43a43f07
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 62 additions and 4 deletions

View file

@ -3844,6 +3844,13 @@ For more help on a command:
sessions_browse.add_argument("--source", help="Filter by source (cli, telegram, discord, etc.)")
sessions_browse.add_argument("--limit", type=int, default=50, help="Max sessions to load (default: 50)")
def _confirm_prompt(prompt: str) -> bool:
"""Prompt for y/N confirmation, safe against non-TTY environments."""
try:
return input(prompt).strip().lower() in ("y", "yes")
except (EOFError, KeyboardInterrupt):
return False
def cmd_sessions(args):
import json as _json
try:
@ -3904,8 +3911,7 @@ For more help on a command:
print(f"Session '{args.session_id}' not found.")
return
if not args.yes:
confirm = input(f"Delete session '{resolved_session_id}' and all its messages? [y/N] ")
if confirm.lower() not in ("y", "yes"):
if not _confirm_prompt(f"Delete session '{resolved_session_id}' and all its messages? [y/N] "):
print("Cancelled.")
return
if db.delete_session(resolved_session_id):
@ -3917,8 +3923,7 @@ For more help on a command:
days = args.older_than
source_msg = f" from '{args.source}'" if args.source else ""
if not args.yes:
confirm = input(f"Delete all ended sessions older than {days} days{source_msg}? [y/N] ")
if confirm.lower() not in ("y", "yes"):
if not _confirm_prompt(f"Delete all ended sessions older than {days} days{source_msg}? [y/N] "):
print("Cancelled.")
return
count = db.prune_sessions(older_than_days=days, source=args.source)

View file

@ -62,3 +62,56 @@ def test_sessions_delete_reports_not_found_when_prefix_is_unknown(monkeypatch, c
output = capsys.readouterr().out
assert "Session 'missing-prefix' not found." in output
def test_sessions_delete_handles_eoferror_on_confirm(monkeypatch, capsys):
"""sessions delete should not crash when stdin is closed (non-TTY)."""
import hermes_cli.main as main_mod
import hermes_state
class FakeDB:
def resolve_session_id(self, session_id):
return "20260315_092437_c9a6ff"
def delete_session(self, session_id):
raise AssertionError("delete_session should not be called when cancelled")
def close(self):
pass
monkeypatch.setattr(hermes_state, "SessionDB", lambda: FakeDB())
monkeypatch.setattr(
sys, "argv",
["hermes", "sessions", "delete", "20260315_092437_c9a6"],
)
monkeypatch.setattr("builtins.input", lambda _prompt="": (_ for _ in ()).throw(EOFError))
main_mod.main()
output = capsys.readouterr().out
assert "Cancelled" in output
def test_sessions_prune_handles_eoferror_on_confirm(monkeypatch, capsys):
"""sessions prune should not crash when stdin is closed (non-TTY)."""
import hermes_cli.main as main_mod
import hermes_state
class FakeDB:
def prune_sessions(self, **kwargs):
raise AssertionError("prune_sessions should not be called when cancelled")
def close(self):
pass
monkeypatch.setattr(hermes_state, "SessionDB", lambda: FakeDB())
monkeypatch.setattr(
sys, "argv",
["hermes", "sessions", "prune"],
)
monkeypatch.setattr("builtins.input", lambda _prompt="": (_ for _ in ()).throw(EOFError))
main_mod.main()
output = capsys.readouterr().out
assert "Cancelled" in output