Files
hermes-agent/tests/test_desktop_update_windows_ui_delivery.py
ethernet f59dd0b310 test(windows): bound updater fixture startup and failure logs
Keep cold interpreter startup separate from progress and pipe deadlines. Reap the exact fixture tree on cleanup and keep flood diagnostics on disk. Retain the real pipe-drain self-test instead of source-text assertions.
2026-09-06 14:46:39 -04:00

96 lines
3.8 KiB
Python

"""The real Windows update server retains terminal events until acknowledged."""
from __future__ import annotations
import json
import os
import re
import shutil
import subprocess
import time
from contextlib import contextmanager
from pathlib import Path
from urllib.error import HTTPError
from urllib.request import Request, urlopen
import pytest
pytestmark = pytest.mark.platforms("windows")
SCRIPT = Path(__file__).resolve().parents[1] / "scripts/desktop-update/windows.ps1"
@contextmanager
def _server(tmp_path: Path, *, failed: bool = False):
powershell = shutil.which("powershell.exe")
assert powershell, "Windows updater tests require Windows PowerShell."
env = os.environ.copy()
env.update(TEMP=str(tmp_path), TMP=str(tmp_path), HERMES_SELFTEST_HOLD_SECONDS="0")
env.pop("HERMES_SELFTEST_FAIL", None)
if failed:
env["HERMES_SELFTEST_FAIL"] = "1"
output_path = tmp_path / "ui-delivery.log"
with output_path.open("wb") as output:
process = subprocess.Popen(
[powershell, "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", str(SCRIPT), "-SelfTestUi", "-NoUi"],
env=env,
stdout=output,
stderr=subprocess.STDOUT,
creationflags=subprocess.CREATE_NO_WINDOW,
)
try:
deadline = time.monotonic() + 120
while time.monotonic() < deadline:
text = output_path.read_text(encoding="utf-8", errors="replace")
match = re.search(r"SELF-TEST: shim at (http://127\.0\.0\.1:\d+/)", text)
if match:
yield process, match.group(1)
return
if process.poll() is not None:
break
time.sleep(0.05)
pytest.fail(f"Update server did not publish a serving URL: {text}")
finally:
if process.poll() is None:
subprocess.run(
["taskkill", "/PID", str(process.pid), "/T", "/F"],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=15,
)
process.wait(timeout=15)
def _request(url: str, *, post: bool = False):
request = Request(url, data=b"" if post else None, method="POST" if post else "GET")
return urlopen(request, timeout=3)
@pytest.mark.parametrize("failed", [False, True], ids=["complete", "failed"])
def test_delayed_client_receives_terminal_state_before_acknowledging(tmp_path: Path, failed: bool) -> None:
with _server(tmp_path, failed=failed) as (process, url):
# A background browser can miss the former 900ms terminal-state window.
time.sleep(2)
assert process.poll() is None, "the updater discarded its final state before the delayed client received it"
with _request(url + "progress") as response:
state = json.load(response)
assert state["status"] == ("error" if failed else "done")
receipt = state["receipt"]
assert isinstance(receipt, str) and receipt
with pytest.raises(HTTPError) as wrong:
_request(url + "ack/not-the-published-receipt", post=True)
assert wrong.value.code == 409
assert process.poll() is None, "an unrelated acknowledgement must not dispose the window's state"
for route, post in [("progress-other", False), ("ack/" + receipt, False), ("ack/" + receipt + "/extra", True)]:
with pytest.raises(HTTPError) as invalid:
_request(url + route, post=post)
assert invalid.value.code == 404
with _request(url + "ack/" + receipt, post=True) as response:
assert response.status == 204
assert process.wait(timeout=15) == 0
def test_no_client_does_not_keep_the_update_process_alive_forever(tmp_path: Path) -> None:
with _server(tmp_path) as (process, _url):
assert process.wait(timeout=25) == 0