From 7797409355cbf257923a81ab9781b706c38f9c3c Mon Sep 17 00:00:00 2001 From: haochen <3033940900@qq.com> Date: Thu, 8 Oct 2026 17:44:10 +0800 Subject: [PATCH] fix(check): preserve UTF-8 dependency diagnostics (#6485) * fix(check): preserve UTF-8 dependency diagnostics * chore: preserve upstream LF line endings --------- Co-authored-by: Zhangchenhao-111 <285911435+Zhangchenhao-111@users.noreply.github.com> --- README.md | 4 ++++ backend/tests/test_check_script.py | 30 ++++++++++++++++++++++++++++++ scripts/AGENTS.md | 8 ++++++++ scripts/check.py | 12 +++++++++++- 4 files changed, 53 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 40c777e4f9..1a75b292c3 100644 --- a/README.md +++ b/README.md @@ -543,6 +543,10 @@ such a checkout, use `bash ./scripts/.sh ...`. The local `make check`, `make install`, `make dev`, and `make start` entry points use a direct `pnpm` executable when available and otherwise fall back to `corepack pnpm`. With native Windows Python, the shared runner checks `pnpm.cmd` before the generic `pnpm` lookup, which follows `PATH`/`PATHEXT` and may select an `.exe` or `.bat` in the same or an earlier PATH directory. The Corepack fallback likewise checks `corepack.cmd` before `corepack`. POSIX Python keeps the generic names first, including when running under MSYS/Cygwin. The runner and diagnostics resolve repository paths absolutely, so these checks work regardless of the caller's current directory. Corepack runs from `frontend/`, so it honors the `packageManager` version pinned in `frontend/package.json`; enabling a global pnpm shim is not required. + `make check` decodes tool diagnostics as UTF-8 and preserves Unicode pnpm + errors on non-UTF-8 hosts. Invalid output bytes are replaced so the checker + can report dependency failures instead of aborting while decoding output. + 2. **Install dependencies**: ```bash make install # Install backend + frontend dependencies + pre-commit hooks diff --git a/backend/tests/test_check_script.py b/backend/tests/test_check_script.py index 6c2a848f8a..56088a8275 100644 --- a/backend/tests/test_check_script.py +++ b/backend/tests/test_check_script.py @@ -2,8 +2,11 @@ from __future__ import annotations import importlib.util import subprocess +import sys from pathlib import Path +import pytest + REPO_ROOT = Path(__file__).resolve().parents[2] CHECK_SCRIPT_PATH = REPO_ROOT / "scripts" / "check.py" PNPM_SCRIPT_PATH = REPO_ROOT / "scripts" / "pnpm.py" @@ -162,3 +165,30 @@ def test_old_node_check_suggests_a_version_manager_command(monkeypatch, capsys): assert check_script.main() == 1 assert "nvm install 22 && nvm use 22" in capsys.readouterr().out + + +@pytest.mark.parametrize("host_encoding", ["ascii", "cp936"]) +def test_check_decodes_tool_output_as_utf8(monkeypatch, host_encoding): + script = _load_script(CHECK_SCRIPT_PATH, "check_utf8_tool") + monkeypatch.setattr(subprocess, "_text_encoding", lambda: host_encoding) + text = "✓ 检查完成" + code = f"import sys; sys.stdout.buffer.write({text.encode('utf-8')!r})" + assert script.run_command([sys.executable, "-c", code]) == text + + +@pytest.mark.parametrize("host_encoding", ["ascii", "cp936"]) +def test_check_preserves_unicode_pnpm_error(tmp_path, monkeypatch, host_encoding): + script = _load_script(CHECK_SCRIPT_PATH, "check_utf8_pnpm") + runner = tmp_path / "pnpm.py" + diagnostic = "✗ 无法运行 pnpm" + runner.write_text(f"import sys\nprint({ascii(diagnostic)}, file=sys.stderr)\nsys.exit(7)\n", encoding="utf-8") + monkeypatch.setattr(script, "PNPM_SCRIPT_PATH", runner) + monkeypatch.setattr(script, "FRONTEND_DIR", tmp_path) + monkeypatch.setattr(subprocess, "_text_encoding", lambda: host_encoding) + monkeypatch.setenv("PYTHONIOENCODING", "ascii") + assert script.run_pnpm_version() == (None, False, diagnostic) + + +def test_check_replaces_invalid_tool_output_bytes(): + script = _load_script(CHECK_SCRIPT_PATH, "check_invalid_tool_bytes") + assert script.run_command([sys.executable, "-c", "import sys; sys.stdout.buffer.write(b'v22.0.0\\xff')"]) == "v22.0.0\ufffd" diff --git a/scripts/AGENTS.md b/scripts/AGENTS.md index e3250b3155..49361815c0 100644 --- a/scripts/AGENTS.md +++ b/scripts/AGENTS.md @@ -1,3 +1,11 @@ +## Dependency Check Diagnostics + +`check.py` captures tool output as UTF-8 with replacement for malformed bytes, +independently of the host locale. Its Python pnpm runner inherits the environment +with `PYTHONIOENCODING=utf-8:backslashreplace`, matching the capture encoding. +Keep Unicode failure diagnostics and exit status available to `make check`. +Real subprocess regressions live in `backend/tests/test_check_script.py`. + ## Manual Claude OAuth Export `export_claude_code_oauth.py` validates Keychain JSON as an object containing an diff --git a/scripts/check.py b/scripts/check.py index d82e596316..74b5963f21 100644 --- a/scripts/check.py +++ b/scripts/check.py @@ -3,6 +3,7 @@ from __future__ import annotations +import os import shutil import subprocess import sys @@ -28,7 +29,13 @@ def run_command(command: list[str]) -> str | None: """Run a command and return trimmed stdout, or None on failure.""" try: result = subprocess.run( - command, capture_output=True, text=True, check=True, shell=False + command, + capture_output=True, + text=True, + encoding="utf-8", + errors="replace", + check=True, + shell=False, ) except (OSError, subprocess.CalledProcessError): return None @@ -42,6 +49,9 @@ def run_pnpm_version() -> tuple[str | None, bool, str | None]: [sys.executable, str(PNPM_SCRIPT_PATH), "-v"], capture_output=True, text=True, + encoding="utf-8", + errors="replace", + env={**os.environ, "PYTHONIOENCODING": "utf-8:backslashreplace"}, check=False, shell=False, cwd=FRONTEND_DIR,