Files
hermes-agent/tests/hermes_cli/test_subprocess_text_encoding.py
Austin Pickett b00b4bb7f2 fix(cli): pin utf-8 decoding on all text-mode subprocess readers
On Windows, subprocess text=True without an explicit encoding decodes
child output with the ANSI code page (e.g. 'gbk'); non-ASCII bytes then
raise UnicodeDecodeError inside subprocess._readerthread, killing the
Hermes backend before it becomes ready and surfacing as the desktop boot
timeout.

Sweep every hermes_cli text=True subprocess call to encoding='utf-8',
errors='replace', and add an AST-based regression test that fails when a
future text-mode call omits the encoding.

Fixes #55658
2026-09-25 14:22:12 -04:00

42 lines
1.5 KiB
Python

"""Guard: every text-mode subprocess call in hermes_cli pins utf-8 decoding (#55658).
On Windows, ``text=True`` without ``encoding`` decodes child output with the ANSI
code page (e.g. 'gbk'), and non-ASCII bytes raise UnicodeDecodeError inside
``subprocess._readerthread``, killing the backend before it becomes ready.
"""
from __future__ import annotations
import ast
from pathlib import Path
import pytest
HERMES_CLI = Path(__file__).resolve().parents[2] / "hermes_cli"
def _iter_call_text_nodes(tree: ast.AST) -> list[ast.Call]:
return [n for n in ast.walk(tree) if isinstance(n, ast.Call)]
@pytest.mark.parametrize(
"path",
sorted(p for p in HERMES_CLI.rglob("*.py") if "__pycache__" not in p.parts and "test" not in p.name),
ids=lambda p: str(p.relative_to(HERMES_CLI)),
)
def test_text_mode_subprocess_calls_pin_utf8(path: Path) -> None:
tree = ast.parse(path.read_text(encoding="utf-8"))
for call in _iter_call_text_nodes(tree):
names = {kw.arg for kw in call.keywords}
for kw in call.keywords:
if (
kw.arg == "text"
and isinstance(kw.value, ast.Constant)
and kw.value.value is True
):
assert "encoding" in names, (
f"{path.relative_to(HERMES_CLI)}:{kw.lineno}: subprocess text=True "
"without encoding= — on Windows this decodes with the ANSI code page "
"and can raise UnicodeDecodeError on non-ASCII child output (#55658)"
)