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"