Files
hermes-agent/tests/scripts/desktop_update/test_desktop_update_runtime_unicode.py
Teknium 3bb4e87775 test(update): runtime unicode docstring matches the ProcessStartInfo UTF-8 capture
Review R8 m7: after the merge with main's 732b63d61a the probe decodes launcher
stdout as UTF-8 through ProcessStartInfo.StandardOutputEncoding and never touches
[Console]::OutputEncoding, so the test docstrings no longer claim the call sites
scope and restore it. runtime.ps1 keeps a one-line #124526 pointer at the
encoding line. Behaviour and assertions are unchanged.
2026-10-06 05:44:21 -07:00

141 lines
6.7 KiB
Python

"""runtime.ps1 must decode the launcher's UTF-8 path output on Windows (#124526).
``scripts/desktop-update/runtime.ps1`` captures two machine-readable outputs from
Hermes launchers: ``--print-runtime-command`` (a JSON array whose first element
is the interpreter path) and ``--version`` (whose ``Install directory:`` line
names the source root). Both can sit under a non-ASCII profile
(``C:\\Users\\Balázs``). The launchers print UTF-8 — the real CLI's
``hermes_bootstrap`` reconfigures stdio on import — while Windows PowerShell 5.1
decodes captured native stdout with the console OEM code page, mojibaking the
path (``á`` → ``├í``, ``ł`` → ``┼é``) so the returned command cannot run and the
legacy identity check never matches.
Both captures go through ``Invoke-HermesProbe``, which decodes the child's
stdout as UTF-8 via ``ProcessStartInfo.StandardOutputEncoding`` and never
touches ``[Console]::OutputEncoding``. These tests drive the REAL script against
a REAL published launcher under non-ASCII paths, across three console code
pages, and assert the caller's console encoding is left unchanged. They never
read the script's text.
"""
from __future__ import annotations
import json
import os
from pathlib import Path
import subprocess
import sys
import pytest
from tests.installation_launcher_fixture import publish_fixture_launcher
ROOT = Path(__file__).resolve().parents[3]
HELPER = str(ROOT / 'scripts/desktop-update/runtime.ps1').replace("'", "''")
# Mirrors the production launchers: hermes_bootstrap reconfigures stdio to
# UTF-8 on import before the entry module runs, so both outputs carry raw
# UTF-8 bytes on a pipe. sys.reconfigure replicates that for the fixture's
# plain interpreter (the embedded bootstrap imports the real one).
CLI = """
import io, json, os, sys
for _stream in (sys.stdout, sys.stderr):
if isinstance(_stream, io.TextIOWrapper):
try: _stream.reconfigure(encoding='utf-8', errors='replace')
except (OSError, ValueError): pass
from pathlib import Path
def main():
if '--version' in sys.argv:
print('Install directory: ' + str(Path(__file__).resolve().parents[1])); return 0
print(json.dumps([sys.executable] + sys.argv[2:]))
return 0
if __name__ == '__main__':
sys.exit(main())
"""
# From #124526 / #128326: profile names that die as OEM mojibake.
PROFILE = 'Balázs Paweł Ñuñez'
def _run_helper(script: str, home: Path, env_extra: dict[str, str], out: Path) -> list[str]:
"""Run *script*, which writes its answer lines to ``$out`` as UTF-8.
The answer goes through a file, not stdout: under code page 437 or 936 the console cannot
represent ``ł``, so stdout would lose the very characters under test.
"""
result = subprocess.run(['powershell', '-NoProfile', '-Command', f"$out = '{out}'; " + script],
env={**os.environ, 'HERMES_HOME': str(home), **env_extra},
capture_output=True, timeout=120)
assert result.returncode == 0, (result.stdout + result.stderr).decode('utf-8', 'replace')
return out.read_text(encoding='utf-8-sig').splitlines()
_WRITE = "[IO.File]::WriteAllLines($out, [string[]]@({lines}), (New-Object Text.UTF8Encoding($false)))"
@pytest.mark.platforms('windows')
@pytest.mark.parametrize('code_page', [936, 437, 65001])
def test_runtime_command_survives_a_non_ascii_profile_path(tmp_path: Path, code_page: int) -> None:
"""Get-HermesRuntimeCommand returns the launcher's exact interpreter path.
The launcher JSON is ASCII-escaped today, so this is the round-trip
contract for the boundary every other consumer (hermes_cli.windows_ssh_
runtime, the Electron updater, the Rust bootstrap) already decodes as
UTF-8: the capture must return the path byte-exact and leave the caller's
console encoding unchanged, whatever code page the console carries.
"""
profile = tmp_path / PROFILE
install = profile / 'hermes-agent'
# PM's store under the same non-ASCII profile, with a facts.json naming a
# store interpreter there — the layout a real install resolves and prints.
store = profile / 'tools'
python_dir = store / 'cpython-3.14-windows-x86_64-none'
python_dir.mkdir(parents=True)
python = python_dir / 'python.exe'
python.write_bytes(Path(sys.executable).read_bytes())
(store / 'facts.json').write_text(
json.dumps({'packages': {'python': {'entry': python_dir.name}}}), encoding='utf-8')
publish_fixture_launcher(install, CLI)
home = tmp_path / 'home'
home.mkdir()
target = str(install).replace("'", "''")
prefix = f"[Console]::OutputEncoding = [Text.Encoding]::GetEncoding({code_page}); "
script = (prefix + f". '{HELPER}'; "
f"$c = @(Get-HermesRuntimeCommand -InstallRoot '{target}'); "
+ _WRITE.format(lines="$c[0], [Console]::OutputEncoding.CodePage"))
lines = _run_helper(script, home, {'HERMES_RUNTIME_DIR': str(store)}, tmp_path / 'answer.txt')
assert lines[0] == str(python), (
f"interpreter path was mojibaked under code page {code_page}: {lines[0]!r}")
assert int(lines[1]) == code_page, 'the capture must leave the caller console encoding unchanged'
@pytest.mark.platforms('windows')
@pytest.mark.parametrize('code_page', [936, 437, 65001])
def test_legacy_version_identity_check_matches_a_non_ascii_install(tmp_path: Path, code_page: int) -> None:
"""The user-bin fallback resolves by reported install directory, not PATH order.
Red on base: the fixture CLI prints ``Install directory:`` as UTF-8 (the
production CLI reconfigures stdio the same way), and Windows PowerShell
5.1 decodes that capture with the console OEM code page — so under CP437
a profile like ``Paweł Łącki`` reports mojibaked and the identity check's
GetFullPath comparison never matches (#124526).
"""
install = tmp_path / PROFILE / 'hermes-agent'
launcher = publish_fixture_launcher(install, CLI)
# Earlier PM installers published only to user-bin: move (not copy) the
# launcher out of .hermes\bin so Get-HermesRuntimeCommand takes the
# legacy branch. Its body still reports the checkout it was minted from.
home = tmp_path / 'profile'
userbin = home / 'bin'
userbin.mkdir(parents=True)
external = userbin / launcher.name
launcher.rename(external)
target = str(install).replace("'", "''")
prefix = f"[Console]::OutputEncoding = [Text.Encoding]::GetEncoding({code_page}); "
script = (prefix + f". '{HELPER}'; "
f"try {{ $c = @(Get-HermesRuntimeCommand -InstallRoot '{target}') }} catch {{ exit 1 }}; "
+ _WRITE.format(lines="$c[0]"))
lines = _run_helper(script, home, {}, tmp_path / 'answer.txt')
assert lines[0] == str(external), (
f"legacy identity check lost the non-ASCII path under code page {code_page}")