Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions tests/tui_gateway/test_subprocess_encoding.py
Original file line number Diff line number Diff line change
@@ -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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This only exercises the git path, which current main already hardens and tests through tui_gateway.git_probe. Please add a focused assertion for _SlashWorker's subprocess.Popen kwargs, because that is the still-unfixed production path this PR changes.


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"
16 changes: 16 additions & 0 deletions tui_gateway/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down
Loading