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.
116 lines
4.7 KiB
Python
116 lines
4.7 KiB
Python
"""Store update contracts use the actual API's sequence and result shapes."""
|
|
from __future__ import annotations
|
|
|
|
import importlib
|
|
import json
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
from enum import IntEnum
|
|
from pathlib import Path
|
|
from types import SimpleNamespace
|
|
|
|
import pytest
|
|
|
|
REPO = Path(__file__).resolve().parents[2]
|
|
MODULE = "hermes_cli.windows_store_update"
|
|
|
|
|
|
class State(IntEnum):
|
|
DOWNLOADING = 1
|
|
COMPLETED = 3
|
|
CANCELED = 4
|
|
OTHER_ERROR = 5
|
|
|
|
|
|
class Operation:
|
|
def __init__(self, result):
|
|
self.result = result
|
|
self.progress = None
|
|
|
|
|
|
class Context:
|
|
def __init__(self, updates, state=State.COMPLETED):
|
|
self.updates = updates
|
|
self.state = state
|
|
self.calls = []
|
|
|
|
def get_app_and_optional_store_package_updates_async(self):
|
|
self.calls.append("check")
|
|
return Operation(self.updates)
|
|
|
|
def request_download_store_package_updates_async(self, updates):
|
|
assert updates is self.updates
|
|
self.calls.append("download")
|
|
return Operation(SimpleNamespace(overall_state=self.state))
|
|
|
|
def request_download_and_install_store_package_updates_async(self, updates):
|
|
assert updates is self.updates
|
|
self.calls.append("install")
|
|
return Operation(SimpleNamespace(overall_state=self.state))
|
|
|
|
|
|
@pytest.fixture
|
|
def checker():
|
|
return importlib.import_module(MODULE)
|
|
|
|
|
|
@pytest.mark.parametrize("mode", ["check", "download", "install"])
|
|
@pytest.mark.parametrize("state", [State.COMPLETED, State.CANCELED, State.DOWNLOADING])
|
|
def test_store_operation_passes_update_sequence_and_requires_completed(checker, mode, state):
|
|
updates = [SimpleNamespace(package=SimpleNamespace(id=SimpleNamespace(full_name="package-v2")))]
|
|
context = Context(updates, state)
|
|
result = checker.perform(context, mode, lambda operation: operation.result, State.COMPLETED)
|
|
assert context.calls == (["check"] if mode == "check" else ["check", mode])
|
|
assert result["available"] is True
|
|
assert result["packages"] == ["package-v2"]
|
|
if mode != "check":
|
|
assert result["ok"] is (state == State.COMPLETED)
|
|
assert ("error" in result) is (state != State.COMPLETED)
|
|
empty = Context([])
|
|
assert checker.perform(empty, mode, lambda operation: operation.result, State.COMPLETED) == {
|
|
"available": False, "packages": [], "ok": True,
|
|
}
|
|
assert empty.calls == ["check"]
|
|
|
|
|
|
@pytest.mark.platforms("windows")
|
|
def test_native_projection_has_required_update_contract(tmp_path):
|
|
code = (
|
|
"from winrt.runtime import init_apartment,uninit_apartment,ApartmentType; "
|
|
"from winrt.runtime.interop import initialize_with_window; "
|
|
"from winrt.windows.services.store import StoreContext,StorePackageUpdateState; "
|
|
"from winrt.windows.foundation import IAsyncOperation, IAsyncOperationWithProgress; "
|
|
"import winrt.windows.foundation.collections,win32gui; "
|
|
"init_apartment(ApartmentType.SINGLE_THREADED); "
|
|
"c=StoreContext.get_default(); "
|
|
"assert callable(c.get_app_and_optional_store_package_updates_async); "
|
|
"assert callable(c.request_download_store_package_updates_async); "
|
|
"assert callable(c.request_download_and_install_store_package_updates_async); "
|
|
"assert callable(initialize_with_window); "
|
|
"assert StorePackageUpdateState.COMPLETED != StorePackageUpdateState.DOWNLOADING; "
|
|
"assert callable(IAsyncOperation.get); "
|
|
"assert callable(IAsyncOperationWithProgress.get); "
|
|
"op=c.get_app_and_optional_store_package_updates_async(); "
|
|
"assert callable(op.get_results); op.cancel(); del op; "
|
|
"assert callable(win32gui.PumpWaitingMessages); "
|
|
"del c; uninit_apartment(); print('store projection ready')"
|
|
)
|
|
result = subprocess.run([sys.executable, "-I", "-c", code], cwd=tmp_path,
|
|
capture_output=True, text=True, timeout=30)
|
|
assert result.returncode == 0, result.stderr
|
|
assert result.stdout.strip() == "store projection ready"
|
|
|
|
|
|
def test_entry_reports_json_when_the_projection_is_unavailable(tmp_path):
|
|
# The desktop spawns `python -P -m <module>` and parses stdout as JSON; a
|
|
# failure inside the checker must still reach it as JSON, never as nothing.
|
|
(tmp_path / "winrt.py").write_text("raise ImportError('fixture unavailable')", encoding="utf-8")
|
|
child = subprocess.run([sys.executable, "-P", "-m", MODULE, "--mode", "check"], 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["ok"] is False and payload["available"] is None
|
|
assert "fixture unavailable" in payload["error"]
|