From 6353bbddda2e5d08f084efacc18a050ea70011c6 Mon Sep 17 00:00:00 2001 From: Eric Date: Thu, 28 May 2026 09:38:29 -0700 Subject: [PATCH] [eric] electron+e2e: close the init-script race - main reads OPENSWARM_E2E env on startup and appends an --openswarm-e2e chromium switch, preload reads the switch BEFORE any page script parses and contextBridge-exposes __OPENSWARM_E2E__=true so the production store-on-window gate fires deterministically rather than racing addInitScript against bundle parse --- e2e/helpers/launch.ts | 17 +++++++++++------ electron/main.js | 9 +++++++++ electron/preload.js | 11 +++++++++++ 3 files changed, 31 insertions(+), 6 deletions(-) diff --git a/e2e/helpers/launch.ts b/e2e/helpers/launch.ts index 848f15ef..e583bc0a 100644 --- a/e2e/helpers/launch.ts +++ b/e2e/helpers/launch.ts @@ -79,12 +79,17 @@ export function hasAnyProviderKey(): boolean { export async function launchApp(): Promise { seedTestUserIfClean(); - const app = await electron.launch({ executablePath: packagedAppPath(), args: [] }); - // The frontend bundle gates `__OPENSWARM_STORE__` exposure on - // `__OPENSWARM_E2E__` being truthy at module-load time. Adding an init - // script BEFORE the main page navigates guarantees the flag is set before - // bundle.js parses, so specs that read the Redux store directly work - // against the production build. + // OPENSWARM_E2E=1 is read by electron/main.js BEFORE the renderer launches; it + // appends a Chromium switch the preload reads to set window.__OPENSWARM_E2E__ + // before bundle.js parses, so the production store-on-window gate fires + // deterministically (no addInitScript race). + const app = await electron.launch({ + executablePath: packagedAppPath(), + args: [], + env: { ...process.env, OPENSWARM_E2E: '1' }, + }); + // Belt-and-braces: addInitScript ALSO sets the flag in case a future Electron + // changes the cmdline propagation. If either path works, the spec succeeds. try { await app.context().addInitScript({ content: '(window).__OPENSWARM_E2E__ = true;' }); } catch { /* best effort */ } return app; } diff --git a/electron/main.js b/electron/main.js index 3a5abf4f..865090b1 100644 --- a/electron/main.js +++ b/electron/main.js @@ -1,5 +1,14 @@ const { app, components, BrowserWindow, ipcMain, shell, session, dialog, crashReporter } = require('electron'); +// E2E flag: when OPENSWARM_E2E=1, append a Chromium command-line switch the +// renderer reads at startup to set window.__OPENSWARM_E2E__ = true BEFORE any +// page script parses, so the production-build store-on-window gate fires +// deterministically. Normal user launches never set the env var so this is a +// no-op for them; only Playwright's electron.launch({env}) flips it on. +if (process.env.OPENSWARM_E2E === '1') { + try { app.commandLine.appendSwitch('openswarm-e2e', '1'); } catch {} +} + // Local-only crash reporter. Captures native renderer crashes that escape JS-level error handlers and don't otherwise surface in Crashpad. uploadToServer=false keeps minidumps on disk under %APPDATA%/OpenSwarm/Crashpad so we can inspect them post-mortem without sending anywhere. try { crashReporter.start({ diff --git a/electron/preload.js b/electron/preload.js index eb631e06..7aad8265 100644 --- a/electron/preload.js +++ b/electron/preload.js @@ -3,6 +3,17 @@ const { contextBridge, ipcRenderer } = require('electron'); // eslint-disable-next-line no-console console.log('[diag][preload] start, ua=', navigator.userAgent); +// E2E gate: set the renderer flag BEFORE any page script parses so the +// production-build store-on-window expose fires deterministically when +// Playwright launches with OPENSWARM_E2E=1. Read from the Chromium switch +// the main process appended; no-op for normal user launches. +try { + const args = (typeof process !== 'undefined' && process.argv) ? process.argv : []; + if (args.some((a) => /--openswarm-e2e(=1)?$/.test(a))) { + contextBridge.exposeInMainWorld('__OPENSWARM_E2E__', true); + } +} catch (e) { console.log('[diag][preload] e2e-flag setup failed:', e && e.message); } + // Synchronous exposure. The previous async IIFE (await ipcRenderer.invoke) raced React mount: any code reading window.openswarm during the gap (BrowserCard's Electron-detection falling back to iframe mode, AgentChat's auth-token call throwing) saw undefined. sendSync blocks the renderer for one IPC round-trip during preload before any user-visible paint, so window.openswarm is guaranteed to exist before the first frontend bundle evaluates. const port = ipcRenderer.sendSync('get-backend-port-sync'); const webviewPreloadPath = ipcRenderer.sendSync('get-webview-preload-path-sync');