From 3f6c082695ef90d4b1402fae19b7e138fbb5576a Mon Sep 17 00:00:00 2001 From: Yijia-Xiao Date: Sat, 18 Jul 2026 06:28:38 +0000 Subject: [PATCH] fix(cli): report an unusable terminal instead of a prompt_toolkit traceback - Windows terminals without a console buffer raised NoConsoleScreenBufferError before the first prompt, surfacing a raw traceback with no guidance - gate the Windows-only import on sys.platform so a broken prompt_toolkit still surfaces there, and the handler stays inert on other platforms #1138 --- cli/main.py | 27 ++++++++++++++++- tests/test_cli_no_console.py | 57 ++++++++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+), 1 deletion(-) create mode 100644 tests/test_cli_no_console.py diff --git a/cli/main.py b/cli/main.py index c14f538d1..2e0c41d0d 100644 --- a/cli/main.py +++ b/cli/main.py @@ -1,5 +1,6 @@ import datetime import os +import sys import time from collections import deque from functools import wraps @@ -52,6 +53,18 @@ from tradingagents.reporting import write_report_tree console = Console() +# prompt_toolkit's win32 output module is importable only on Windows (it asserts +# the platform at import time), so gate on the platform rather than catching the +# failure — that way a genuinely broken prompt_toolkit on Windows still surfaces +# instead of silently disabling the handler below. Off Windows this stays an +# empty tuple, which `except` accepts and never matches (#1138). +if sys.platform == "win32": # pragma: no cover - platform dependent + from prompt_toolkit.output.win32 import NoConsoleScreenBufferError + + _NO_CONSOLE_ERRORS: tuple[type[BaseException], ...] = (NoConsoleScreenBufferError,) +else: + _NO_CONSOLE_ERRORS = () + app = typer.Typer( name="TradingAgents", help="TradingAgents CLI: Multi-Agents LLM Financial Trading Framework", @@ -1285,7 +1298,19 @@ def analyze( from tradingagents.graph.checkpointer import clear_all_checkpoints n = clear_all_checkpoints(DEFAULT_CONFIG["data_cache_dir"]) console.print(f"[yellow]Cleared {n} checkpoint(s).[/yellow]") - run_analysis(checkpoint=checkpoint) + try: + run_analysis(checkpoint=checkpoint) + except _NO_CONSOLE_ERRORS: + # A terminal with no console buffer cannot host the interactive prompts. + # Emit one actionable line on stderr instead of a prompt_toolkit + # traceback; plain text, since rich may not render here either (#1138). + typer.echo( + "Error: no Windows console available. The interactive CLI needs a real " + "console buffer — run it from Windows Terminal, PowerShell, or cmd.exe " + "rather than a piped or embedded terminal.", + err=True, + ) + raise typer.Exit(code=1) from None if __name__ == "__main__": diff --git a/tests/test_cli_no_console.py b/tests/test_cli_no_console.py new file mode 100644 index 000000000..68063c9b2 --- /dev/null +++ b/tests/test_cli_no_console.py @@ -0,0 +1,57 @@ +"""A terminal without a console buffer must fail with one actionable line (#1138). + +prompt_toolkit raises NoConsoleScreenBufferError before the first prompt in +non-interactive Windows terminals; the CLI should not surface that traceback. +The Windows-only exception import must also stay inert on other platforms. +""" +from __future__ import annotations + +import sys + +from typer.testing import CliRunner + +import cli.main as m + + +def test_no_console_error_tuple_matches_platform(): + # Off Windows the win32 module is never imported (it asserts the platform), + # so the tuple is empty — which `except` accepts and never matches. On + # Windows it holds the real exception type, and a broken prompt_toolkit + # would raise at import rather than silently disabling the handler. + assert isinstance(m._NO_CONSOLE_ERRORS, tuple) + assert all(issubclass(e, BaseException) for e in m._NO_CONSOLE_ERRORS) + if sys.platform == "win32": + assert m._NO_CONSOLE_ERRORS, "Windows must resolve the console error type" + else: + assert m._NO_CONSOLE_ERRORS == () + + +def test_missing_console_prints_actionable_message(monkeypatch): + class _NoConsole(Exception): + pass + + # Simulate the Windows failure on any platform by registering a stand-in. + monkeypatch.setattr(m, "_NO_CONSOLE_ERRORS", (_NoConsole,)) + + def _boom(*a, **k): + raise _NoConsole("No Windows console found. Are you running cmd.exe?") + + monkeypatch.setattr(m, "run_analysis", _boom) + + result = CliRunner().invoke(m.app, []) + assert result.exit_code == 1 + assert "no Windows console available" in result.output + # The raw prompt_toolkit traceback must not reach the user. + assert "Traceback" not in result.output + + +def test_unrelated_errors_still_propagate(monkeypatch): + # The handler must stay narrow: only the console error is translated. + monkeypatch.setattr(m, "_NO_CONSOLE_ERRORS", (RuntimeError,)) + + def _boom(*a, **k): + raise ValueError("unrelated") + + monkeypatch.setattr(m, "run_analysis", _boom) + result = CliRunner().invoke(m.app, []) + assert isinstance(result.exception, ValueError)