diff --git a/tests/tui_gateway/test_subprocess_encoding.py b/tests/tui_gateway/test_subprocess_encoding.py new file mode 100644 index 000000000000..d2d6f0427e67 --- /dev/null +++ b/tests/tui_gateway/test_subprocess_encoding.py @@ -0,0 +1,28 @@ +"""Regression: tui_gateway subprocess I/O must decode child output as UTF-8, +not the OS locale codepage. On non-UTF-8 Windows consoles (e.g. cp950) the +default text-mode decode raises UnicodeDecodeError in the subprocess reader +threads, killing them and stalling the gateway on a fixed cadence (#52649). +""" + +from unittest.mock import MagicMock, patch + + +def test_git_branch_lookup_decodes_as_utf8_replace(): + import tui_gateway.server as srv + + captured = [] + + def fake_run(cmd, **kwargs): + captured.append(kwargs) + m = MagicMock() + m.returncode = 0 + m.stdout = "main" + return m + + with patch.object(srv.subprocess, "run", side_effect=fake_run): + srv._git_branch_for_cwd("/some/repo") + + assert captured, "expected subprocess.run to be called" + for kwargs in captured: + assert kwargs.get("encoding") == "utf-8" + assert kwargs.get("errors") == "replace" diff --git a/tui_gateway/server.py b/tui_gateway/server.py index 24299a82ceb6..4ee557cd7b67 100644 --- a/tui_gateway/server.py +++ b/tui_gateway/server.py @@ -266,6 +266,14 @@ def __init__(self, session_key: str, model: str): stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, + # Decode child output as UTF-8 (what Hermes emits) instead of the + # OS locale codepage. On non-UTF-8 Windows consoles (e.g. cp950) + # the default text-mode decode raises UnicodeDecodeError inside the + # reader threads on any non-ASCII byte, killing _drain_stdout / + # _drain_stderr and stalling the worker (#52649). errors="replace" + # keeps a stray byte from ever crashing the stream. + encoding="utf-8", + errors="replace", bufsize=1, cwd=os.getcwd(), env=os.environ.copy(), @@ -1369,6 +1377,12 @@ def _git_branch_for_cwd(cwd: str) -> str: ["git", "-C", cwd, "branch", "--show-current"], capture_output=True, text=True, + # Decode as UTF-8, not the OS locale codepage: a non-ASCII branch + # name on a cp950 console otherwise raises UnicodeDecodeError in the + # subprocess reader thread (#52649). This runs on the periodic + # session-status refresh, so the crash repeats on a fixed cadence. + encoding="utf-8", + errors="replace", timeout=1.5, check=False, stdin=subprocess.DEVNULL, @@ -1381,6 +1395,8 @@ def _git_branch_for_cwd(cwd: str) -> str: ["git", "-C", cwd, "rev-parse", "--short", "HEAD"], capture_output=True, text=True, + encoding="utf-8", + errors="replace", timeout=1.5, check=False, stdin=subprocess.DEVNULL,