// Packaged desktop, on camera: the REAL electron-builder bundle (app.isPackaged // === true) attaches to an already-running OS-supervised daemon instead of // spawning its own sidecar. This is the production-only path — dev electron skips // ensureSupervisedConnection entirely and always spawns a desktop-sidecar, so the // attach behavior can ONLY be proven against the packaged artifact. // // We start the daemon as the bundle's OWN compiled `executor` binary (the exact // binary a supervised install runs) in EXECUTOR_SUPERVISED mode. It publishes a // manifest of kind "cli-daemon". Then we launch the packaged app // pointed at the same HOME and prove it attached: the manifest still names the // daemon's pid (a spawned sidecar would rewrite it to "desktop-sidecar" with a // fresh pid), and the console — served by the bearer-gated daemon — renders, // which only happens if the app injected the bearer it read from the manifest. // The recording (session.mp4 + screenshots) is the artifact; the waits assert. import { type ChildProcess, execFileSync, spawn } from "node:child_process"; import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import net from "node:net"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { expect, it } from "@effect/vitest"; import { Effect } from "effect"; import { scenario } from "../src/scenario"; import { RunDir } from "../src/services"; import { waitForHttp } from "../setup/boot"; // Driving the packaged Electron app needs a real window-server session: Aqua on // macOS, an X/Wayland display on Linux. An SSH/CI shell runs in the background // (non-GUI) session where Electron can't open a window — so this scenario runs // only where a display is reachable (a logged-in console, or a guest under // autologin/Xvfb) and skips honestly elsewhere rather than hanging on launch. const guiAvailable = (): boolean => { if (process.platform === "darwin") { // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: probing the session manager; absence = no GUI try { return execFileSync("launchctl", ["managername"], { encoding: "utf8" }).trim() === "Aqua"; } catch { return false; } } if (process.platform === "linux") { return Boolean(process.env.DISPLAY || process.env.WAYLAND_DISPLAY); } return true; // windows: the runner places this in an interactive session }; const SCENARIO_NAME = "Desktop (packaged) · the real bundle attaches to the OS-supervised daemon"; const appExe = process.env.E2E_DESKTOP_APP_EXE; const executorBin = process.env.E2E_DESKTOP_EXECUTOR_BIN; interface PackagedApp { readonly child: ChildProcess; readonly debugPort: string; cdp: CdpPage; } interface CdpResponse { readonly id: number; readonly result?: T; readonly error?: { readonly message?: string }; } interface CdpEvaluateResult { readonly result: { readonly value?: unknown }; readonly exceptionDetails?: unknown; } interface CdpTarget { readonly type: string; readonly url: string; readonly webSocketDebuggerUrl?: string; } class CdpPage { private nextId = 1; private readonly pending = new Map< number, { readonly resolve: (value: unknown) => void; readonly reject: (error: Error) => void; } >(); private constructor(private readonly socket: WebSocket) { socket.addEventListener("message", (event) => { const data = event.data; if (typeof data !== "string") return; const message = JSON.parse(data) as CdpResponse; if (!message.id) return; const pending = this.pending.get(message.id); if (!pending) return; this.pending.delete(message.id); if (message.error) { pending.reject(new Error(message.error.message ?? "CDP command failed")); return; } pending.resolve(message.result); }); socket.addEventListener("close", () => { for (const [, pending] of this.pending) { pending.reject(new Error("CDP socket closed")); } this.pending.clear(); }); } static connect = (url: string): Promise => new Promise((resolve, reject) => { const socket = new WebSocket(url); const timer = setTimeout(() => { socket.close(); // oxlint-disable-next-line executor/no-promise-reject, executor/no-error-constructor -- boundary: WebSocket connection promise adapter reject(new Error(`Timed out connecting to page CDP target ${url}`)); }, 30_000); socket.addEventListener( "open", () => { clearTimeout(timer); resolve(new CdpPage(socket)); }, { once: true }, ); socket.addEventListener( "error", () => { clearTimeout(timer); // oxlint-disable-next-line executor/no-promise-reject, executor/no-error-constructor -- boundary: WebSocket connection promise adapter reject(new Error(`Failed to connect to page CDP target ${url}`)); }, { once: true }, ); }); command = async (method: string, params: Record = {}): Promise => { const id = this.nextId; this.nextId += 1; const result = new Promise((resolve, reject) => { this.pending.set(id, { resolve: (value) => resolve(value as T), reject, }); }); this.socket.send(JSON.stringify({ id, method, params })); return result; }; evaluate = async (expression: string): Promise => { const result = await this.command("Runtime.evaluate", { expression, awaitPromise: true, returnByValue: true, }); if (result.exceptionDetails) { throw new Error(`CDP evaluation failed: ${JSON.stringify(result.exceptionDetails)}`); } return result.result.value as T; }; waitForText = async (text: string, timeoutMs: number): Promise => { const deadline = Date.now() + timeoutMs; const expression = `document.body?.innerText.includes(${JSON.stringify(text)}) ?? false`; for (;;) { if (await this.evaluate(expression).catch(() => false)) return; if (Date.now() >= deadline) throw new Error(`Timed out waiting for text: ${text}`); await new Promise((resolve) => setTimeout(resolve, 250)); } }; screenshot = async (path: string): Promise => { const result = await this.command<{ readonly data: string }>("Page.captureScreenshot", { format: "png", fromSurface: true, }); writeFileSync(path, Buffer.from(result.data, "base64")); }; close = (): void => { this.socket.close(); }; } const freePort = (): Promise => new Promise((resolve, reject) => { const srv = net.createServer(); srv.on("error", reject); srv.listen(0, "127.0.0.1", () => { const port = (srv.address() as net.AddressInfo).port; srv.close(() => resolve(port)); }); }); interface Manifest { readonly kind: string; readonly pid: number; } interface DaemonStart { readonly child: ChildProcess; readonly ready: boolean; readonly stderr: string; } /** Spawn the bundle's compiled executor as a supervised daemon; resolves once it * announces readiness (or times out / exits early, ready:false). */ const startSupervisedDaemon = (env: NodeJS.ProcessEnv, port: number): Promise => new Promise((resolve) => { const child = spawn( executorBin as string, ["daemon", "run", "--foreground", "--port", String(port), "--hostname", "127.0.0.1"], { env, stdio: ["ignore", "pipe", "pipe"] }, ); let stderr = ""; const settle = (ready: boolean) => resolve({ child, ready, stderr }); const timer = setTimeout(() => settle(false), 60_000); child.stdout.on("data", (chunk: Buffer) => { if (/Daemon ready on http:\/\//.test(chunk.toString())) { clearTimeout(timer); settle(true); } }); child.stderr.on("data", (chunk: Buffer) => { stderr += chunk.toString(); }); child.on("exit", () => { clearTimeout(timer); settle(false); }); }); const packagedSingleInstanceAvailable = (): boolean => { if (process.platform !== "darwin" || !appExe) return true; try { const lines = execFileSync("pgrep", ["-fl", "Executor.app/Contents/MacOS/Executor"], { encoding: "utf8", }) .split("\n") .filter(Boolean); return !lines.some((line) => !line.includes(appExe)); } catch { return true; } }; const waitForPageWebSocket = async (debugPort: string): Promise => { const deadline = Date.now() + 120_000; for (;;) { const targets = (await fetch(`http://127.0.0.1:${debugPort}/json/list`) .then((response) => (response.ok ? response.json() : [])) .catch(() => [])) as ReadonlyArray; const page = targets.find( (target) => target.type === "page" && target.webSocketDebuggerUrl && !target.url.startsWith("devtools://"), ); if (page?.webSocketDebuggerUrl) return page.webSocketDebuggerUrl; if (Date.now() >= deadline) { throw new Error("Timed out waiting for packaged app page CDP target"); } await new Promise((resolve) => setTimeout(resolve, 100)); } }; const launchPackaged = async (home: string): Promise => { let output = ""; let settled = false; const child = spawn(appExe as string, ["--remote-debugging-port=0"], { env: { ...process.env, HOME: home }, stdio: ["ignore", "pipe", "pipe"], }); const browserCdpUrl = await new Promise((resolve, reject) => { const timer = setTimeout(() => { // oxlint-disable-next-line executor/no-promise-reject, executor/no-error-constructor -- boundary: packaged-app launch promise adapter reject(new Error(`Timed out waiting for packaged app CDP URL\n${output}`)); }, 120_000); const settle = (fn: () => void) => { if (settled) return; settled = true; clearTimeout(timer); fn(); }; const collectOutput = (chunk: Buffer) => { const text = chunk.toString(); output = (output + text).slice(-16_384); const match = output.match(/DevTools listening on (ws:\/\/[^\s]+)/); if (match) settle(() => resolve(match[1]!)); }; child.stdout?.on("data", collectOutput); child.stderr?.on("data", collectOutput); // oxlint-disable-next-line executor/no-promise-reject -- boundary: packaged-app launch promise adapter child.once("error", (error) => settle(() => reject(error))); child.once("exit", (code, signal) => settle(() => // oxlint-disable-next-line executor/no-promise-reject, executor/no-error-constructor -- boundary: packaged-app launch promise adapter reject( new Error(`Packaged app exited before CDP (code=${code} signal=${signal})\n${output}`), ), ), ); }); const debugPort = new URL(browserCdpUrl).port; const pageCdpUrl = await waitForPageWebSocket(debugPort); const cdp = await CdpPage.connect(pageCdpUrl); await cdp.command("Runtime.enable"); await cdp.command("Page.enable"); return { child, cdp, debugPort }; }; const stopProcess = async (child: ChildProcess | undefined): Promise => { if (!child || child.exitCode !== null || child.signalCode !== null) return; await new Promise((resolve) => { const timeout = setTimeout(() => { child.kill("SIGKILL"); resolve(); }, 5_000); child.once("exit", () => { clearTimeout(timeout); resolve(); }); child.kill("SIGTERM"); }); }; const closePackaged = async (app: PackagedApp | undefined): Promise => { app?.cdp.close(); await stopProcess(app?.child); }; if (!guiAvailable() || !packagedSingleInstanceAvailable()) { it.skip(`${SCENARIO_NAME} (needs a GUI display and no already-running Executor.app)`, () => {}); } else { scenario( SCENARIO_NAME, { timeout: 240_000 }, Effect.gen(function* () { if (!appExe || !executorBin) { return yield* Effect.die( "E2E_DESKTOP_APP_EXE / E2E_DESKTOP_EXECUTOR_BIN not set — did desktop-packaged.globalsetup run?", ); } const runDir = yield* RunDir; yield* Effect.promise(() => run(runDir)); }), ); } const run = async (runDir: string) => { const home = mkdtempSync(join(tmpdir(), "executor-pkg-attach-")); const dataDir = join(home, ".executor"); const manifestPath = join(dataDir, "server-control", "server.json"); const port = await freePort(); let daemon: ChildProcess | undefined; let app: PackagedApp | undefined; let stepIndex = 0; try { const started = await startSupervisedDaemon( { ...process.env, HOME: home, EXECUTOR_SUPERVISED: "1", EXECUTOR_DATA_DIR: dataDir, EXECUTOR_AUTH_TOKEN: "packaged-attach-film", EXECUTOR_CLIENT: "desktop", }, port, ); daemon = started.child; expect(started.ready, `supervised daemon became ready; stderr:\n${started.stderr}`).toBe(true); await waitForHttp(`http://127.0.0.1:${port}/`, { timeoutMs: 30_000 }); const daemonManifest = JSON.parse(readFileSync(manifestPath, "utf8")) as Manifest; expect(daemonManifest.kind, "the bundled executor advertises itself as cli-daemon").toBe( "cli-daemon", ); const daemonPid = daemonManifest.pid; // Launch the PACKAGED bundle directly. `app.isPackaged` is true, so boot() // runs the supervised attach path; CDP drives the real renderer. app = await launchPackaged(home); const page = app.cdp; const step = async (label: string, body: () => Promise) => { await body(); stepIndex += 1; const slug = label.toLowerCase().replace(/[^a-z0-9]+/g, "-"); await page.screenshot(join(runDir, `${String(stepIndex).padStart(2, "0")}-${slug}.png`)); }; // The console only renders once the app has a live connection AND the bearer // it injects is accepted by the gated daemon — so reaching it proves both the // attach and the bearer wiring through the packaged session layer. await step("packaged app boots into the bearer-gated console", async () => { await page.waitForText("Settings", 120_000); }); // Proof it ATTACHED, not spawned: the manifest is untouched — same pid, still // cli-daemon. A managed sidecar would have rewritten it to "desktop-sidecar". await step("server manifest still names the supervised daemon", async () => { const after = JSON.parse(readFileSync(manifestPath, "utf8")) as Manifest; expect(after.kind, "still the supervised daemon (not a desktop sidecar)").toBe("cli-daemon"); expect(after.pid, "the packaged app attached to our daemon, not a new sidecar").toBe( daemonPid, ); }); } finally { await closePackaged(app); await stopProcess(daemon); rmSync(home, { recursive: true, force: true }); } };