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.
119 lines
4.8 KiB
Python
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"
|