From bc6788df981305c318e1972a1d39987fc9f66c69 Mon Sep 17 00:00:00 2001 From: johnny4young Date: Tue, 13 Oct 2026 20:35:29 -0500 Subject: [PATCH 1/3] fix(main): forward desktop smoke mode to the sandboxed renderer - The renderer starts the smoke harness only when its preload reports smoke mode, but a packaged sandboxed renderer sees neither the launcher environment nor main's argv, so the release smoke never started and timed out. - Main now passes the smoke flag to the renderer through additionalArguments whenever it is itself in smoke mode; ordinary windows get no extra argument. - Verified with a locally packaged app: the packaged smoke now completes all cases instead of hanging. Changelog: none --- src/main/index.ts | 3 ++- src/main/ipc/desktopSmoke.ts | 10 +++++++++- tests/ipc/desktopSmoke.test.ts | 12 ++++++++++++ 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/main/index.ts b/src/main/index.ts index cdebf9abc..c969d302e 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -27,7 +27,7 @@ import { registerAltJsRuntimeHandlers } from './altJsRuntimes'; import { registerAppInfoHandlers } from './ipc/appInfo'; import { registerFileSystemHandlers } from './ipc/fileSystem'; import { registerLocaleHandlers } from './ipc/locale'; -import { registerDesktopSmokeHandlers } from './ipc/desktopSmoke'; +import { desktopSmokeRendererArguments, registerDesktopSmokeHandlers } from './ipc/desktopSmoke'; import { registerEnvHandlers } from './ipc/env'; import { registerLspHandlers, disposeLspBridge } from './ipc/lsp'; import { registerProfileHandlers } from './ipc/profile'; @@ -171,6 +171,7 @@ const createWindow = async () => { contextIsolation: true, nodeIntegration: false, sandbox: true, + additionalArguments: desktopSmokeRendererArguments(), // Pin the same-origin policy explicitly. This is the // Electron default, but pinning it ensures a future edit cannot // silently disable web security (which would let a compromised diff --git a/src/main/ipc/desktopSmoke.ts b/src/main/ipc/desktopSmoke.ts index c7e5ef59c..00c67f40a 100644 --- a/src/main/ipc/desktopSmoke.ts +++ b/src/main/ipc/desktopSmoke.ts @@ -20,13 +20,21 @@ const SMOKE_LICENSE_TOKEN_ENV = 'LINGUA_DESKTOP_SMOKE_LICENSE_TOKEN'; * preload surface inert for normal production sessions while still letting the * packaged release smoke reuse the real app binary. */ -function isDesktopSmokeEnabled(): boolean { +export function isDesktopSmokeEnabled(): boolean { return ( process.env.LINGUA_DESKTOP_SMOKE === '1' || process.argv.includes(DESKTOP_SMOKE_FLAG) ); } +/** + * Renderer arguments that let the sandboxed preload see smoke mode. A packaged + * renderer does not inherit the launcher's environment or main's argv. + */ +export function desktopSmokeRendererArguments(): string[] { + return isDesktopSmokeEnabled() ? [DESKTOP_SMOKE_FLAG] : []; +} + /** * packaged-subset gate. When the smoke harness is * launched against a release artifact (the `.app` bundle on macOS), diff --git a/tests/ipc/desktopSmoke.test.ts b/tests/ipc/desktopSmoke.test.ts index 7080e350f..e68e06319 100644 --- a/tests/ipc/desktopSmoke.test.ts +++ b/tests/ipc/desktopSmoke.test.ts @@ -283,4 +283,16 @@ describe('desktop smoke IPC handlers', () => { reason: 'smoke-disabled', }); }); + + it('forwards smoke mode to the sandboxed renderer only when main is in smoke mode', async () => { + const { desktopSmokeRendererArguments } = await import('#src/main/ipc/desktopSmoke'); + expect(desktopSmokeRendererArguments()).toEqual(['--lingua-desktop-smoke']); + + delete process.env.LINGUA_DESKTOP_SMOKE; + process.argv = ['electron', 'main.js']; + expect(desktopSmokeRendererArguments()).toEqual([]); + + process.argv = ['electron', 'main.js', '--lingua-desktop-smoke']; + expect(desktopSmokeRendererArguments()).toEqual(['--lingua-desktop-smoke']); + }); }); From cb3171cd710ff844ed2ef2fa656c70f6bd1bf256 Mon Sep 17 00:00:00 2001 From: johnny4young Date: Tue, 13 Oct 2026 21:02:52 -0500 Subject: [PATCH 2/3] fix(main): keep the smoke-mode check private to its module - Only desktopSmokeRendererArguments needs to leave the module; knip flagged the extra export. Changelog: none --- src/main/ipc/desktopSmoke.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/ipc/desktopSmoke.ts b/src/main/ipc/desktopSmoke.ts index 00c67f40a..1cd774ee6 100644 --- a/src/main/ipc/desktopSmoke.ts +++ b/src/main/ipc/desktopSmoke.ts @@ -20,7 +20,7 @@ const SMOKE_LICENSE_TOKEN_ENV = 'LINGUA_DESKTOP_SMOKE_LICENSE_TOKEN'; * preload surface inert for normal production sessions while still letting the * packaged release smoke reuse the real app binary. */ -export function isDesktopSmokeEnabled(): boolean { +function isDesktopSmokeEnabled(): boolean { return ( process.env.LINGUA_DESKTOP_SMOKE === '1' || process.argv.includes(DESKTOP_SMOKE_FLAG) From 1782cf17827065d9ca869470793233f8f22489c6 Mon Sep 17 00:00:00 2001 From: johnny4young Date: Tue, 13 Oct 2026 21:07:20 -0500 Subject: [PATCH 3/3] test(main): lock the smoke flag forwarding contract - Share the smoke flag between main and the preload so the forwarded argument and the preload check cannot drift apart. - Assert that the main window passes the forwarded arguments and that the preload reads the shared flag, so dropping the wiring fails the unit suite instead of the release smoke. Changelog: none --- src/main/ipc/desktopSmoke.ts | 2 +- src/preload/index.ts | 3 ++- src/shared/desktopSmoke.ts | 2 ++ tests/ipc/desktopSmoke.test.ts | 16 ++++++++++++++++ 4 files changed, 21 insertions(+), 2 deletions(-) create mode 100644 src/shared/desktopSmoke.ts diff --git a/src/main/ipc/desktopSmoke.ts b/src/main/ipc/desktopSmoke.ts index 1cd774ee6..4a77e84ed 100644 --- a/src/main/ipc/desktopSmoke.ts +++ b/src/main/ipc/desktopSmoke.ts @@ -6,8 +6,8 @@ import { getBlockedOfflineSmokeUrls, isOfflineSmokeRequested, } from '../offlineSmoke'; +import { DESKTOP_SMOKE_FLAG } from '../../shared/desktopSmoke'; -const DESKTOP_SMOKE_FLAG = '--lingua-desktop-smoke'; const SMOKE_ARTIFACT_DIR_PREFIX = '--lingua-smoke-artifact-dir='; const SMOKE_LICENSE_TOKEN_PREFIX = '--lingua-smoke-license-token='; const SMOKE_LAUNCHED_AT_ENV = 'LINGUA_SMOKE_LAUNCHED_AT_MS'; diff --git a/src/preload/index.ts b/src/preload/index.ts index 41c80097b..d50d26059 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -12,6 +12,7 @@ import type { PythonDebuggerBridge } from '../shared/pythonDebugger'; import type { GoDebuggerBridge } from '../shared/goDebugger'; import type { RustDebuggerBridge } from '../shared/rustDebugger'; import { typedInvoke, typedOn, typedSend } from './ipcTyped'; +import { DESKTOP_SMOKE_FLAG } from '../shared/desktopSmoke'; const lsp: LspBridge = { resolveTarget: (rootId: RootId, uri: string) => typedInvoke('lsp:resolve-target', rootId, uri), @@ -48,7 +49,7 @@ const lsp: LspBridge = { const desktopSmokeEnabled = process.env.LINGUA_DESKTOP_SMOKE === '1' || - process.argv.includes('--lingua-desktop-smoke'); + process.argv.includes(DESKTOP_SMOKE_FLAG); const http: HttpDesktopAPI = { execute: (runId, request, options) => diff --git a/src/shared/desktopSmoke.ts b/src/shared/desktopSmoke.ts new file mode 100644 index 000000000..5bd87b0c0 --- /dev/null +++ b/src/shared/desktopSmoke.ts @@ -0,0 +1,2 @@ +/** Read by main and by the sandboxed preload, which receives it via additionalArguments. */ +export const DESKTOP_SMOKE_FLAG = '--lingua-desktop-smoke'; diff --git a/tests/ipc/desktopSmoke.test.ts b/tests/ipc/desktopSmoke.test.ts index e68e06319..e64ec13eb 100644 --- a/tests/ipc/desktopSmoke.test.ts +++ b/tests/ipc/desktopSmoke.test.ts @@ -1,4 +1,7 @@ +import { readFileSync } from 'node:fs'; +import path from 'node:path'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { DESKTOP_SMOKE_FLAG } from '#src/shared/desktopSmoke'; const handlers = new Map unknown>(); const listeners = new Map unknown>(); @@ -295,4 +298,17 @@ describe('desktop smoke IPC handlers', () => { process.argv = ['electron', 'main.js', '--lingua-desktop-smoke']; expect(desktopSmokeRendererArguments()).toEqual(['--lingua-desktop-smoke']); }); + + it('wires the forwarded flag into the main window and reads the same flag in the preload', () => { + const root = path.resolve(__dirname, '../..'); + const main = readFileSync(path.join(root, 'src/main/index.ts'), 'utf8'); + const webPreferences = main.slice(main.indexOf('webPreferences: {')); + expect(webPreferences.slice(0, webPreferences.indexOf('}'))).toContain( + 'additionalArguments: desktopSmokeRendererArguments()' + ); + + const preload = readFileSync(path.join(root, 'src/preload/index.ts'), 'utf8'); + expect(preload).toContain('process.argv.includes(DESKTOP_SMOKE_FLAG)'); + expect(preload).not.toContain(`'${DESKTOP_SMOKE_FLAG}'`); + }); });