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>
This commit is contained in:
haochen 2026-10-08 17:44:10 +08:00 committed by GitHub
parent 89c9f08714
commit 7797409355
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 53 additions and 1 deletions

View File

@ -543,6 +543,10 @@ such a checkout, use `bash ./scripts/<name>.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

View File

@ -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"

View File

@ -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

View File

@ -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,