summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/tests/test_packaging_win.py
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-09-04 09:28:14 +0200
committerChristophe Besson <cbesson@gmail.com>2026-09-04 09:28:14 +0200
commit3ce52774760b222d94d78bc0118e9da2662a809f (patch)
tree52a16c7e05080869fbb17b83665b2fb2048b21e2 /packages/meshbay-node/tests/test_packaging_win.py
parent220e6e701806213a576ce80fa655cd9cf4a51880 (diff)
downloadmeshbay-3ce52774760b222d94d78bc0118e9da2662a809f.tar.gz
feat: Windows installer (W4) — one per-user NSIS package, client + node
`npm run dist:win` produces MeshBay-Setup-<version>.exe: the Electron client and, beside it under resources/node-runtime/, the frozen meshbay-node daemon (meshbay-common inside it). No hub. Per-user, no elevation — matches the W3 constraint that a logon-triggered scheduled task needs admin. electron-builder / package.json build.win nsis, build/icon.ico, extraResources -> node-runtime/ build.nsis oneClick:false perMachine:false allowElevation:false allowToChangeInstallationDirectory:true dist:win -> packaging/win/build-win.ps1 (mirrors dist -> build-client.sh) packaging/win/ meshbay-node.spec + node-entry.py PyInstaller freeze of meshbay_node.daemon:main. The awkward deps (aiortc, av, aioquic, pydantic_core, uvicorn, watchdog, guessit, blake3, tzdata) are pulled in whole with collect_all — that list is expected to grow when a frozen run raises ModuleNotFoundError. build-node-runtime.ps1 throwaway venv -> pip install -> PyInstaller -> packages/meshbay-client/node-runtime/ (gitignored) build-win.ps1 Node>=22 check, npm ci, Electron bump, sync-ui, node runtime, electron-builder --win nsis bump-electron.mjs the Chromium-CVE "build against latest Electron" policy, out of the PS script (5.1 here-string terminator rules) README.md PyInstaller, not the python-embed zip: the frozen meshbay-node.exe is a genuine relocatable single binary, which is what src/main.js:findNodeBinary spawns (process.resourcesPath/node-runtime/meshbay-node.exe when packaged) and what the W3 autostart launcher points at. The embeddable zip needs pip to make that wrapper and the wrapper bakes in an absolute interpreter path. build/installer.nsh: on uninstall, taskkill meshbay-node.exe and delete the W3 Startup .vbs (it would point wscript at a deleted binary every sign-in). %LOCALAPPDATA%\meshbay\ — node.toml, keystore.enc — is never touched. ffmpeg is not bundled by default (node finds it on PATH); build-win.ps1 -FfmpegDir copies ffmpeg.exe/ffprobe.exe in for a self-contained installer. Verified on the Windows guest: PyInstaller freeze builds first try (node-runtime 147 MB), frozen `meshbay-node status` talks to the live daemon's loopback API; electron-builder --win nsis produces MeshBay-Setup-0.1.0.exe (155 MB), oneClick/perMachine flags applied, node-runtime bundled at the path findNodeBinary expects. test_packaging_win.py (14) pins the config invariants and the NSIS <-> platform.py autostart seam. Node suite 798 pass / 34 skip. Open: Authenticode signing (13.9 — unsigned => SmartScreen), Windows CI (18.3), electron-updater. First clean-machine install + DPAPI + autostart round-trip is a manual check. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-node/tests/test_packaging_win.py')
-rw-r--r--packages/meshbay-node/tests/test_packaging_win.py153
1 files changed, 153 insertions, 0 deletions
diff --git a/packages/meshbay-node/tests/test_packaging_win.py b/packages/meshbay-node/tests/test_packaging_win.py
new file mode 100644
index 0000000..1b7521a
--- /dev/null
+++ b/packages/meshbay-node/tests/test_packaging_win.py
@@ -0,0 +1,153 @@
+"""
+The Windows installer (W4): a single per-user NSIS package carrying the Electron
+client and the frozen node daemon.
+
+Like test_packaging_units.py this reads the config rather than building anything
+— there is no electron-builder or PyInstaller run here. Weak evidence, and the
+right kind for the defects it guards against: a per-machine flag that would make
+the installer demand admin, a build step wired to the wrong file, the 150 MB
+node-runtime artifact slipping into git, the autostart seam between the NSIS
+uninstaller and meshbay_node.platform drifting apart.
+"""
+
+import json
+import re
+from pathlib import Path
+
+import pytest
+
+ROOT = Path(__file__).resolve().parents[3]
+CLIENT = ROOT / "packages" / "meshbay-client"
+PKG_JSON = CLIENT / "package.json"
+WIN = ROOT / "packaging" / "win"
+NSH = CLIENT / "build" / "installer.nsh"
+
+pytestmark = pytest.mark.skipif(
+ not PKG_JSON.exists() or not WIN.exists(),
+ reason="Windows packaging not present")
+
+
+def _pkg() -> dict:
+ return json.loads(PKG_JSON.read_text(encoding="utf-8"))
+
+
+# ── electron-builder: Windows target ────────────────────────────────────────
+
+def test_the_windows_target_is_nsis_with_the_committed_icon():
+ win = _pkg()["build"]["win"]
+ assert win["target"] == "nsis"
+ icon = CLIENT / win["icon"]
+ assert icon.suffix == ".ico" and icon.exists(), f"{icon} is missing"
+
+
+def test_the_installer_is_per_user_and_never_asks_for_admin():
+ """
+ A logon-triggered scheduled task needs elevation (that is why W3 uses the
+ Startup folder), and the whole desktop design is no-admin. perMachine or
+ allowElevation here would undo that at install time.
+ """
+ nsis = _pkg()["build"]["nsis"]
+ assert nsis["oneClick"] is False
+ assert nsis["perMachine"] is False
+ assert nsis["allowElevation"] is False
+ assert nsis["allowToChangeInstallationDirectory"] is True
+
+
+def test_the_node_runtime_is_carried_as_an_extraresource():
+ """
+ PyInstaller output lands in packages/meshbay-client/node-runtime/ and rides
+ into the package under resources/node-runtime/. src/main.js:findNodeBinary
+ resolves exactly that path (process.resourcesPath / node-runtime /
+ meshbay-node.exe), so the two names must agree.
+ """
+ extra = _pkg()["build"]["win"]["extraResources"]
+ entry = next((e for e in extra if e.get("to") == "node-runtime"), None)
+ assert entry, "no extraResources entry mapping to node-runtime"
+ assert entry["from"] == "node-runtime"
+
+ main_js = (CLIENT / "src" / "main.js").read_text(encoding="utf-8")
+ assert "'node-runtime', 'meshbay-node.exe'" in main_js, (
+ "findNodeBinary no longer looks for the bundled daemon where "
+ "extraResources puts it")
+
+
+def test_dist_win_delegates_to_the_build_script():
+ """`dist` (Linux) delegates to build-client.sh; `dist:win` is its
+ counterpart and must not be a second inline electron-builder invocation."""
+ scripts = _pkg()["scripts"]
+ assert "dist:win" in scripts
+ assert "build-win.ps1" in scripts["dist:win"]
+ # `dist` stays Linux-only and unchanged (test_desktop_shell.py guards it too).
+ assert "win" not in scripts["dist"].lower()
+
+
+# ── the build scripts exist and point at real files ────────────────────────
+
+@pytest.mark.parametrize("name", [
+ "build-win.ps1",
+ "build-node-runtime.ps1",
+ "meshbay-node.spec",
+ "node-entry.py",
+ "README.md",
+])
+def test_packaging_win_ships_its_scripts(name):
+ assert (WIN / name).exists(), f"packaging/win/{name} is missing"
+
+
+def test_the_pyinstaller_entry_point_is_the_daemon_main():
+ src = (WIN / "node-entry.py").read_text(encoding="utf-8")
+ assert "from meshbay_node.daemon import main" in src
+ assert "main()" in src
+
+
+def test_the_spec_pulls_in_the_awkward_dependencies_whole():
+ """
+ The C/Rust-extension and dynamic-import packages are the ones PyInstaller's
+ static pass drops. If someone trims collect_all to shrink the build, the
+ frozen daemon fails at runtime, not at build time.
+ """
+ spec = (WIN / "meshbay-node.spec").read_text(encoding="utf-8")
+ for pkg in ("aiortc", "av", "aioquic", "pydantic_core", "uvicorn",
+ "watchdog", "guessit", "blake3", "meshbay_node", "meshbay_common"):
+ assert re.search(rf'["\']{re.escape(pkg)}["\']', spec), (
+ f"{pkg} dropped from the PyInstaller spec's collect list")
+
+
+# ── the artifact never gets committed ──────────────────────────────────────
+
+def test_the_node_runtime_output_is_gitignored():
+ """It is ~150 MB of frozen Python. The `ui/` fork guard in
+ test_desktop_shell.py exists for the same reason."""
+ gitignore = (ROOT / ".gitignore").read_text(encoding="utf-8")
+ assert "packages/meshbay-client/node-runtime/" in gitignore
+
+
+# ── the NSIS ↔ platform.py autostart seam ─────────────────────────────────
+
+def test_the_uninstaller_clears_the_autostart_launcher():
+ """
+ W3's `meshbay-node autostart install` drops a .vbs in the Startup folder
+ (meshbay_node.platform._startup_vbs). After an uninstall it would point
+ wscript at a deleted binary every sign-in, so customUnInstall must delete
+ it — and at the path platform.py actually uses.
+ """
+ from meshbay_node import platform as plat
+
+ nsh = NSH.read_text(encoding="utf-8")
+ assert "!macro customUnInstall" in nsh
+ assert "taskkill /IM meshbay-node.exe /F" in nsh
+
+ # The tail platform.py builds, made NSIS-relative ($APPDATA == %APPDATA%).
+ tail = plat._startup_vbs()
+ parts = tail.parts
+ i = parts.index("Microsoft")
+ rel = "\\".join(parts[i:]) # Microsoft\...\Startup\MeshBay Node.vbs
+ assert rel in nsh, (
+ f"customUnInstall does not delete {rel!r} — the W3 autostart path "
+ "changed and installer.nsh was not updated")
+
+
+def test_customInstall_stops_a_running_daemon_before_overwriting_it():
+ nsh = NSH.read_text(encoding="utf-8")
+ body = nsh.split("!macro customInstall", 1)[1].split("!macroend", 1)[0]
+ assert "taskkill /IM meshbay-node.exe /F" in body