Files
hermes-agent/tests/tools/test_code_execution_windows_env.py
ethernet 071ccfbcdf test: restore platforms() host gating that merges reverted to the legacy trio
Four origin/main merges brought back `linux_only` / `macos_only` /
`windows_only` marks in 41 test files, along with the pre-platforms()
versions of scripts/ci/list_os_marked_tests.py and check_os_marker_fakes.py.
Because the legacy names are no longer registered, pytest treated them as
unknown marks — a warning — so every Windows- or macOS-only test RAN on
Linux (test_local_runtime_recovery.py tripped the live-system kill guard).

Rewrite the marks, restore the platforms()-aware CI scripts (keeping main's
os.walk fix for vanishing __pycache__ dirs), drop the stale _BASELINE entries,
and make the conftest reject the retired marks outright so the next merge
cannot resurrect them silently.
2026-09-18 19:06:52 -04:00

225 lines
9.6 KiB
Python

"""Windows essentials and native Winsock/legacy-encoding controls.
Actual production UTF-8 env and Unicode RPC are exercised in
`test_code_execution_modes.py`; never reproduce the production scrubber here.
"""
import os
import subprocess
import sys
import textwrap
import time
import pytest
from tools.code_execution_env import _scrub_child_env
from tools import code_execution_env
def _no_passthrough(_):
return False
@pytest.mark.parametrize("windows", [False, True])
@pytest.mark.parametrize("passthrough", [False, True])
def test_native_essentials_and_passthrough_priority(windows, passthrough):
# Platform is explicit input to this pure policy helper, not a fake host.
essentials = {
"SYSTEMROOT": r"C:\Windows", "SystemRoot": r"C:\Windows",
"SystemDrive": "C:", "WINDIR": r"C:\Windows",
"ComSpec": r"C:\Windows\System32\cmd.exe", "comspec": r"C:\Windows\System32\cmd.exe",
"APPDATA": r"C:\Users\alice\AppData\Roaming",
"LOCALAPPDATA": r"C:\Users\alice\AppData\Local",
}
safe = {"PATH": r"C:\Windows\System32;C:\Python", "HOME": r"C:\Users\alice",
"PATHEXT": ".COM;.EXE;.BAT;.CMD;.PY",
"USERPROFILE": r"C:\Users\alice", "TEMP": r"C:\Users\alice\Temp"}
secret = {"OPENAI_API_KEY": "fake-provider", "GITHUB_TOKEN": "fake-github",
"MY_PASSWORD": "fake-password", "TENOR_API_KEY": "fake-third-party",
"RANDOM_UNKNOWN_VAR": "unknown"}
result = _scrub_child_env({**essentials, **safe, **secret}, is_windows=windows,
is_passthrough=lambda k: passthrough and k == "TENOR_API_KEY")
assert result == {**safe, **(essentials if windows else {}),
**({"TENOR_API_KEY": "fake-third-party"} if passthrough else {})}
# ``platforms("windows")`` rather than ``skipif(sys.platform != "win32")``: the
# dedicated Windows CI job selects its files by grepping for the marker, so a
# bare skipif is invisible to it — the file is never imported there and these
# tests run on no host at all.
@pytest.mark.platforms("windows")
class TestWindowsSocketSmokeTest:
"""Integration-ish smoke test: spawn a child Python with a scrubbed
env and confirm it can create an AF_INET socket. This is the
regression that motivated the fix — without SYSTEMROOT the child
hits WinError 10106 before any RPC is attempted."""
def test_child_can_create_socket_with_scrubbed_env(self):
scrubbed = _scrub_child_env(os.environ, is_passthrough=_no_passthrough)
# Build a tiny child script that simply opens an AF_INET socket.
script = textwrap.dedent("""
import socket, sys
try:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.close()
print("OK")
sys.exit(0)
except OSError as exc:
print(f"FAIL: {exc}")
sys.exit(1)
""").strip()
result = subprocess.run(
[sys.executable, "-c", script],
env=scrubbed,
capture_output=True,
text=True,
timeout=15,
)
assert result.returncode == 0, (
f"Child failed to create socket with scrubbed env:\n"
f" stdout={result.stdout!r}\n"
f" stderr={result.stderr!r}\n"
f" scrubbed keys={sorted(scrubbed.keys())}"
)
assert "OK" in result.stdout
class TestNativeLegacyEncodingControls:
@pytest.mark.platforms("windows")
def test_windows_default_encoding_would_have_failed(self):
"""Negative control: prove that on Windows, writing the stub
*without* ``encoding="utf-8"`` would corrupt the file. If this
test ever starts failing (i.e. default write succeeds), it means
Python's default encoding has changed and the explicit UTF-8
requirement may be obsolete — reconsider the fix."""
from tools.code_execution_tool import generate_hermes_tools_module
import tempfile
stub = generate_hermes_tools_module(["terminal"], transport="uds")
# Find a non-ASCII character we can use to prove the corruption.
non_ascii = [c for c in stub if ord(c) > 127]
if not non_ascii:
pytest.skip("stub has no non-ASCII chars — nothing to corrupt")
# Write with default encoding (simulating the old buggy code).
with tempfile.NamedTemporaryFile(
mode="w", suffix=".py", delete=False
) as f:
try:
f.write(stub)
tmp_path = f.name
wrote_successfully = True
except UnicodeEncodeError:
# Default encoding can't even encode it — that's the bug
# in a different form. Still proves the point.
tmp_path = f.name
wrote_successfully = False
try:
if not wrote_successfully:
# Default-encoding write raised outright. The bug is real.
return
# Read back as UTF-8 (what Python does on import).
with open(tmp_path, encoding="utf-8") as fh:
try:
fh.read()
# If this succeeds on Windows, the platform default is
# already UTF-8 (e.g. Python 3.15 with UTF-8 mode on).
# In that case the explicit encoding= is belt-and-
# suspenders but no longer strictly required. Skip.
pytest.skip(
"Default text-file encoding is UTF-8-compatible on "
"this Windows build — explicit encoding= is no "
"longer load-bearing, but keep it for belt-and-"
"suspenders."
)
except UnicodeDecodeError:
# Exactly the failure mode that motivated the fix.
pass
finally:
os.unlink(tmp_path)
@pytest.mark.platforms("windows")
def test_windows_child_without_utf8_env_would_fail(self):
"""Negative control: spawn a Python child *without* our env
overrides and prove that on Windows, printing non-ASCII fails.
If this ever starts passing, Python has changed its default
stdio encoding on Windows and the fix may be obsolete — but
keep the env vars anyway for belt-and-suspenders."""
script = textwrap.dedent("""
import sys
print("em-dash \\u2014 arrow \\u2192")
sys.exit(0)
""").strip()
# Scrubbed env WITHOUT the PYTHONIOENCODING / PYTHONUTF8 overrides.
# Also scrub PYTHONUTF8 and PYTHONIOENCODING from the inherited
# env so we reproduce the buggy state even if the parent test
# runner has them set.
scrubbed = _scrub_child_env(os.environ, is_passthrough=_no_passthrough)
for k in ("PYTHONIOENCODING", "PYTHONUTF8", "PYTHONLEGACYWINDOWSSTDIO"):
scrubbed.pop(k, None)
result = subprocess.run(
[sys.executable, "-c", script],
env=scrubbed,
capture_output=True,
text=False,
timeout=15,
)
# Either the child crashed (expected), or modern Python handled
# it anyway — in which case the fix is still defensive but no
# longer strictly required. Skip with a note if so.
if result.returncode == 0 and b"\xe2\x80\x94" in result.stdout:
pytest.skip(
"This Python/Windows build handles non-ASCII stdout even "
"without PYTHONIOENCODING/PYTHONUTF8 — fix is defensive "
"but no longer strictly load-bearing. Keep the env vars "
"for older Python builds and C.ASCII-locale containers."
)
# Otherwise: crash OR garbled output — both count as proving the
# bug is real on this system.
def _configured_timezone_child_env():
return code_execution_env._build_child_env(
rpc_endpoint="socket",
rpc_token="token",
tmpdir="/tmp/hermes-code-execution-test",
child_python=sys.executable,
)
def test_windows_child_keeps_os_local_timezone_when_timezone_is_configured(monkeypatch):
"""Windows CPython cannot interpret an IANA zone name in ``TZ``."""
monkeypatch.setattr(code_execution_env, "_IS_WINDOWS", True)
monkeypatch.setattr("hermes_time.get_timezone_name", lambda: "America/Los_Angeles")
assert "TZ" not in _configured_timezone_child_env()
@pytest.mark.platforms("windows")
def test_windows_live_child_offset_matches_os_zone_when_timezone_is_configured(monkeypatch):
"""The user-visible contract of #112233: with ``timezone:`` configured, a real Windows child
must report the OS zone's UTC offset — an IANA name in ``TZ`` made the MSVC runtime derive
``time.timezone == 0`` (+01:00 instead of -07:00) while ``time.tzname`` still read correctly."""
import datetime
import json
monkeypatch.setattr("hermes_time.get_timezone_name", lambda: "America/Los_Angeles")
child_env = _configured_timezone_child_env()
result = subprocess.run(
[sys.executable, "-c",
"import json, time, datetime; print(json.dumps([time.timezone, "
"datetime.datetime.now().astimezone().utcoffset().total_seconds()]))"],
env=child_env, capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=30,
)
assert result.returncode == 0, result.stderr
child_timezone, child_offset = json.loads(result.stdout.strip())
# The test process itself has no TZ override, so its view IS the OS zone.
assert child_offset == datetime.datetime.now().astimezone().utcoffset().total_seconds()
assert child_timezone == time.timezone