diff --git a/src/basic_memory/cli/promo.py b/src/basic_memory/cli/promo.py index 7139500d..e8e87e59 100644 --- a/src/basic_memory/cli/promo.py +++ b/src/basic_memory/cli/promo.py @@ -24,7 +24,14 @@ def _promos_disabled_by_env() -> bool: def _is_interactive_session() -> bool: """Return whether stdin/stdout are interactive terminals.""" - return sys.stdin.isatty() and sys.stdout.isatty() + try: + return sys.stdin.isatty() and sys.stdout.isatty() + except ValueError: + # Trigger: stdin/stdout already closed (e.g., MCP stdio transport shutdown). + # Why: isatty() raises ValueError on closed file objects; treat as non-interactive + # so the promo is suppressed rather than crashing the shutdown path. + # Outcome: promo skipped for this invocation. + return False def _build_cloud_promo_message() -> str: diff --git a/tests/cli/test_cloud_promo.py b/tests/cli/test_cloud_promo.py index 5451dc19..776ec5c7 100644 --- a/tests/cli/test_cloud_promo.py +++ b/tests/cli/test_cloud_promo.py @@ -20,6 +20,43 @@ def _capture_console() -> tuple[Console, StringIO]: return Console(file=buf, force_terminal=True), buf +# --- _is_interactive_session tests --- + + +def test_is_interactive_session_returns_false_on_closed_stdin(monkeypatch): + """isatty() raises ValueError when stdio is closed (e.g., MCP shutdown).""" + import sys + + from basic_memory.cli.promo import _is_interactive_session + + class _ClosedStdin: + def isatty(self): + raise ValueError("I/O operation on closed file") + + monkeypatch.setattr(sys, "stdin", _ClosedStdin()) + assert _is_interactive_session() is False + + +def test_is_interactive_session_returns_false_on_closed_stdout(monkeypatch): + """isatty() raises ValueError on closed stdout (e.g., MCP shutdown).""" + import sys + + from basic_memory.cli.promo import _is_interactive_session + + class _ClosedStdout: + def isatty(self): + raise ValueError("I/O operation on closed file") + + # stdin reports interactive, but stdout is closed + class _InteractiveStdin: + def isatty(self): + return True + + monkeypatch.setattr(sys, "stdin", _InteractiveStdin()) + monkeypatch.setattr(sys, "stdout", _ClosedStdout()) + assert _is_interactive_session() is False + + # --- maybe_show_init_line tests ---