From a743ad39e9cb344e1fbfdbc72aef52adc76ed882 Mon Sep 17 00:00:00 2001 From: Abhay Singh Date: Sun, 12 Jul 2026 23:02:45 +0530 Subject: [PATCH] fix(cli): make CLI output UTF-8-safe so --help survives Windows cp1252 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A stray -> (U+2192) in the certify command's Typer help string crashed both 'volo certify --help' and (because it renders in the top-level command list) 'volo --help' with UnicodeEncodeError whenever stdout was cp1252 — i.e. any 'volo --help | cat', redirect to a file, subprocess capture, or Windows CI runner. Two fixes: (1) the help string is now ASCII; (2) _force_utf8_io() in main.py reconfigures stdout/stderr to UTF-8 (errors=replace) at import, before Click renders --help, so a future non-ASCII help string can't reintroduce the crash. No-op where the stream can't be reconfigured (e.g. pytest capture). Verified: 'volo --help' and 'volo certify --help' now exit 0 under PYTHONIOENCODING=cp1252 (were exit 1). 3 tests. Closes #11. --- .../volo-cli/src/volo_cli/commands/certify.py | 2 +- packages/volo-cli/src/volo_cli/main.py | 22 +++++++++++++++ packages/volo-cli/tests/test_cli_encoding.py | 28 +++++++++++++++++++ 3 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 packages/volo-cli/tests/test_cli_encoding.py diff --git a/packages/volo-cli/src/volo_cli/commands/certify.py b/packages/volo-cli/src/volo_cli/commands/certify.py index ded187a..15076be 100644 --- a/packages/volo-cli/src/volo_cli/commands/certify.py +++ b/packages/volo-cli/src/volo_cli/commands/certify.py @@ -18,7 +18,7 @@ certify_app = typer.Typer( name="certify", - help="Volo Certified — reliability + safety → a signed agent certificate + badge.", + help="Volo Certified - reliability + safety -> a signed agent certificate + badge.", no_args_is_help=True, ) diff --git a/packages/volo-cli/src/volo_cli/main.py b/packages/volo-cli/src/volo_cli/main.py index 122e92d..60294cf 100644 --- a/packages/volo-cli/src/volo_cli/main.py +++ b/packages/volo-cli/src/volo_cli/main.py @@ -2,6 +2,9 @@ from __future__ import annotations +import contextlib +import sys + import typer from volo_cli.commands.certify import certify_app @@ -28,6 +31,25 @@ __version__ = "0.1.0.dev0" + +def _force_utf8_io() -> None: + """Make stdout/stderr UTF-8 so the CLI never crashes on non-ASCII output under a legacy + codepage (Windows cp1252 when piped/redirected/in CI). Runs at import — before Click renders + ``--help`` — so even help text with a stray non-ASCII char is safe. ``errors='replace'`` keeps + a bad byte from ever raising. No-op where the stream can't be reconfigured (e.g. pytest capture). + """ + for stream in (sys.stdout, sys.stderr): + reconfigure = getattr(stream, "reconfigure", None) + if reconfigure is not None: + # stream already detached/closed -> nothing we can do, and never worth crashing over + with contextlib.suppress(ValueError, OSError): + reconfigure(encoding="utf-8", errors="replace") + + +# Runs at import (the console-script imports this module before invoking `app`), so it takes +# effect before Click renders any output — including `--help`. +_force_utf8_io() + app = typer.Typer( name="volo", help="Volo — a flight simulator for AI agents. See https://github.com/volo.", diff --git a/packages/volo-cli/tests/test_cli_encoding.py b/packages/volo-cli/tests/test_cli_encoding.py new file mode 100644 index 0000000..d27b499 --- /dev/null +++ b/packages/volo-cli/tests/test_cli_encoding.py @@ -0,0 +1,28 @@ +"""The CLI must not crash on non-ASCII output under a legacy codepage (Windows cp1252).""" + +from __future__ import annotations + +from typer.testing import CliRunner + +from volo_cli.main import _force_utf8_io, app + +runner = CliRunner() + + +def test_force_utf8_io_is_safe_and_idempotent() -> None: + _force_utf8_io() + _force_utf8_io() # calling twice must not raise + + +def test_top_level_help_renders() -> None: + res = runner.invoke(app, ["--help"]) + assert res.exit_code == 0 + assert "certify" in res.output + + +def test_command_help_strings_are_cp1252_safe() -> None: + # every registered command's help text must survive a legacy codepage (the --help crash was a + # stray non-ASCII char in a help string) + for group in app.registered_groups: + help_text = getattr(group.typer_instance.info, "help", "") or "" + help_text.encode("cp1252") # raises UnicodeEncodeError if a stray char sneaks back in