summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-node')
-rw-r--r--packages/meshbay-node/tests/test_packaging_win.py114
1 files changed, 114 insertions, 0 deletions
diff --git a/packages/meshbay-node/tests/test_packaging_win.py b/packages/meshbay-node/tests/test_packaging_win.py
index 2cc8f12..6f7573b 100644
--- a/packages/meshbay-node/tests/test_packaging_win.py
+++ b/packages/meshbay-node/tests/test_packaging_win.py
@@ -1102,3 +1102,117 @@ def test_build_win_msix_points_electron_builder_at_the_system_sdk():
# 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.
+
+
+# ------------------------------------------------------------------------
+# Three gaps a real sideload install found (2026-09-12) that reading the
+# manifest and launching the app once had missed: no install-time hook means
+# no equivalent of installer.nsh's PATH write either, an immediate daemon
+# crash was silently discarded instead of surfacing to the user, and nobody
+# told a first-time MSIX user the startup-mode choice existed at all.
+# Confirmed live: process.resourcesPath/spawn work correctly under the
+# installed AppX path, PATH actually gained the entry on next launch, and a
+# real port-18000 collision now rejects in ~2s with the daemon's own stderr
+# instead of a 60s generic timeout.
+# ------------------------------------------------------------------------
+
+ENSURE_NODE_PATH_PS1 = WIN / "ensure-node-path.ps1"
+
+
+def test_ensure_node_path_script_is_idempotent_and_unelevated():
+ """
+ No admin verb, no elevation helper -- a per-user HKCU write never needed
+ elevation in the first place (installer.nsh's customInstall already did
+ this one unelevated); what MSIX lacks is an install-time hook to run
+ anything from, not the right to make this specific change.
+ """
+ assert ENSURE_NODE_PATH_PS1.exists(), f"{ENSURE_NODE_PATH_PS1} is missing"
+ src = ENSURE_NODE_PATH_PS1.read_text(encoding="utf-8")
+ assert "HKEY_CURRENT_USER\\Environment" in src
+ assert "already present" in src, "must be a no-op when the entry already exists"
+ assert "RunAs" not in src and "Verb" not in src
+ assert "WM_SETTINGCHANGE" in src or "SendMessageTimeout" in src, (
+ "must broadcast the change so already-open shells notice, same as "
+ "installer.nsh's own SendMessage")
+
+
+def test_ensure_node_path_shipped_to_full_and_msix_not_light():
+ 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
+
+ light_yml = LIGHT_YML.read_text(encoding="utf-8")
+ assert "ensure-node-path.ps1" not in light_yml, (
+ "Light has no bundled node-runtime to add to PATH")
+
+
+def test_main_js_calls_ensure_node_path_on_every_launch():
+ src = MAIN_JS.read_text(encoding="utf-8")
+ assert "function winEnsureNodeOnPath()" in src
+ body = src.split("function winEnsureNodeOnPath()", 1)[1].split("\n }", 1)[0]
+ assert "hasBundledNode()" in body, "must not run at all for a Light install"
+ assert "ensure-node-path.ps1" in body
+ # Actually invoked, not just defined -- registerBridge() calls it once,
+ # unconditionally, on every launch (idempotent, so Full's already-set
+ # PATH is just a fast no-op query each time).
+ assert src.count("winEnsureNodeOnPath()") >= 2, (
+ "must be both defined and called")
+
+
+def test_node_start_surfaces_an_immediate_daemon_crash_instead_of_a_60s_timeout():
+ """
+ Reproduced live: a daemon that exits within ~1s (a port already bound,
+ reproduced with a second instance colliding on 127.0.0.1:18000) used to
+ be indistinguishable from one that simply never started -- spawn()'s
+ stdio was 'ignore', discarding the exact stderr line that named the real
+ problem, and waitForNode()'s 60s generic timeout was the only failure
+ path left. spawnNodeDetachedWatched watches for an early exit and
+ rejects with the daemon's own tail of stderr instead.
+ """
+ src = MAIN_JS.read_text(encoding="utf-8")
+ assert "function spawnNodeDetachedWatched(" in src
+ body = src.split("function spawnNodeDetachedWatched(", 1)[1].split("\n }", 1)[0]
+ assert "stdio: ['ignore', 'pipe', 'pipe']" in body
+ assert "exited immediately" in body
+ assert "NODE_CRASH_WATCH_MS" in body
+ # The tail must be bounded by length, not by a line count -- a real
+ # capture had the actual OSError line pushed out by two uvicorn/asyncio
+ # tracebacks that followed it, which a short "last N lines" cut before
+ # this was fixed to bound by characters instead.
+ assert "split(/\\r?\\n/).slice(" not in body, (
+ "a line-count tail can cut the one line that names the real error "
+ "-- bound by characters instead (reproduced live, see the comment "
+ "above this constant)")
+ assert "4000" in body
+
+ async_fn = src.split("async function spawnNodeDetached()", 1)[1].split("\n }", 1)[0]
+ assert "spawnNodeDetachedWatched" in async_fn, (
+ "spawnNodeDetached must actually use the watched spawn, not the old "
+ "fire-and-forget one")
+
+
+def test_setup_welcome_hints_at_the_node_startup_choice():
+ """
+ build/installer.nsh's radio page was the only place this choice was ever
+ offered, and an AppX/MSIX install has no install-time page at all to
+ replace it with -- a first-time user of a build with a bundled node
+ otherwise has no reason to ever find the Node page's startup-mode
+ control. Shown only while neither mode is configured yet (so it
+ disappears on its own once one is, or never appears for Light/non-
+ Windows/browser, where platform.node.service.available is false).
+ """
+ src = (HUB_STATIC / "app.js").read_text(encoding="utf-8")
+ fn = src.split("function SetupWelcome(", 1)[1].split("\nfunction ", 1)[0]
+ assert "platform.node.bundled()" in fn
+ assert "platform.node.service.status()" in fn
+ assert "mode === 'service'" in fn and "autostart" in fn
+ assert "setup.node_startup_hint" in fn
+
+
+def test_node_startup_hint_key_exists_in_all_ten_locales():
+ for name in ("en", "fr", "es", "pt-BR", "zh-CN", "ja", "de", "it", "nl", "pl"):
+ cat = (HUB_STATIC / "locales" / f"{name}.js").read_text(encoding="utf-8")
+ assert "'setup.node_startup_hint':" in cat, f"{name}.js is missing the key"