Files
hermes-agent/tests/desktop/test_appinstaller_check_script.py
ethernet 2bf86dd20c fix(desktop): run the Windows update checkers from bundled core code
A packaged Windows build reported "Microsoft Store returned invalid
JSON" on every update check. The Store checker, the App Installer
checker and the relaunch waiter were resolved under the payload's repo
snapshot (<payload>/hermes-agent/apps/desktop/scripts/), but
INERT_SNAPSHOT_DIRS has excluded apps/ and scripts/ from that snapshot
since f6713f2762. The installed 2026.6523.3313.0 package has no apps/
directory at all. The payload python then failed to open the script,
exited 2 with empty stdout, and parseResult reported invalid JSON.

The two checkers are python that runs under the payload interpreter, so
they belong with the core code, like the other hermes_cli helpers the
desktop runs with -m:

- apps/desktop/scripts/check-store-update.py becomes
  hermes_cli/windows_store_update.py and check-appinstaller-update.py
  becomes hermes_cli/windows_appinstaller_update.py (git mv).
- The runner spawns `python -P -m <module>` with PYTHONPATH set to the
  bundled core code, then the venv dependencies: the order the bin
  launchers give sys.path. -P keeps the working directory off sys.path.
- The relaunch waiter is PowerShell run outside the package, not core
  code. It ships as an app resource (win.extraResources) and resolves
  from process.resourcesPath.

The Python tests now import the modules, and a real `python -P -m`
subprocess test proves a checker failure still reaches the desktop as
JSON. A vitest checks the packaging config ships the waiter.
2026-10-02 10:42:07 -04:00

119 lines
4.8 KiB
Python

"""App Installer checker output contracts and native WinRT dependency coverage.
Stubbed subprocess checks cover OS outcomes without a packaged process.
The native Windows test checks the URI and async projection types.
Root causes pinned here (audit C08):
- ``Package.current`` is a PROPERTY, not a callable.
- ``PackageManager.check_package_update_availability_async`` does not exist;
the method lives on the Package instance.
- A not-packaged process is identifiable by the package-identity HRESULT
(0x80073D54); ANY other failure must surface as unknown, never as
"no update".
"""
from __future__ import annotations
import json
import os
import subprocess
import sys
from pathlib import Path
import pytest
HERMES_PYTHON = sys.executable
REPO = Path(__file__).resolve().parents[2]
MODULE = "hermes_cli.windows_appinstaller_update"
@pytest.mark.parametrize("state,code,available", [
("not-packaged", 0, False), ("AVAILABLE", 2, True), ("REQUIRED", 2, True),
("NO_UPDATES", 0, False), ("UNKNOWN", 1, None), ("ERROR", 1, None),
("no-source", 1, None), ("check-error", 1, None), ("identity-error", 1, None),
("missing-projection", 1, None), ("missing-current", 1, None),
])
def test_projection_outcomes(monkeypatch, capsys, state, code, available):
import enum
import importlib
from types import SimpleNamespace
main = importlib.import_module(MODULE).main
availability = enum.IntEnum("Availability", "UNKNOWN NO_UPDATES AVAILABLE REQUIRED ERROR", start=0)
class PackageInstance:
def get_app_installer_info(self):
return None if state == "no-source" else SimpleNamespace(
uri=SimpleNamespace(absolute_uri="https://registered.example/updates.appinstaller"))
def check_update_availability_async(self):
def get():
if state == "check-error":
raise RuntimeError("rpc boom")
return SimpleNamespace(availability=availability[state])
return SimpleNamespace(get=get)
class PackageMeta(type):
@property
def current(cls):
if state in ("not-packaged", "identity-error"):
error = OSError("identity failure")
error.winerror = -2147009196 if state == "not-packaged" else -2147024891
raise error
return PackageInstance()
class Package(metaclass=PackageMeta):
pass
def load():
if state == "missing-projection":
raise ImportError("missing winrt")
return (object if state == "missing-current" else Package), availability
monkeypatch.setitem(main.__globals__, "_load_projection", load)
assert main() == code
payload = json.loads(capsys.readouterr().out)
assert payload["available"] is available
if state == "not-packaged":
assert payload == {"available": False, "reason": "not-packaged"}
elif state in ("AVAILABLE", "REQUIRED", "NO_UPDATES"):
assert payload["availability"] == state
assert payload["source_uri"] == "https://registered.example/updates.appinstaller"
else:
assert payload["error"]
if state == "no-source":
assert payload["reason"] == "no-app-installer-source"
assert ".appinstaller" in payload["error"]
def test_script_import_failure_json_and_exit(tmp_path):
# A real subprocess runs the entry the desktop spawns (python -P -m) and
# imports the absent projection.
(tmp_path / "winrt.py").write_text("raise ImportError('fixture unavailable')", encoding="utf-8")
child = subprocess.run([HERMES_PYTHON, "-P", "-m", MODULE], capture_output=True,
text=True, timeout=30,
env={**os.environ, "PYTHONPATH": os.pathsep.join([str(REPO), str(tmp_path)])})
assert child.returncode == 1, child.stderr
payload = json.loads(child.stdout)
assert payload["available"] is None
assert "fixture unavailable" in payload["error"]
@pytest.mark.platforms("windows")
def test_installed_winrt_projects_checker_uri_and_async_types(tmp_path):
# A dev process has no package identity, so exercise the types that the
# packaged update call projects only after Package.current succeeds.
probe = subprocess.run(
[HERMES_PYTHON, "-I", "-c",
f"import sys; sys.path.insert(0, {str(REPO)!r}); "
f"from {MODULE} import _load_projection; "
"from winrt.windows.foundation import IAsyncOperation, Uri; "
"uri = Uri('https://example.invalid/updates.appinstaller'); "
"assert uri.absolute_uri == 'https://example.invalid/updates.appinstaller'; "
"assert callable(IAsyncOperation.get); "
"_load_projection(); "
"print('WINRT_CHECKER_TYPES_OK')"],
cwd=tmp_path, capture_output=True, text=True, timeout=30,
)
assert probe.returncode == 0, probe.stderr
assert probe.stdout.strip() == "WINRT_CHECKER_TYPES_OK"