diff --git a/hermes_cli/main.py b/hermes_cli/main.py index f038cf276..05b817a60 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 6f6d359b4..e763cacf8 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