aboutsummaryrefslogtreecommitdiffstats
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.py249
1 files changed, 249 insertions, 0 deletions
diff --git a/packages/meshbay-node/tests/test_packaging_win.py b/packages/meshbay-node/tests/test_packaging_win.py
index 7421a8c..b223156 100644
--- a/packages/meshbay-node/tests/test_packaging_win.py
+++ b/packages/meshbay-node/tests/test_packaging_win.py
@@ -21,6 +21,20 @@ CLIENT = ROOT / "packages" / "meshbay-client"
PKG_JSON = CLIENT / "package.json"
WIN = ROOT / "packaging" / "win"
NSH = CLIENT / "build" / "installer.nsh"
+MAIN_JS = CLIENT / "src" / "main.js"
+PRELOAD_JS = CLIENT / "src" / "preload.js"
+
+# The "Light" target: Electron client + UI, no bundled node. See
+# C:\Users\admin\devel\light-client.md for the evaluation this implements.
+LIGHT_NSH = CLIENT / "build" / "installer-light.nsh"
+LIGHT_YML = WIN / "electron-builder.light.yml"
+BUILD_WIN_LIGHT = WIN / "build-win-light.ps1"
+BUILD_WIN_COMMON = WIN / "build-win-common.ps1"
+
+# sync-ui.js copies this into CLIENT/ui/ verbatim -- read the source of
+# truth, same as every other cross-package check in this file already does
+# for meshbay-client's own src/.
+HUB_STATIC = ROOT / "packages" / "meshbay-hub" / "src" / "meshbay_hub" / "static"
pytestmark = pytest.mark.skipif(
not PKG_JSON.exists() or not WIN.exists(),
@@ -659,3 +673,238 @@ def test_ffmpeg_bundling_is_the_default_not_opt_in():
win_src = (WIN / "build-win.ps1").read_text(encoding="utf-8")
assert "SkipFfmpeg" in win_src
assert "FfmpegDir" not in win_src
+
+
+# ------------------------------------------------------------------------
+# The "Light" target: Electron client + UI, no bundled node. See
+# C:\Users\admin\devel\light-client.md for the evaluation. Weak, text-
+# reading evidence throughout, same reasoning as the rest of this file:
+# there is no electron-builder/PowerShell/NSIS runner here, and it is the
+# right kind of evidence for what these guard against -- a config drifting
+# back to carrying the node-runtime it must not, or the two orchestrators'
+# shared steps diverging silently.
+# ------------------------------------------------------------------------
+
+def test_light_config_is_standalone_and_ships_no_node():
+ """
+ electron-builder.light.yml is passed via --config, which (per
+ app-builder-lib/out/util/config/load.js's getConfig) makes electron-
+ builder read ONLY that file -- package.json's own build field, and
+ therefore its node-runtime/service*.ps1 extraResources, is never even
+ loaded. Pins the config's own content regardless: it must name neither
+ the node runtime nor the two service scripts, and must still carry
+ firewall.ps1 (the one thing Light still needs -- packaging/win/
+ firewall.ps1's header explains why the client needs an inbound rule too,
+ not only a node).
+ """
+ assert LIGHT_YML.exists(), f"{LIGHT_YML} is missing"
+ yml = LIGHT_YML.read_text(encoding="utf-8")
+
+ assert "appId: org.meshbay.client.light" in yml
+ assert "productName: MeshBay Light" in yml
+ assert "output: dist-light" in yml
+
+ assert "node-runtime" not in yml
+ assert "service.ps1" not in yml
+ assert "service-mode.ps1" not in yml
+ assert "firewall.ps1" in yml
+
+ assert "include: installer-light.nsh" in yml, (
+ "nsis.include must point at the Light NSIS customisation, not the "
+ "default build/installer.nsh (Full's)")
+
+
+def test_light_config_keeps_the_same_no_admin_nsis_policy():
+ """Per-user, no elevation at install time -- identical policy to Full's
+ package.json build.nsis, for the same reason (MESHBAY_DESIGN.md
+ 11.2/7.5: the DPAPI-protected hub device key is account-bound)."""
+ yml = LIGHT_YML.read_text(encoding="utf-8")
+ assert "oneClick: false" in yml
+ assert "perMachine: false" in yml
+ assert "allowElevation: false" in yml
+
+
+def test_dist_win_light_delegates_to_the_light_orchestrator():
+ pkg = _pkg()
+ scripts = pkg["scripts"]
+ assert "dist:win:light" in scripts
+ assert "build-win-light.ps1" in scripts["dist:win:light"]
+ assert "dist:win" in scripts
+ assert "build-win.ps1" in scripts["dist:win"]
+ assert "build-win-light.ps1" not in scripts["dist:win"]
+
+
+def test_build_win_light_skips_the_node_runtime_step():
+ """The entire point of Light: no PyInstaller freeze, no ffmpeg fetch."""
+ assert BUILD_WIN_LIGHT.exists(), f"{BUILD_WIN_LIGHT} is missing"
+ src = BUILD_WIN_LIGHT.read_text(encoding="utf-8")
+ assert "build-node-runtime.ps1" not in src
+ assert "electron-builder.light.yml" in src
+ assert "--config" in src
+
+
+def test_build_orchestrators_share_the_common_steps_not_a_copy():
+ """
+ Node check / npm ci / Electron bump / sync-ui must live in exactly one
+ place (build-win-common.ps1), dot-sourced by both -- a copy would let
+ the two drift the way the installer flow itself once did (the W3
+ one-shot dialog bug). Both orchestrators must call the shared
+ functions, neither may inline its own npm ci / Electron-bump logic.
+ """
+ assert BUILD_WIN_COMMON.exists(), f"{BUILD_WIN_COMMON} is missing"
+ common = BUILD_WIN_COMMON.read_text(encoding="utf-8")
+ for fn in ("Assert-NodeVersion", "Invoke-NpmCi", "Invoke-ElectronBump", "Invoke-SyncUi"):
+ assert f"function {fn}" in common, f"{fn} is not defined in build-win-common.ps1"
+
+ full = (WIN / "build-win.ps1").read_text(encoding="utf-8")
+ light = BUILD_WIN_LIGHT.read_text(encoding="utf-8")
+ for src, name in ((full, "build-win.ps1"), (light, "build-win-light.ps1")):
+ assert '. (Join-Path $WinDir "build-win-common.ps1")' in src, (
+ f"{name} does not dot-source build-win-common.ps1")
+ for fn in ("Assert-NodeVersion", "Invoke-NpmCi", "Invoke-ElectronBump", "Invoke-SyncUi"):
+ assert fn in src, f"{name} does not call {fn}"
+ assert "npm ci --ignore-scripts" not in src, (
+ f"{name} must not re-implement npm ci inline")
+
+
+def test_firewall_ps1_skips_the_node_rule_when_the_exe_is_missing():
+ """
+ The one behaviour Light structurally depends on: firewall.ps1 is
+ shipped unforked (test_light_config_is_standalone_and_ships_no_node
+ above), relying on `add` already skipping any rule whose target .exe
+ does not exist -- true for "MeshBay Node" in a Light install, where
+ node-runtime\\meshbay-node.exe is never there. If this guard were ever
+ removed, New-NetFirewallRule would be pointed at a path that does not
+ exist and Light's one firewall elevation would start failing.
+ """
+ src = (WIN / "firewall.ps1").read_text(encoding="utf-8")
+ add_block = src.split('if ($Action -eq "add")', 1)[1]
+ assert "if (-not (Test-Path $r.Path))" in add_block
+ skip_stanza = add_block.split("if (-not (Test-Path $r.Path))", 1)[1].split("}", 1)[0]
+ assert "continue" in skip_stanza
+
+
+def test_light_installer_forces_per_user_and_elevates_firewall_once():
+ assert LIGHT_NSH.exists(), f"{LIGHT_NSH} is missing"
+ nsh = LIGHT_NSH.read_text(encoding="utf-8")
+ install = _macro_body(nsh, "customInstall")
+ mode = _macro_body(nsh, "customInstallMode")
+
+ assert 'StrCpy $isForceCurrentInstall "1"' in mode
+
+ assert "${IfNot} ${Silent}" in install
+ i_check = install.index('firewall.ps1" check')
+ i_runas = install.index('ExecShellWait "runas"')
+ assert i_check < i_runas, "the unelevated check must run before any elevation"
+ assert 'firewall.ps1" add' in install
+
+ assert "MB_AutoMode" not in nsh, "there is no autostart choice for Light"
+ assert "Page custom" not in nsh
+ assert "MessageBox MB_YESNO" not in install, (
+ "customInstall itself must ask nothing -- only customUnInstall's "
+ "opt-in firewall-removal prompt uses MessageBox")
+
+
+def test_light_installer_never_touches_a_co_installed_full_clients_node():
+ """
+ Full and Light can be installed side by side (distinct appId/
+ productName/install dir -- test_light_config_is_standalone_and_ships_
+ no_node above). Light's installer/uninstaller must be a complete no-op
+ with respect to anything a co-installed Full client owns: its node
+ process, its Startup .vbs, its Scheduled Task, its own firewall rule.
+ The easy mistake here is copy-pasting installer.nsh and trimming it,
+ leaving one of these in by accident.
+ """
+ nsh = LIGHT_NSH.read_text(encoding="utf-8")
+ for forbidden in ("taskkill", 'HKCU "Environment"', "service.ps1",
+ "service-mode.ps1", ".vbs", "MeshBay Node.vbs"):
+ assert forbidden not in nsh, (
+ f"installer-light.nsh must not reference {forbidden!r} -- that "
+ "belongs to a co-installed Full client, not to Light")
+
+
+def test_light_uninstaller_offers_to_remove_the_firewall_rules_default_no():
+ nsh = LIGHT_NSH.read_text(encoding="utf-8")
+ uninstall = _macro_body(nsh, "customUnInstall")
+ assert "${IfNot} ${Silent}" in uninstall
+ assert "/SD IDNO" in uninstall
+ assert 'firewall.ps1" remove' in uninstall
+ assert 'ExecShellWait "runas"' in uninstall
+
+
+# ── the app-side gaps a bundle-less build would otherwise hit ──────────────
+
+def test_has_bundled_node_is_windows_only_and_packaged_only():
+ """
+ Off win32, or unpackaged (dev), this must stay true unconditionally --
+ only a packaged Windows build can even be Light, so nothing here may
+ change today's behaviour for Linux, macOS or `npm start`.
+ """
+ src = MAIN_JS.read_text(encoding="utf-8")
+ assert "function hasBundledNode()" in src
+ body = src.split("function hasBundledNode()", 1)[1].split("\n }", 1)[0]
+ assert "process.platform === 'win32'" in body
+ assert "app.isPackaged" in body
+ assert "return true" in body, (
+ "must fall back to true (today's behaviour) off win32 / unpackaged")
+
+
+def test_node_bundled_ipc_channel_exists_end_to_end():
+ """main.js handles it, preload.js exposes it, platform.js wraps it --
+ the same three-layer shape every other node.* capability already has."""
+ main = MAIN_JS.read_text(encoding="utf-8")
+ assert "ipcMain.handle('node:bundled'" in main
+ assert "hasBundledNode()" in main
+
+ preload = PRELOAD_JS.read_text(encoding="utf-8")
+ assert "ipcRenderer.invoke('node:bundled')" in preload
+
+ platform_js = (HUB_STATIC / "platform.js").read_text(encoding="utf-8")
+ assert "bridge.node.bundled()" in platform_js
+
+
+def test_can_elevate_checks_service_mode_ps1_actually_exists():
+ """
+ app.isPackaged alone used to gate canElevate -- true for Light too,
+ where service-mode.ps1 is never shipped, so the Node page would offer
+ "background service" and only fail when clicked. Both nodeServiceStatus
+ branches (service installed, and the per-user Startup branch) must go
+ through the same helper rather than reimplementing the check.
+ """
+ src = MAIN_JS.read_text(encoding="utf-8")
+ assert "function winCanElevateServiceMode()" in src
+ helper = src.split("function winCanElevateServiceMode()", 1)[1].split("\n }", 1)[0]
+ assert "app.isPackaged" in helper
+ assert "'service-mode.ps1'" in helper
+ assert src.count("canElevate: winCanElevateServiceMode()") == 2, (
+ "both nodeServiceStatus branches must use the helper, not a bare "
+ "app.isPackaged")
+ assert "canElevate: app.isPackaged," not in src
+
+
+def test_startup_mode_is_null_not_a_lie_when_no_node_is_found():
+ """
+ The per-user Startup branch used to report mode: 'startup' unconditionally,
+ so a Light install with no node anywhere (bundled or on PATH) showed a
+ working-looking autostart dropdown for a node that did not exist --
+ node-page.js's showStartupRow is gated on typeof info.mode === 'string'.
+ """
+ src = MAIN_JS.read_text(encoding="utf-8")
+ assert "mode: bin ? 'startup' : null," in src
+ assert "mode: 'startup'," not in src
+
+
+def test_create_group_page_falls_back_when_no_node_is_bundled():
+ """
+ The wizard's node-linking step is exactly the known "Detecting local
+ node..." hang when nothing ever answers. Gating it on `bundled` (not
+ just `available`, which is true for Light too -- both are Electron)
+ routes a Light install to CreateGroupFormSimple: not a lesser feature,
+ but the same node-free form a plain browser member already uses.
+ """
+ src = (HUB_STATIC / "create-group-page.js").read_text(encoding="utf-8")
+ body = src.split("export function CreateGroupPage(props) {", 1)[1] \
+ .split("\nfunction CreateGroupFormSimple", 1)[0]
+ assert "platform.node.bundled()" in body
+ assert "platform.node.available && bundled" in body
+ assert "CreateGroupWizard" in body and "CreateGroupFormSimple" in body