From 9cc2909cb4a360c81b471ceab1d9578a7655a88e Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Sat, 12 Sep 2026 15:48:03 +0200 Subject: fix(packaging): three MSIX first-run regressions found by a real sideload A second-machine sideload of the MSIX target surfaced three things the earlier verification round (which only proved the package installs and runs) had missed: 1. meshbay-node missing from PATH. installer.nsh's customInstall adds node-runtime\ to HKCU\Environment at install time -- an unelevated per-user write, never blocked by MSIX's no-elevation rule, only by the more basic fact that an AppX/MSIX install runs no custom code at all. packaging/win/ensure-node-path.ps1 (idempotent, no admin verb) plus main.js's winEnsureNodeOnPath() do it from the app itself instead, once per launch, shipped to Full and MSIX (not Light, nothing to add there). Verified live via the Node inspector protocol: the entry was in HKCU\Environment\Path after a launch, absent before. 2. A daemon that crashes on startup failed silently. spawnNodeDetached() used stdio: 'ignore', so a real crash reproduced live (a second instance colliding with the first on 127.0.0.1:18000) left waitForNode()'s generic 60s timeout as the only failure ever shown. spawnNodeDetachedWatched() pipes stdio and watches ~2.5s, rejecting immediately with the daemon's own stderr on an early exit; a survivor has its streams released and runs fully detached exactly as before. First version bounded the captured text by line count and a live test showed that cut the actual OSError line -- two uvicorn/asyncio tracebacks followed it in the real capture -- so it is bounded by characters instead. 3. No hint that a startup-mode choice exists. The install-time radio page was the only place this was ever offered, and nothing replaces it now that no install-time page can exist at all. SetupWelcome (the existing first-run banner) grew a conditional hint, shown only while a bundled node is present and neither autostart nor service mode is configured yet. Considered and rejected: linking straight to the Node page -- its route is gated on a linked hub node key, false on the exact fresh-install screen this hint targets, so the link would have been dead on arrival. New key setup.node_startup_hint, added to all ten locale catalogues. test_packaging_win.py gained six tests pinning all three (69 total). Full plan and verification detail: C:\Users\admin\devel\msix-installer.md section 13 (out of repo). Co-Authored-By: Claude Sonnet 5 --- packages/meshbay-client/package.json | 4 + packages/meshbay-client/src/main.js | 92 +++++++++++++++-- packages/meshbay-hub/src/meshbay_hub/static/app.js | 28 +++++ .../src/meshbay_hub/static/locales/de.js | 1 + .../src/meshbay_hub/static/locales/en.js | 1 + .../src/meshbay_hub/static/locales/es.js | 1 + .../src/meshbay_hub/static/locales/fr.js | 1 + .../src/meshbay_hub/static/locales/it.js | 1 + .../src/meshbay_hub/static/locales/ja.js | 1 + .../src/meshbay_hub/static/locales/nl.js | 1 + .../src/meshbay_hub/static/locales/pl.js | 1 + .../src/meshbay_hub/static/locales/pt-BR.js | 1 + .../src/meshbay_hub/static/locales/zh-CN.js | 1 + packages/meshbay-node/tests/test_packaging_win.py | 114 +++++++++++++++++++++ 14 files changed, 239 insertions(+), 9 deletions(-) (limited to 'packages') diff --git a/packages/meshbay-client/package.json b/packages/meshbay-client/package.json index 6d8e711..28e0f3c 100644 --- a/packages/meshbay-client/package.json +++ b/packages/meshbay-client/package.json @@ -49,6 +49,10 @@ { "from": "../../packaging/win/service-mode.ps1", "to": "service-mode.ps1" + }, + { + "from": "../../packaging/win/ensure-node-path.ps1", + "to": "ensure-node-path.ps1" } ] }, diff --git a/packages/meshbay-client/src/main.js b/packages/meshbay-client/src/main.js index d7e3e7b..bebe144 100644 --- a/packages/meshbay-client/src/main.js +++ b/packages/meshbay-client/src/main.js @@ -1000,6 +1000,29 @@ function registerBridge() { return true; } + // build/installer.nsh's customInstall adds node-runtime\ to the per-user + // PATH at install time (HKCU\Environment, no elevation needed for that -- + // it never was the elevation that blocked it here). An AppX/MSIX install + // has no install-time hook at all, so `meshbay-node` in a terminal simply + // never got added for that target -- a real regression found by actually + // running a sideloaded build, not a theoretical gap. Idempotent (the script + // itself checks first) and harmless to call on every launch, NSIS Full + // included, where it is normally already a no-op. Fire-and-forget: a + // terminal convenience is not worth blocking startup or surfacing an error + // dialog over. + function winEnsureNodeOnPath() { + if (process.platform !== 'win32' || !hasBundledNode()) return; + const script = path.join(process.resourcesPath, 'ensure-node-path.ps1'); + if (!fs.existsSync(script)) return; // dev run, or an older build without it + const nodeDir = path.join(process.resourcesPath, 'node-runtime'); + execFile(MB_PWSH, + ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', script, '-NodeDir', nodeDir], + (err, stdout) => { + if (err) { console.error('[path] ensure-node-path.ps1 failed:', err.message); return; } + console.log('[path] node-runtime on PATH:', (stdout || '').trim()); + }); + } + function findNodeBinary() { if (process.platform === 'win32') { // A packaged Windows build carries the frozen daemon as an @@ -1166,18 +1189,67 @@ function registerBridge() { }); } + // A daemon that crashes immediately (a port already in use -- reproduced + // live: a second node instance found 18000 taken by the first -- a corrupt + // config, antivirus interference) used to fail silently: stdio was + // 'ignore', so its stderr was thrown away, and the only failure path left + // was the caller's waitForNode() timing out after a generic 60s ("did not + // start within 60s"). The real reason was sitting on stderr the whole time, + // just never read. This watches for a few seconds -- long enough for any + // startup crash, reproduced consistently well under one second -- and + // rejects with the daemon's own tail of stderr if it exits in that window. + // If it survives the window, stdio is released and it is left fully + // detached, same as before this existed. + const NODE_CRASH_WATCH_MS = 2500; + + function spawnNodeDetachedWatched(bin, args = []) { + return new Promise((resolve, reject) => { + const child = spawn(bin, args, { + detached: true, stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true, + }); + let stderr = ''; + let settled = false; + child.stderr.on('data', (d) => { stderr += d.toString(); }); + // spawn() failures (bad path, a stale PATH entry, antivirus + // interference) land on the ChildProcess as an 'error' event, + // asynchronously -- with no listener, Node rethrows it as an uncaught + // exception and takes the whole main process down with it. + child.on('error', (err) => { + if (settled) return; + settled = true; + reject(err); + }); + child.on('exit', (code, signal) => { + if (settled) return; + settled = true; + // By lines (last 8) at first cut the actual OSError -- a real crash + // captured live logged the bind failure, then two separate uvicorn/ + // asyncio tracebacks *after* it, which pushed it out of a short tail. + // Character-bounded instead: Python's own daemon rarely writes more + // than a couple of screens on a startup crash, so keeping the last + // stretch of raw text is far more likely to still include the one + // line that actually says what went wrong than guessing a line count. + let tail = stderr.trim(); + if (tail.length > 4000) tail = `…${tail.slice(-4000)}`; + reject(new Error( + `meshbay-node exited immediately (code ${code}${signal ? `, signal ${signal}` : ''})` + + (tail ? `:\n${tail}` : ''))); + }); + setTimeout(() => { + if (settled) return; + settled = true; + child.stdout.destroy(); + child.stderr.destroy(); + child.unref(); + resolve(); + }, NODE_CRASH_WATCH_MS); + }); + } + async function spawnNodeDetached() { const bin = await findNodeBinary(); if (!bin) throw new Error('meshbay-node not found on PATH'); - const child = spawn(bin, [], { detached: true, stdio: 'ignore', windowsHide: true }); - // spawn() failures (bad path, a stale PATH entry, antivirus interference) - // land on the ChildProcess as an 'error' event, asynchronously -- with no - // listener, Node rethrows it as an uncaught exception and takes the whole - // main process down with it. The caller's waitForNode() timeout already - // turns "never came up" into a clean message; this only has to keep that - // path reachable instead of crashing first. - child.on('error', (err) => console.error('[node] failed to start:', err.message)); - child.unref(); + await spawnNodeDetachedWatched(bin); } async function waitForNode(deadline) { @@ -1724,6 +1796,8 @@ function registerBridge() { await castChromecast.disconnect(); return true; }); + + winEnsureNodeOnPath(); } /** diff --git a/packages/meshbay-hub/src/meshbay_hub/static/app.js b/packages/meshbay-hub/src/meshbay_hub/static/app.js index ba24822..188b389 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/app.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/app.js @@ -569,10 +569,38 @@ function HomePage({ groups, notifications, onMarkRead, onPurge, allowPublicGroup // ── First-run welcome (Electron-only, shown once on empty home) ───────────── function SetupWelcome({ onDismiss }) { + // An install-time NSIS page used to be the only place this choice was + // ever offered, and an AppX/MSIX install has no install-time page at all + // (no custom actions, full stop, not just no elevation) -- so a build + // running its own bundled node needs to say so somewhere the user will + // actually see it, not just leave the choice sitting unfound on the Node + // page. Shown only while neither startup mode is configured yet; it + // disappears on its own once one is (or stays hidden forever if the + // platform has no node.service at all, e.g. Light, non-Windows, browser). + const [startupHint, setStartupHint] = useState(false); + useEffect(() => { + let cancelled = false; + (async () => { + try { + if (!platform.node.available || !(await platform.node.bundled())) return; + if (!platform.node.service.available) return; + const status = await platform.node.service.status(); + const configured = status.mode === 'service' || Boolean(status.autostart); + if (!cancelled && status.supported !== false && !configured) setStartupHint(true); + } catch { /* best effort -- the Node page itself is the source of truth */ } + })(); + return () => { cancelled = true; }; + }, []); + return html`

