Files
hermes-agent/tests/tui_gateway/test_subprocess_encoding.py
teknium1 48340244dc test: restore TUI gateway regression guards dropped by #120071
- test_cold_start_gil_stall::test_handle_ws_resolves_skin_off_the_loop_thread:
  real handle_ws resolves the skin off the event-loop thread and the result
  reaches gateway.ready (#60800, #72720).
- test_protocol::test_slow_handlers_run_off_the_reader_thread[7 methods]:
  completion/voice/wake RPCs are dispatched to the pool and never block the
  reader (#21123).
- test_tui_gateway_server::test_prompt_submit_releases_old_history_before_heap_trim:
  the pre-turn history snapshot is gone before trim_memory, trim before the
  HERMES_HOME reset (weakref, no frame-local inspection).
- test_entry_sys_path: importing tui_gateway.entry from a CWD with colliding
  utils/ proxy/ ui/ packages succeeds (#15989, #51286), real subprocess.
- test_subprocess_encoding: shell.exec and quick-command exec survive a real
  child emitting invalid UTF-8 (#53137, #52700); rewritten from Popen-kwarg pins.
2026-09-23 10:34:54 -07:00

65 lines
2.5 KiB
Python

"""Regression tests for lossy UTF-8 decoding of TUI gateway subprocess output (#53137).
On Windows with a non-UTF-8 system locale (e.g. GBK on Chinese Windows), text-mode
subprocess reads defaulted to the locale encoding, and a child emitting bytes invalid in
that encoding raised UnicodeDecodeError inside the RPC handler / gateway thread.
#53137 decodes every captured child stream as UTF-8 with ``errors="replace"``.
These tests drive the real RPC handlers against a real child process that writes
invalid UTF-8, so they fail on any host if the lossy decode is dropped (strict UTF-8
raises on ``b"\\xff\\xfe"`` just like GBK does on the reported bytes).
# Test pattern adapted from @devorun's PR #52700 (salvage convention).
"""
from __future__ import annotations
import sys
import pytest
from tui_gateway import server
@pytest.fixture()
def bad_bytes_cmd(tmp_path):
"""A shell command running a child that writes bytes invalid in UTF-8 (and in most
locale codepages), then text. A script file rather than ``-c`` so the shell.exec
approval gate lets it through; quoted paths work under /bin/sh and cmd.exe."""
script = tmp_path / "emit_bad_bytes.py"
script.write_text(
"import sys\nsys.stdout.buffer.write(bytes([255, 254]) + b' tail-ok')\nsys.stdout.flush()\n",
encoding="utf-8",
)
return f'"{sys.executable}" "{script}"'
def test_shell_exec_survives_non_utf8_child_output(bad_bytes_cmd):
"""``!cmd`` via shell.exec returns the output (with replacement chars) instead of a
decode failure surfaced as a 5003 error."""
resp = server.handle_request(
{"id": "enc", "method": "shell.exec", "params": {"command": bad_bytes_cmd}}
)
assert "error" not in resp, resp
assert resp["result"]["code"] == 0
assert "tail-ok" in resp["result"]["stdout"]
assert "\ufffd" in resp["result"]["stdout"]
def test_quick_command_exec_survives_non_utf8_child_output(monkeypatch, bad_bytes_cmd):
"""A ``type: exec`` quick command runs subprocess.run outside any try block, so a
strict decode would raise straight out of command.dispatch."""
monkeypatch.setattr(
server,
"_load_cfg",
lambda: {"quick_commands": {"badbytes": {"type": "exec", "command": bad_bytes_cmd}}},
)
resp = server.handle_request(
{"id": "enc", "method": "command.dispatch", "params": {"name": "badbytes"}}
)
assert "error" not in resp, resp
assert resp["result"]["type"] == "exec"
assert "tail-ok" in resp["result"]["output"]