aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/tests/test_packaging_win.py
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-node/tests/test_packaging_win.py')
-rw-r--r--packages/meshbay-node/tests/test_packaging_win.py220
1 files changed, 152 insertions, 68 deletions
diff --git a/packages/meshbay-node/tests/test_packaging_win.py b/packages/meshbay-node/tests/test_packaging_win.py
index b5f6191..6f6388e 100644
--- a/packages/meshbay-node/tests/test_packaging_win.py
+++ b/packages/meshbay-node/tests/test_packaging_win.py
@@ -1263,35 +1263,41 @@ def test_create_group_page_falls_back_when_no_node_is_bundled():
# ------------------------------------------------------------------------
-# The "MSIX" target: same feature set as Full, packaged for Microsoft Store
-# submission instead of NSIS. Unlike Light, this target keeps the node
-# runtime and both service scripts -- what changes is packaging format, not
-# what ships.
+# The "MSIX" target: the bundled node, packaged for Microsoft Store
+# submission instead of NSIS. What the NSIS build does with scripts (firewall
+# rules, a sign-in launcher, a PATH entry) this package declares in its
+# manifest, because everything a script writes names the versioned install
+# folder each Store update deletes. At boot is the .exe installer's alone.
# Weak, text-reading evidence throughout, same reasoning as the rest of
# this file: there is no electron-builder/appx runner here either.
# ------------------------------------------------------------------------
-def test_msix_config_is_standalone_and_keeps_the_full_bundle():
+def _yml_from_entries(yml: str) -> list[str]:
+ """The `from:` of every extraResources entry: directives, not the prose
+ around them, which names the very scripts it explains are absent."""
+ return [ln.split("from:", 1)[1].strip() for ln in yml.splitlines()
+ if ln.strip().startswith("- from:")]
+
+
+def test_msix_config_is_standalone_and_ships_the_node_not_the_scripts():
"""
- Unlike Light, MSIX ships the same node-runtime/ffmpeg/service scripts as
- Full -- an AppX install never elevating is not a reason to drop the
- daemon, only to change how its two elevated operations get triggered
- (see the two tests below). --config still
- means this file is read alone (app-builder-lib's getConfig), so it
- cannot silently inherit Full's package.json build.nsis or any signing
- config meant for NSIS.
+ --config means this file is read alone (app-builder-lib's getConfig), so
+ it cannot silently inherit Full's package.json build.nsis or any signing
+ config meant for NSIS. It ships the node runtime and none of the scripts
+ whose output names the install folder: on a real Store install each of
+ them was stale after the next update.
"""
assert MSIX_YML.exists(), f"{MSIX_YML} is missing"
yml = MSIX_YML.read_text(encoding="utf-8")
-
assert "appId: org.meshbay.client" in yml
assert "target: appx" in yml
assert "output: dist-msix" in yml
- assert "node-runtime" in yml
- assert "service.ps1" in yml
- assert "service-mode.ps1" in yml
- assert "firewall.ps1" in yml
+ sources = _yml_from_entries(yml)
+ assert "node-runtime" in sources
+ for script in ("firewall.ps1", "service.ps1", "service-mode.ps1", "ensure-node-path.ps1"):
+ assert not any(src.endswith(script) for src in sources), (
+ f"{script} must not ship in the Store package (see this test's docstring)")
def test_msix_identity_matches_the_partner_center_reservation():
@@ -1333,51 +1339,100 @@ def test_msix_declares_no_csc_on_purpose():
assert forbidden not in yml
-def test_msix_declares_the_network_capabilities_firewall_ps1_would_add():
+MSIX_MANIFEST_XML = CLIENT / "build" / "appx-manifest.xml"
+
+
+def test_msix_declares_the_firewall_rules_of_both_node_executables():
"""
- Matches firewall.ps1's own rules, which are `-Profile Any` (private AND
- public network). Whether Windows actually auto-exempts a full-trust
- packaged app on the strength of these declarations is unverified until
- sideloaded, but the declaration itself must at least match what the
- elevated NSIS path grants today, or
- an MSIX install would be silently narrower than Full/Light.
+ Declared rules are created at install without an administrator prompt,
+ follow the executable's versioned path on every update and go with the
+ package (all three measured on a real install). The capabilities
+ internetClientServer / privateNetworkClientServer were here before and
+ covered nothing: they make rules for sandboxed apps, which a full-trust
+ process is not, and a listener got the Windows prompt regardless.
"""
yml = MSIX_YML.read_text(encoding="utf-8")
- caps_block = yml.split("capabilities:", 1)[1].split("customExtensionsPath", 1)[0]
- assert "internetClientServer" in caps_block
- assert "privateNetworkClientServer" in caps_block
+ assert "customManifestPath: build/appx-manifest.xml" in yml
+ assert MSIX_MANIFEST_XML.exists(), f"{MSIX_MANIFEST_XML} is missing"
+ xml = MSIX_MANIFEST_XML.read_text(encoding="utf-8")
+ package_level = xml.split("</Applications>", 1)[1]
+ for exe in ("meshbay-node.exe", "meshbay-nodew.exe"):
+ block = package_level.split(f'Executable="app\\resources\\node-runtime\\{exe}"', 1)
+ assert len(block) == 2, f"no firewall rules for {exe}"
+ rules = block[1].split("</desktop2:FirewallRules>", 1)[0]
+ for proto in ("TCP", "UDP"):
+ assert f'Direction="in" IPProtocol="{proto}" Profile="all"' in rules, (exe, proto)
+ assert 'Category="windows.firewallRules"' in package_level
+ assert 'xmlns:desktop2="http://schemas.microsoft.com/appx/manifest/desktop/windows10/2"' in xml
-def test_msix_startup_task_targets_the_node_not_the_electron_shell():
+def test_msix_manifest_keeps_every_macro_of_electron_builders_template():
+ """AppxTarget.js fills the custom template the way it fills its own. One
+ dropped from ours would leave a field electron-builder computes (identity,
+ version, languages) out; one it does not know fails the build, comments
+ included ("Macro macros is not defined", from a comment that said so)."""
+ xml = MSIX_MANIFEST_XML.read_text(encoding="utf-8")
+ stock = CLIENT / "node_modules" / "app-builder-lib" / "templates" / "appx" / "appxmanifest.xml"
+ if not stock.exists():
+ pytest.skip("electron-builder is not installed (npm ci in packages/meshbay-client)")
+ macros = set(re.findall(r"\$\{([a-zA-Z0-9]+)\}", stock.read_text(encoding="utf-8")))
+ assert macros == set(re.findall(r"\$\{([a-zA-Z0-9]+)\}", xml))
+
+
+def test_msix_manifest_fragments_are_valid_xml_comments_included():
+ """makeappx refuses a manifest with "--" inside a comment ('>' expected),
+ and only at the very end of a build."""
+ for path in (MSIX_MANIFEST_XML, MSIX_EXTENSIONS_XML):
+ for comment in re.findall(r"<!--(.*?)-->", path.read_text(encoding="utf-8"), re.S):
+ assert "--" not in comment, (path.name, comment[:60])
+
+
+def test_msix_startup_task_is_the_windowless_node_and_off_by_default():
"""
- addAutoLaunchExtension's built-in windows.startupTask (app-builder-lib's
- AppxTarget.js) always targets the package's own main executable -- the
- Electron shell -- which is not what "starts at sign in" means today
- (main.js's WIN_STARTUP_VBS launches meshbay-node.exe directly, keeping
- the daemon running whether or not the UI is ever opened). This config
- must NOT use addAutoLaunchExtension for that reason, and must instead
- supply its own extension via customExtensionsPath pointing at the node
- binary's in-package path -- app\\resources\\node-runtime\\meshbay-node.exe,
- derived from AppxTarget.js's own `"app\\\\" + appOutDir-relative path`
- mapping (build() in that file), which is not the same prefix
- process.resourcesPath resolves to at runtime and easy to get wrong.
+ Windows runs a startup task's executable as is: the console build opened
+ a window whose close button stopped the node, so the task runs
+ meshbay-nodew.exe. Off by default -- it was on, and started the node at
+ every sign-in whatever mode the Node page said; the app switches it now.
+ Not addAutoLaunchExtension, which always starts the Electron shell.
"""
yml = MSIX_YML.read_text(encoding="utf-8")
- # The comment explaining *why* addAutoLaunchExtension is not used
- # necessarily names it -- check the directive, not the prose (the same
- # "parse directives, not text" mistake CLAUDE.md's engineering lessons
- # already record for a differently-shaped bug).
lines = [ln.strip() for ln in yml.splitlines()]
- assert not any(ln.startswith("addAutoLaunchExtension:") for ln in lines), (
- "must not set addAutoLaunchExtension -- it always targets the "
- "Electron shell, not the node binary (see this test's docstring)")
+ assert not any(ln.startswith("addAutoLaunchExtension:") for ln in lines)
assert "customExtensionsPath: build/appx-extensions.xml" in yml
+ ext = MSIX_EXTENSIONS_XML.read_text(encoding="utf-8")
+ task = ext.split('Category="windows.startupTask"', 1)[1].split("</desktop:Extension>", 1)[0]
+ assert 'Executable="app\\resources\\node-runtime\\meshbay-nodew.exe"' in task
+ assert 'TaskId="MeshBayNodeStartup"' in task and 'Enabled="false"' in task
+ # The id the CLI asks Windows for.
+ from meshbay_node import platform as plat
+ assert 'TaskId="' + plat.STARTUP_TASK_ID + '"' in task
- assert MSIX_EXTENSIONS_XML.exists(), f"{MSIX_EXTENSIONS_XML} is missing"
+
+def test_msix_gives_the_cli_an_execution_alias_and_the_windows_it_needs():
+ """`meshbay-node` in a terminal: an alias keeps one path across versions,
+ where the PATH entry the app used to add named the install folder.
+ Aliases need Windows 10 1709; the declared minimum is 1809."""
ext = MSIX_EXTENSIONS_XML.read_text(encoding="utf-8")
- assert 'Category="windows.startupTask"' in ext
- assert 'Executable="app\\resources\\node-runtime\\meshbay-node.exe"' in ext
- assert 'EntryPoint="Windows.FullTrustApplication"' in ext
+ alias = ext.split('Category="windows.appExecutionAlias"', 1)[1]
+ assert 'Executable="app\\resources\\node-runtime\\meshbay-node.exe"' in alias
+ assert 'Alias="meshbay-node.exe"' in alias
+ yml = MSIX_YML.read_text(encoding="utf-8")
+ assert "minVersion: 10.0.17763.0" in yml
+ xml = MSIX_MANIFEST_XML.read_text(encoding="utf-8")
+ assert 'xmlns:uap5="http://schemas.microsoft.com/appx/manifest/uap/windows10/5"' in xml
+
+
+def test_the_node_runtime_has_a_windowless_daemon_beside_the_cli():
+ spec = (WIN / "meshbay-node.spec").read_text(encoding="utf-8")
+ windowless = spec.split('name="meshbay-nodew"', 1)
+ assert len(windowless) == 2, "meshbay-node.spec must build meshbay-nodew.exe"
+ assert "console=False" in windowless[1].split(")", 1)[0]
+ coll = spec.split("COLLECT(", 1)[1].split(")", 1)[0]
+ assert "exe_windowless" in coll, "both executables share one _internal/"
+ assert '"meshbay-nodew.exe"' in (WIN / "build-node-runtime.ps1").read_text(encoding="utf-8")
+ # Started with no stdio at all, it must not die on a library's write.
+ entry = (WIN / "node-entry.py").read_text(encoding="utf-8")
+ assert "os.devnull" in entry and entry.index("os.devnull") < entry.index("import main")
def test_msix_ships_the_four_required_tile_images():
@@ -1430,21 +1485,49 @@ def test_build_win_msix_points_electron_builder_at_the_system_sdk():
assert "makeappx.exe" in src
-# ── main.js needs nothing new for this target ──────────────────────────────
-#
-# Unlike Light (which needed hasBundledNode/winCanElevateServiceMode/the
-# create-group gate because the bundle itself is smaller), MSIX ships
-# everything Full does, and the two elevation paths it cannot get from an
-# installer already exist independently of installer.nsh:
-# firewall.ps1's per-first-use Windows prompt (no admin needed for that
-# fallback -- see firewall.ps1's own header) and main.js's
-# winElevateServiceMode(), driven from the Node page ("the other door",
-# already exercised by test_can_elevate_checks_service_mode_ps1_actually_
-# exists above) rather than from setup. There is deliberately no
-# MSIX-specific test here pinning main.js: the Light-target tests above
-# already pin that winCanElevateServiceMode() checks for service-mode.ps1's
-# presence generically, which is exactly what makes it work for a third
-# packaged target without being told about it.
+# ── main.js in the Store package ──────────────────────────────────────────
+
+def test_main_js_switches_the_startup_task_in_the_store_package():
+ """In the Store package "at sign-in" is the package's startup task, read
+ and switched through the node's CLI, which has the package's identity
+ where a powershell.exe the app started had none ("Element not found").
+ Never the Startup-folder .vbs, whose path every update deletes."""
+ src = MAIN_JS.read_text(encoding="utf-8")
+ assert "const WIN_STORE = process.platform === 'win32' && Boolean(process.windowsStore);" in src
+ task_fn = _fn_body(src, "async function winStartupTask(sub)")
+ assert "winNodeCli(['autostart', sub])" in task_fn
+ assert "powershell" not in task_fn.lower()
+ assert "STARTUP_TASK_TTL_MS" in _fn_body(src, "async function winSigninEnabled()"), (
+ "the Node page polls; each uncached answer is a process")
+ signin = _fn_body(src, "async function winSigninEnabled()")
+ assert "if (!WIN_STORE) return winAutostartInstalled();" in signin
+ assert "winStartupTask('status')" in signin
+ assert "await winSigninEnabled()" in _fn_body(src, "async function winStartupMode()")
+ handler = src.split("handle('node:autostart'", 1)[1].split("\n });\n", 1)[0]
+ assert "winStartupTask('install')" in handler and "winStartupTask('remove')" in handler
+ # Every reader of the sign-in state asks the same function.
+ callers = [ln for ln in src.splitlines() if "winAutostartInstalled()" in ln
+ and not ln.strip().startswith("//") and "function winAutostartInstalled" not in ln]
+ assert len(callers) == 1, callers
+
+
+def test_main_js_offers_no_service_mode_and_adds_no_path_entry_in_the_store_package():
+ src = MAIN_JS.read_text(encoding="utf-8")
+ assert "!WIN_STORE" in _fn_body(src, "function winCanElevateServiceMode()")
+ status = _fn_body(src, "async function nodeServiceStatus()")
+ assert "store: WIN_STORE" in status
+ path_fn = src.split("function winEnsureNodeOnPath()", 1)[1].split("\n }", 1)[0]
+ assert "if (WIN_STORE) return;" in path_fn
+ page = (HUB_STATIC / "node-page.js").read_text(encoding="utf-8")
+ assert "info.store" in page and "node.startup_mode_service_store_hint" in page
+
+
+def test_main_js_counts_the_windowless_daemon_as_a_node():
+ """A node started at sign-in by the startup task is meshbay-nodew.exe:
+ Stop, the status and Quit must see it as they see meshbay-node.exe."""
+ src = MAIN_JS.read_text(encoding="utf-8")
+ pids = _fn_body(src, "function winNodePids()")
+ assert "'IMAGENAME eq meshbay-node*'" in pids
# ------------------------------------------------------------------------
@@ -1479,13 +1562,14 @@ def test_ensure_node_path_script_is_idempotent_and_unelevated():
"installer.nsh's own SendMessage")
-def test_ensure_node_path_shipped_to_full_and_msix_not_light():
+def test_ensure_node_path_shipped_to_full_only():
pkg = _pkg()
full_yml = json.dumps(pkg["build"])
assert "ensure-node-path.ps1" in full_yml
- msix_yml = MSIX_YML.read_text(encoding="utf-8")
- assert "ensure-node-path.ps1" in msix_yml
+ msix_sources = _yml_from_entries(MSIX_YML.read_text(encoding="utf-8"))
+ assert not any(src.endswith("ensure-node-path.ps1") for src in msix_sources), (
+ "the Store package has an execution alias instead")
light_yml = LIGHT_YML.read_text(encoding="utf-8")
assert "ensure-node-path.ps1" not in light_yml, (