Files
maigret/tests/test_ai.py
T
Nguyen Thanh DatandGitHub da0fe665b8 Keep result lines and the AI spinner from crashing on a non-UTF-8 stdout (#3056)
* fix: keep result lines from crashing on a non-UTF-8 stdout

#3050 stopped the startup banners from raising UnicodeEncodeError on a
Windows console, but the banner was not the only line the ANSI codepage can
break. Two ordinary things a run prints still went through a bare print():

- the --enrich notifications built in checking.py carry U+2192, which cp1252
  has no slot for (checking.py:1847, 1851, 1859, 1899);
- any profile field maigret extracts carries the script the page was written
  in, so a Cyrillic or CJK fullname breaks the result line that reports it.

Both arrive mid-scan, after the network work is done, so the crash discarded
results the run had already collected -- worse than failing at startup.

Route every write in notify.py through the existing _print_encodable helper.

Verified on a real cp1252 stdout, not a mock: both cases raise before this
change and print with replacement characters after it.

* fix: keep the AI spinner alive on a stderr that cannot encode braille

Second half of #3055. The spinner frames are braille, which cp1252 -- the
ANSI codepage of a stock Windows install -- cannot encode, so the first frame
raised UnicodeEncodeError inside the daemon thread: the thread died with a
traceback printed over the output and the animation stopped for the rest of
the run.

Encoding with replacement would only leave a row of '?' spinning, so pick the
frame set from what the stream can carry instead. A UTF-8 terminal keeps the
braille; anything that cannot hold it gets |/-\ and still animates.

print_streaming() in the same module writes model output the same way and
would raise on any non-Latin reply, but it has no callers anywhere in the
tree, so it is left alone rather than fixed as dead code.
2026-08-31 18:26:21 +02:00

81 lines
3.0 KiB
Python

"""Tests for maigret.ai terminal output."""
import os
import subprocess
import sys
def _run_probe(tmp_path, name, lines, encoding):
"""Run a probe in a child process whose streams really use `encoding`.
Forcing the encoding rather than mocking the failure is deliberate: this then
fails on any machine if the guard goes, the same way the banner test does.
"""
probe = tmp_path / name
probe.write_text("\n".join(lines) + "\n", encoding="utf-8")
env = dict(os.environ)
env["PYTHONIOENCODING"] = encoding
return subprocess.run(
[sys.executable, str(probe)],
capture_output=True,
encoding=encoding,
errors="replace",
env=env,
cwd=str(tmp_path),
)
def test_spinner_renders_on_a_stderr_that_cannot_encode_braille(tmp_path):
"""The spinner frames are braille, which cp1252 -- the ANSI codepage of a
stock Windows install -- cannot encode. Writing one raised UnicodeEncodeError
inside the spinner's daemon thread: the thread died with a traceback printed
over the output, and the animation stopped for the rest of the run.
"""
result = _run_probe(
tmp_path,
"spinner_probe.py",
[
"import sys, time",
"from maigret.ai import _Spinner",
"",
"spinner = _Spinner('probe')",
"spinner.start()",
"time.sleep(0.3)",
"spinner.stop()",
"sys.stdout.write('FRAMES=' + ''.join(spinner._frames) + chr(10))",
"sys.stdout.write('STILL_RUNNING=' + str(spinner._thread.is_alive()) + chr(10))",
"sys.stdout.write('REACHED_END' + chr(10))",
],
"cp1252",
)
assert result.returncode == 0, f"the spinner must not crash the run: stderr={result.stderr!r}"
assert "REACHED_END" in result.stdout, f"stdout={result.stdout!r}"
# The daemon thread swallows nothing: an encode failure surfaces as a
# traceback on stderr and leaves the spinner dead for the rest of the run.
assert "UnicodeEncodeError" not in result.stderr, f"stderr={result.stderr!r}"
assert "Exception in thread" not in result.stderr, f"stderr={result.stderr!r}"
# It still animates, using frames the stream can carry.
assert "FRAMES=|/-\\" in result.stdout, f"stdout={result.stdout!r}"
def test_spinner_keeps_its_braille_when_the_stream_can_encode_it(tmp_path):
"""The frame set is chosen from the stream, not swapped unconditionally: a
UTF-8 terminal must still get the original animation."""
result = _run_probe(
tmp_path,
"spinner_utf8_probe.py",
[
"import sys",
"from maigret.ai import _Spinner",
"",
"sys.stdout.write('FRAMES=' + ''.join(_Spinner('probe')._frames) + chr(10))",
],
"utf-8",
)
assert result.returncode == 0, f"stderr={result.stderr!r}"
braille = "".join(["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"])
assert f"FRAMES={braille}" in result.stdout, f"stdout={result.stdout!r}"