mirror of
https://github.com/zhayujie/chatgpt-on-wechat.git
synced 2026-07-17 11:07:11 +08:00
fix(bash): write large-output temp file as UTF-8
When a command's output exceeds DEFAULT_MAX_BYTES the Bash tool spills the full output to a temp file. The file was opened in text mode without an explicit encoding, so it used the platform locale encoding (cp936/GBK on Chinese Windows). Output containing emoji or other characters not representable in that codepage raised UnicodeEncodeError, which propagated out and turned a successful command (exit code 0) into a tool error, discarding all output. Open the temp file with encoding='utf-8', matching the sibling temp file written in _rewrite_long_python_c. Adds a regression test. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -202,8 +202,12 @@ SAFETY:
|
|||||||
total_bytes = len(output.encode('utf-8'))
|
total_bytes = len(output.encode('utf-8'))
|
||||||
|
|
||||||
if total_bytes > DEFAULT_MAX_BYTES:
|
if total_bytes > DEFAULT_MAX_BYTES:
|
||||||
# Save full output to temp file
|
# Save full output to temp file. encoding='utf-8' is required:
|
||||||
with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.log', prefix='bash-') as f:
|
# the default text-mode encoding is the platform locale (e.g.
|
||||||
|
# cp936/GBK on Chinese Windows), which raises UnicodeEncodeError
|
||||||
|
# for output containing emoji or other non-locale characters and
|
||||||
|
# would discard an otherwise successful command result.
|
||||||
|
with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.log', prefix='bash-', encoding='utf-8') as f:
|
||||||
f.write(output)
|
f.write(output)
|
||||||
temp_file_path = f.name
|
temp_file_path = f.name
|
||||||
|
|
||||||
|
|||||||
59
tests/test_bash_output_encoding.py
Normal file
59
tests/test_bash_output_encoding.py
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
# encoding:utf-8
|
||||||
|
"""
|
||||||
|
Regression test for the Bash tool spilling large output to a temp file.
|
||||||
|
|
||||||
|
When a command's output exceeds DEFAULT_MAX_BYTES the full output is written to
|
||||||
|
a temp file. That file must be opened with encoding='utf-8'; otherwise it falls
|
||||||
|
back to the platform locale encoding (e.g. cp936/GBK on Chinese Windows), which
|
||||||
|
raises UnicodeEncodeError for output containing emoji or other characters not
|
||||||
|
representable in that codepage. The exception previously propagated out and
|
||||||
|
turned an otherwise-successful command (exit code 0) into a tool error, losing
|
||||||
|
all of its output.
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
||||||
|
|
||||||
|
from agent.tools.bash.bash import Bash
|
||||||
|
|
||||||
|
|
||||||
|
def test_large_non_locale_output_is_saved_as_utf8(tmp_path):
|
||||||
|
tool = Bash({"cwd": str(tmp_path), "safety_mode": False})
|
||||||
|
|
||||||
|
# Emit ~80KB (> 50KB DEFAULT_MAX_BYTES) of an emoji so the tool spills the
|
||||||
|
# full output to a temp file. Raw UTF-8 bytes are written from the child so
|
||||||
|
# the command line stays pure ASCII and the child's own stdout encoding is
|
||||||
|
# irrelevant.
|
||||||
|
code = "import sys; sys.stdout.buffer.write((chr(0x1F389) * 20000).encode('utf-8'))"
|
||||||
|
command = f'"{sys.executable}" -c "{code}"'
|
||||||
|
|
||||||
|
real_named_temp_file = tempfile.NamedTemporaryFile
|
||||||
|
captured = {}
|
||||||
|
|
||||||
|
def spy(*args, **kwargs):
|
||||||
|
captured.update(kwargs)
|
||||||
|
return real_named_temp_file(*args, **kwargs)
|
||||||
|
|
||||||
|
temp_file_path = None
|
||||||
|
try:
|
||||||
|
with patch(
|
||||||
|
"agent.tools.bash.bash.tempfile.NamedTemporaryFile", side_effect=spy
|
||||||
|
):
|
||||||
|
result = tool.execute({"command": command, "timeout": 60})
|
||||||
|
|
||||||
|
# Command succeeded, so the tool must not report an error.
|
||||||
|
assert result.status == "success", result.result
|
||||||
|
|
||||||
|
# The temp file must be opened as UTF-8 (the actual fix).
|
||||||
|
assert captured.get("encoding") == "utf-8"
|
||||||
|
|
||||||
|
# And the emoji must round-trip through the saved file.
|
||||||
|
temp_file_path = result.result["details"]["full_output_path"]
|
||||||
|
with open(temp_file_path, encoding="utf-8") as f:
|
||||||
|
assert "\U0001f389" in f.read()
|
||||||
|
finally:
|
||||||
|
if temp_file_path and os.path.exists(temp_file_path):
|
||||||
|
os.remove(temp_file_path)
|
||||||
Reference in New Issue
Block a user