From a479ec01ed73ed43d8649f88781e433aedd980a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=96=87=E6=A3=AE=2EZ?= Date: Mon, 11 May 2026 04:16:09 -0400 Subject: [PATCH] fix: make web UI build output decoding robust on Windows On Windows systems using a Chinese GBK locale, `hermes update` could misreport the Web UI build as failed even when `npm run build` actually succeeded. The failure was caused by Python decoding captured npm output with the process locale inside a background subprocess reader thread. When npm emitted bytes such as `0x85`, decoding under GBK raised `UnicodeDecodeError`, and Hermes then surfaced a misleading "Web UI build failed" warning. This change makes the npm install/npm ci path and the Web UI build step decode captured output explicitly as UTF-8 with `errors="replace"`. That keeps unexpected bytes from crashing output collection, preserves successful builds, and prevents false negatives during update on Windows. The patch also adds regression tests that verify these subprocess calls always use explicit UTF-8 decoding with replacement semantics. --- hermes_cli/main.py | 13 +++++++++++- tests/hermes_cli/test_web_ui_build.py | 30 ++++++++++++++++++++++++++- 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/hermes_cli/main.py b/hermes_cli/main.py index 74b74f2725b..7795aa2dd4e 100644 --- a/hermes_cli/main.py +++ b/hermes_cli/main.py @@ -5549,6 +5549,8 @@ def _run_npm_install_deterministic( cwd=cwd, capture_output=capture_output, text=True, + encoding="utf-8", + errors="replace", check=False, ) if ci_result.returncode == 0: @@ -5561,6 +5563,8 @@ def _run_npm_install_deterministic( cwd=cwd, capture_output=capture_output, text=True, + encoding="utf-8", + errors="replace", check=False, ) @@ -5597,7 +5601,14 @@ def _build_web_ui(web_dir: Path, *, fatal: bool = False) -> bool: if fatal: print(" Run manually: cd web && npm install && npm run build") return False - r2 = subprocess.run([npm, "run", "build"], cwd=web_dir, capture_output=True) + r2 = subprocess.run( + [npm, "run", "build"], + cwd=web_dir, + capture_output=True, + text=True, + encoding="utf-8", + errors="replace", + ) if r2.returncode != 0: print( f" {'✗' if fatal else '⚠'} Web UI build failed" diff --git a/tests/hermes_cli/test_web_ui_build.py b/tests/hermes_cli/test_web_ui_build.py index 47d3bb95a44..688cf7bdc29 100644 --- a/tests/hermes_cli/test_web_ui_build.py +++ b/tests/hermes_cli/test_web_ui_build.py @@ -13,7 +13,7 @@ from unittest.mock import patch import pytest -from hermes_cli.main import _web_ui_build_needed, _build_web_ui +from hermes_cli.main import _web_ui_build_needed, _build_web_ui, _run_npm_install_deterministic def _touch(path: Path, offset: float = 0.0) -> None: @@ -119,3 +119,31 @@ class TestBuildWebUISkipsWhenFresh: assert result is True assert mock_run.call_count == 2 # npm install + npm run build + + def test_npm_install_uses_utf8_replace_output_decoding(self, tmp_path): + web_dir, _ = _make_web_dir(tmp_path) + (web_dir / "package-lock.json").write_text("{}", encoding="utf-8") + + mock_cp = __import__("subprocess").CompletedProcess([], 0, stdout="", stderr="") + with patch("hermes_cli.main.subprocess.run", return_value=mock_cp) as mock_run: + result = _run_npm_install_deterministic("/usr/bin/npm", web_dir) + + assert result.returncode == 0 + _, kwargs = mock_run.call_args + assert kwargs["text"] is True + assert kwargs["encoding"] == "utf-8" + assert kwargs["errors"] == "replace" + + def test_web_build_uses_utf8_replace_output_decoding(self, tmp_path): + web_dir, _ = _make_web_dir(tmp_path) + + mock_cp = __import__("subprocess").CompletedProcess([], 0, stdout="", stderr="") + with patch("hermes_cli.main.shutil.which", return_value="/usr/bin/npm"), \ + patch("hermes_cli.main.subprocess.run", side_effect=[mock_cp, mock_cp]) as mock_run: + result = _build_web_ui(web_dir) + + assert result is True + _, build_kwargs = mock_run.call_args_list[1] + assert build_kwargs["text"] is True + assert build_kwargs["encoding"] == "utf-8" + assert build_kwargs["errors"] == "replace"