From bd43a43f0761c78ee16ea92ab45e76d1a5d892cb Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Wed, 25 Mar 2026 18:06:04 -0700 Subject: [PATCH] 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. --- hermes_cli/main.py | 13 ++++-- tests/hermes_cli/test_sessions_delete.py | 53 ++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 4 deletions(-) diff --git a/hermes_cli/main.py b/hermes_cli/main.py index f038cf27..05b817a6 100644 --- a/hermes_cli/main.py +++ b/hermes_cli/main.py @@ -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) diff --git a/tests/hermes_cli/test_sessions_delete.py b/tests/hermes_cli/test_sessions_delete.py index 6f6d359b..e763cacf 100644 --- a/tests/hermes_cli/test_sessions_delete.py +++ b/tests/hermes_cli/test_sessions_delete.py @@ -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