Import AITURK IDE 1.0.0-beta.1 from Hermes 63279301; preserve MIT license
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
"""Regression tests for UTF-8 encoding hardening in tui_gateway/server.py (#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. When a child
|
||||
process emitted bytes invalid in that locale, an unhandled UnicodeDecodeError
|
||||
crashed the reader thread / gateway thread.
|
||||
|
||||
#53137 added encoding="utf-8", errors="replace" to every text-mode subprocess
|
||||
call in tui_gateway/server.py. These tests assert that the kwargs survive so
|
||||
the crash class cannot silently regress.
|
||||
|
||||
# Test pattern adapted from @devorun's PR #52700 (salvage convention).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import subprocess
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
import tui_gateway.server as server
|
||||
|
||||
|
||||
# ── helpers ──────────────────────────────────────────────────────────────
|
||||
|
||||
def _make_completed_process() -> MagicMock:
|
||||
"""A CompletedProcess-like mock with str stdout/stderr (text=True contract)."""
|
||||
cp = MagicMock()
|
||||
cp.stdout = ""
|
||||
cp.stderr = ""
|
||||
cp.returncode = 0
|
||||
return cp
|
||||
|
||||
|
||||
# ── _SlashWorker.Popen path ──────────────────────────────────────────────
|
||||
|
||||
def test_slash_worker_popen_uses_utf8_replace():
|
||||
"""The slash-worker subprocess.Popen must pass encoding="utf-8" and
|
||||
errors="replace" so invalid bytes in child stdout/stderr don't raise
|
||||
UnicodeDecodeError inside the drain threads (#53137).
|
||||
"""
|
||||
with patch.dict("sys.modules", {
|
||||
"hermes_constants": MagicMock(
|
||||
get_hermes_home=MagicMock(return_value="/tmp/hermes_test")
|
||||
),
|
||||
}):
|
||||
with patch("subprocess.Popen") as mock_popen:
|
||||
mock_popen.return_value.stdout = MagicMock()
|
||||
mock_popen.return_value.stderr = MagicMock()
|
||||
|
||||
from tui_gateway.server import _SlashWorker
|
||||
|
||||
_SlashWorker(
|
||||
session_key="test_key",
|
||||
model="test-model",
|
||||
)
|
||||
|
||||
assert mock_popen.called, "Popen was not invoked"
|
||||
kwargs = mock_popen.call_args[1]
|
||||
assert kwargs.get("encoding") == "utf-8", (
|
||||
f"slash-worker Popen must set encoding='utf-8' (got {kwargs.get('encoding')!r})"
|
||||
)
|
||||
assert kwargs.get("errors") == "replace", (
|
||||
f"slash-worker Popen must set errors='replace' (got {kwargs.get('errors')!r})"
|
||||
)
|
||||
|
||||
|
||||
# ── cli.exec handler ─────────────────────────────────────────────────────
|
||||
|
||||
def test_cli_exec_uses_utf8_replace():
|
||||
"""The cli.exec RPC handler runs `python -m hermes_cli.main` via
|
||||
subprocess.run; it must pass encoding="utf-8" and errors="replace"
|
||||
(#53137)."""
|
||||
handler = server._methods["cli.exec"]
|
||||
with patch("subprocess.run", return_value=_make_completed_process()) as mock_run:
|
||||
# Non-interactive argv that passes _cli_exec_blocked.
|
||||
resp = handler(1, {"argv": ["--version"]})
|
||||
assert mock_run.called, "subprocess.run was not invoked"
|
||||
kwargs = mock_run.call_args[1]
|
||||
assert kwargs.get("encoding") == "utf-8", (
|
||||
f"cli.exec subprocess.run must set encoding='utf-8' (got {kwargs.get('encoding')!r})"
|
||||
)
|
||||
assert kwargs.get("errors") == "replace", (
|
||||
f"cli.exec subprocess.run must set errors='replace' (got {kwargs.get('errors')!r})"
|
||||
)
|
||||
|
||||
|
||||
# ── shell.exec handler ───────────────────────────────────────────────────
|
||||
|
||||
def test_shell_exec_uses_utf8_replace():
|
||||
"""The shell.exec RPC handler runs an arbitrary shell command via
|
||||
subprocess.run; it must pass encoding="utf-8" and errors="replace"
|
||||
(#53137)."""
|
||||
handler = server._methods["shell.exec"]
|
||||
with patch("subprocess.run", return_value=_make_completed_process()) as mock_run:
|
||||
# A harmless, non-dangerous command that passes the approval gate.
|
||||
with patch("tools.approval.detect_hardline_command", return_value=(False, "")), \
|
||||
patch("tools.approval.detect_dangerous_command", return_value=(False, None, "")):
|
||||
resp = handler(1, {"command": "echo hello"})
|
||||
assert mock_run.called, "subprocess.run was not invoked"
|
||||
kwargs = mock_run.call_args[1]
|
||||
assert kwargs.get("encoding") == "utf-8", (
|
||||
f"shell.exec subprocess.run must set encoding='utf-8' (got {kwargs.get('encoding')!r})"
|
||||
)
|
||||
assert kwargs.get("errors") == "replace", (
|
||||
f"shell.exec subprocess.run must set errors='replace' (got {kwargs.get('errors')!r})"
|
||||
)
|
||||
|
||||
|
||||
# ── quick-command exec path (via command.dispatch) ───────────────────────
|
||||
|
||||
def test_quick_command_exec_uses_utf8_replace():
|
||||
"""A quick_command of type 'exec' is dispatched via command.dispatch;
|
||||
the underlying subprocess.run must pass encoding="utf-8" and
|
||||
errors="replace" (#53137)."""
|
||||
handler = server._methods["command.dispatch"]
|
||||
fake_cp = _make_completed_process()
|
||||
with patch("subprocess.run", return_value=fake_cp) as mock_run, \
|
||||
patch("tui_gateway.server._load_cfg", return_value={
|
||||
"quick_commands": {"runcmd": {"type": "exec", "command": "echo hi"}}
|
||||
}), \
|
||||
patch("tools.environments.local._sanitize_subprocess_env", return_value={"PATH": "/usr/bin"}):
|
||||
resp = handler(1, {"name": "runcmd", "arg": "", "session_id": ""})
|
||||
assert mock_run.called, "subprocess.run was not invoked for quick-command exec"
|
||||
kwargs = mock_run.call_args[1]
|
||||
assert kwargs.get("encoding") == "utf-8", (
|
||||
f"quick-command exec subprocess.run must set encoding='utf-8' (got {kwargs.get('encoding')!r})"
|
||||
)
|
||||
assert kwargs.get("errors") == "replace", (
|
||||
f"quick-command exec subprocess.run must set errors='replace' (got {kwargs.get('errors')!r})"
|
||||
)
|
||||
Reference in New Issue
Block a user