${t('setup.welcome_title')}

${t('setup.welcome_message')}

+ ${startupHint && html` +

+ ${t('setup.node_startup_hint')} +

+ `}
${t('setup.create_group')} diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js index a41eeee..2d67bb2 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js @@ -730,6 +730,7 @@ export default { 'setup.node_started': 'Node gestartet.', 'setup.node_not_installed': 'meshbay-node ist auf diesem Rechner nicht installiert.', 'setup.node_install_hint': 'Installieren Sie es mit Ihrem Paketmanager und kommen Sie hierher zurück.', + 'setup.node_startup_hint': 'Sobald Ihr Node läuft, besuchen Sie jederzeit „Node“ in der Seitenleiste, um es automatisch bei der Anmeldung zu starten oder als Hintergrunddienst auszuführen.', 'setup.skip': 'Überspringen — Gruppe ohne lokalen Node erstellen', 'setup.create_group': 'Erste Gruppe erstellen', 'setup.dismiss': 'Einrichtung überspringen', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js index 4561663..88ee3da 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js @@ -593,6 +593,7 @@ export default { 'setup.node_started': 'Node started.', 'setup.node_not_installed': 'meshbay-node is not installed on this machine.', 'setup.node_install_hint': 'Install it with your package manager, then come back here.', + 'setup.node_startup_hint': 'Once your node is running, visit “Node” in the sidebar any time to have it start automatically at sign-in, or run as a background service.', 'setup.skip': 'Skip — create a group without a local node', 'setup.create_group': 'Create your first group', 'setup.dismiss': 'Skip setup', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js index bbfcfa5..8dabc10 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js @@ -725,6 +725,7 @@ export default { 'setup.node_started': 'Node iniciado.', 'setup.node_not_installed': 'meshbay-node no está instalado en esta máquina.', 'setup.node_install_hint': 'Instálelo con su gestor de paquetes y vuelva aquí.', + 'setup.node_startup_hint': 'Una vez que su node esté en funcionamiento, visite «Node» en la barra lateral en cualquier momento para que se inicie automáticamente al iniciar sesión, o se ejecute como un servicio en segundo plano.', 'setup.skip': 'Omitir — crear un grupo sin node local', 'setup.create_group': 'Crear su primer grupo', 'setup.dismiss': 'Omitir configuración', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js index b5560fa..3cc1348 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js @@ -728,6 +728,7 @@ export default { 'setup.node_started': 'Node démarré.', 'setup.node_not_installed': 'meshbay-node n\'est pas installé sur cette machine.', 'setup.node_install_hint': 'Installez-le avec votre gestionnaire de paquets, puis revenez ici.', + 'setup.node_startup_hint': 'Une fois votre node en fonctionnement, consultez « Node » dans la barre latérale à tout moment pour le faire démarrer automatiquement à la connexion, ou fonctionner comme un service en arrière-plan.', 'setup.skip': 'Passer — créer un groupe sans node local', 'setup.create_group': 'Créer votre premier groupe', 'setup.dismiss': 'Passer la configuration', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js index 358e824..83fe150 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js @@ -727,6 +727,7 @@ export default { 'setup.node_started': 'Node avviato.', 'setup.node_not_installed': 'meshbay-node non è installato su questa macchina.', 'setup.node_install_hint': 'Lo installi con il suo gestore di pacchetti, poi torni qui.', + 'setup.node_startup_hint': 'Una volta che il node è in esecuzione, visiti «Node» nella barra laterale in qualsiasi momento per farlo avviare automaticamente all\'accesso, o eseguirlo come servizio in background.', 'setup.skip': 'Salta — crea un gruppo senza node locale', 'setup.create_group': 'Crea il suo primo gruppo', 'setup.dismiss': 'Salta la configurazione', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js index e28635a..75728f8 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js @@ -715,6 +715,7 @@ export default { 'setup.node_started': 'Node が起動しました。', 'setup.node_not_installed': 'この端末に meshbay-node がインストールされていません。', 'setup.node_install_hint': 'パッケージマネージャーでインストールし、こちらにお戻りください。', + 'setup.node_startup_hint': 'ノードが起動したら、いつでもサイドバーの「Node」にアクセスして、サインイン時に自動的に起動するか、バックグラウンドサービスとして実行するように設定できます。', 'setup.skip': 'スキップ — ローカル node なしでグループを作成', 'setup.create_group': '最初のグループを作成', 'setup.dismiss': 'セットアップをスキップ', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js index 4d35bdc..ae1c168 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js @@ -729,6 +729,7 @@ export default { 'setup.node_started': 'Node gestart.', 'setup.node_not_installed': 'meshbay-node is niet geïnstalleerd op deze machine.', 'setup.node_install_hint': 'Installeer het met uw pakketbeheerder en kom hier terug.', + 'setup.node_startup_hint': 'Zodra uw node actief is, bezoek dan op elk moment \'Node\' in de zijbalk om het automatisch te laten starten bij aanmelding, of als achtergrondservice te laten draaien.', 'setup.skip': 'Overslaan — groep aanmaken zonder lokale node', 'setup.create_group': 'Eerste groep aanmaken', 'setup.dismiss': 'Installatie overslaan', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js index 85015b0..b3d1a1d 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js @@ -747,6 +747,7 @@ export default { 'setup.node_started': 'Node uruchomiony.', 'setup.node_not_installed': 'meshbay-node nie jest zainstalowany na tej maszynie.', 'setup.node_install_hint': 'Zainstaluj go za pomocą menedżera pakietów, a potem wróć tutaj.', + 'setup.node_startup_hint': 'Gdy twój node już działa, odwiedź w dowolnym momencie „Node” na pasku bocznym, aby uruchamiał się automatycznie przy logowaniu lub działał jako usługa w tle.', 'setup.skip': 'Pomiń — utwórz grupę bez lokalnego node', 'setup.create_group': 'Utwórz pierwszą grupę', 'setup.dismiss': 'Pomiń konfigurację', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js index 9b9e80c..e0ec3fe 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js @@ -726,6 +726,7 @@ export default { 'setup.node_started': 'Node iniciado.', 'setup.node_not_installed': 'meshbay-node não está instalado nesta máquina.', 'setup.node_install_hint': 'Instale-o com seu gerenciador de pacotes e volte aqui.', + 'setup.node_startup_hint': 'Assim que seu node estiver em execução, acesse "Node" na barra lateral a qualquer momento para fazê-lo iniciar automaticamente ao entrar, ou rodar como um serviço em segundo plano.', 'setup.skip': 'Pular — criar um grupo sem node local', 'setup.create_group': 'Criar seu primeiro grupo', 'setup.dismiss': 'Pular configuração', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js index 99872f2..01b54c3 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js @@ -702,6 +702,7 @@ export default { 'setup.node_started': 'Node 已启动。', 'setup.node_not_installed': '此机器上未安装 meshbay-node。', 'setup.node_install_hint': '请使用包管理器安装,然后回到此处。', + 'setup.node_startup_hint': '节点运行后,随时可在侧边栏访问"Node",将其设置为登录时自动启动,或作为后台服务运行。', 'setup.skip': '跳过 — 不使用本地 node 创建群组', 'setup.create_group': '创建第一个群组', 'setup.dismiss': '跳过设置', 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" -- cgit v1.2.3