#!/usr/bin/env node import { spawn } from "node:child_process"; import fs from "node:fs"; import { createServer } from "node:http"; import net from "node:net"; import os from "node:os"; import path from "node:path"; import process from "node:process"; import { setTimeout as delay } from "node:timers/promises"; import { fileURLToPath } from "node:url"; import { experimental_createMCPClient } from "ai"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; import { chromium } from "playwright"; const scriptDir = path.dirname(fileURLToPath(import.meta.url)); const desktopDir = path.resolve(scriptDir, ".."); const rootDir = path.resolve(desktopDir, "../.."); const devRunner = path.join(desktopDir, "scripts", "dev-runner.mjs"); const workspaceIds = [ "tab-bridge-original", "tab-bridge-evict-one", "tab-bridge-evict-two", "tab-bridge-evict-three", ]; const timeoutMs = 90_000; function assert(condition, message) { if (!condition) throw new Error(message); } async function reservePort() { return await new Promise((resolve, reject) => { const server = net.createServer(); server.unref(); server.once("error", reject); server.listen(0, "127.0.0.1", () => { const address = server.address(); server.close((error) => { if (error) return reject(error); if (!address || typeof address === "string") { return reject(new Error("Failed to reserve a local port")); } resolve(address.port); }); }); }); } async function waitForPort(port, label, processInfo) { const deadline = Date.now() + timeoutMs; while (Date.now() < deadline) { if ( processInfo && (processInfo.child.exitCode !== null || processInfo.child.signalCode !== null) ) { throw new Error( `${label} process exited before opening its port; see ${processInfo.logPath}`, ); } const connected = await new Promise((resolve) => { const socket = net.createConnection({ host: "127.0.0.1", port }); socket.setTimeout(500); socket.once("connect", () => { socket.destroy(); resolve(true); }); socket.once("timeout", () => { socket.destroy(); resolve(false); }); socket.once("error", () => resolve(false)); }); if (connected) return; await delay(200); } throw new Error(`Timed out waiting for ${label} on port ${port}`); } function writeJson(filePath, value) { fs.mkdirSync(path.dirname(filePath), { recursive: true }); fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`); } function seedPaseoHome(paseoHome, listen, workspaceRoot) { const timestamp = "2026-01-01T00:00:00.000Z"; const projects = workspaceIds.map((workspaceId, index) => { const cwd = path.join(workspaceRoot, `workspace-${index + 1}`); fs.mkdirSync(cwd, { recursive: true }); return { projectId: `project-${workspaceId}`, rootPath: cwd, kind: "non_git", displayName: `Tab bridge project ${index + 1}`, customName: null, createdAt: timestamp, updatedAt: timestamp, archivedAt: null, }; }); const workspaces = workspaceIds.map((workspaceId, index) => ({ workspaceId, projectId: projects[index].projectId, cwd: projects[index].rootPath, kind: "directory", displayName: `Tab bridge workspace ${index + 1}`, title: `Tab bridge workspace ${index + 1}`, branch: null, baseBranch: null, createdAt: timestamp, updatedAt: timestamp, archivedAt: null, pinnedAt: null, })); writeJson(path.join(paseoHome, "config.json"), { version: 1, daemon: { listen, relay: { enabled: false }, mcp: { enabled: true, injectIntoAgents: false }, browserTools: { enabled: true }, cors: { allowedOrigins: ["*"] }, }, }); writeJson(path.join(paseoHome, "projects", "projects.json"), projects); writeJson(path.join(paseoHome, "projects", "workspaces.json"), workspaces); } function spawnLogged(name, command, args, options, logDir) { const logPath = path.join(logDir, `${name}.log`); const log = fs.createWriteStream(logPath, { flags: "a" }); const child = spawn(command, args, { ...options, detached: process.platform !== "win32", stdio: ["ignore", "pipe", "pipe"], }); let openStreams = 2; const closeLogStream = () => { openStreams -= 1; if (openStreams === 0) log.end(); }; child.stdout.pipe(log, { end: false }); child.stderr.pipe(log, { end: false }); child.stdout.once("end", closeLogStream); child.stderr.once("end", closeLogStream); return { child, logPath }; } function stopProcess(child) { if (!child?.pid || child.exitCode !== null) return; try { if (process.platform === "win32") child.kill("SIGTERM"); else process.kill(-child.pid, "SIGTERM"); } catch { // The process may exit between the liveness check and signal delivery. } } async function waitForAppPage(browser, expoPort) { const deadline = Date.now() + timeoutMs; while (Date.now() < deadline) { for (const context of browser.contexts()) { for (const page of context.pages()) { if (page.url().includes(`localhost:${expoPort}`)) return page; } } await delay(250); } throw new Error("Timed out waiting for the real Electron app renderer"); } async function waitForDesktopStatus(page) { const deadline = Date.now() + timeoutMs; let lastError; while (Date.now() < deadline) { try { const status = await page.evaluate(async () => { if (typeof window.paseoDesktop?.invoke !== "function") return null; return await window.paseoDesktop.invoke("desktop_daemon_status"); }); if (typeof status?.serverId === "string") return status; } catch (error) { // Metro may replace the renderer execution context during its initial load. lastError = error; } await delay(250); } throw new Error( `Timed out waiting for the Electron desktop bridge${lastError ? `: ${String(lastError)}` : ""}`, ); } async function startTargetPage() { const server = createServer((_request, response) => { response.writeHead(200, { "content-type": "text/html; charset=utf-8" }); response.end(` Tab bridge target `); }); await new Promise((resolve, reject) => { server.once("error", reject); server.listen(0, "127.0.0.1", resolve); }); const address = server.address(); assert(address && typeof address !== "string", "Target page did not bind a TCP port"); return { server, url: `http://127.0.0.1:${address.port}/` }; } async function closeServer(server) { await new Promise((resolve) => server.close(resolve)); } function mcpPayload(result, command) { const payload = result.structuredContent; assert( payload && typeof payload === "object", `${command} returned no structured payload: ${JSON.stringify(result)}`, ); assert(payload.ok === true, `${command} failed: ${JSON.stringify(payload)}`); return payload.result; } async function callBrowserTool(client, name, args = {}) { return mcpPayload(await client.callTool({ name, args }), name); } async function createCallerAgent(daemonPort) { const transport = new StreamableHTTPClientTransport( new URL(`http://127.0.0.1:${daemonPort}/mcp/agents`), ); const client = await experimental_createMCPClient({ transport }); try { const response = await client.callTool({ name: "create_agent", args: { relationship: { kind: "detached" }, workspace: { kind: "existing", workspaceId: workspaceIds[0] }, title: "Browser tab bridge E2E caller", provider: "mock/ten-second-stream", settings: { modeId: "load-test" }, initialPrompt: "Remain available while the browser bridge regression runs.", background: true, }, }); const result = response.structuredContent; assert( result && typeof result === "object", `create_agent returned no structured payload: ${JSON.stringify(response)}`, ); assert(typeof result.agentId === "string", "create_agent returned no caller agent id"); assert( result.workspaceId === workspaceIds[0], `MCP caller attached to unexpected workspace ${result.workspaceId}`, ); return result.agentId; } finally { await client.close(); } } async function readGuest(page, browserId) { return await page.evaluate((id) => { const webview = document.querySelector(`[data-paseo-browser-id="${id}"]`); if (!(webview instanceof HTMLElement) || typeof webview.getWebContentsId !== "function") { return null; } return { webContentsId: webview.getWebContentsId(), parentId: webview.parentElement?.id ?? null, }; }, browserId); } async function runRegression({ page, client, serverId, targetUrl }) { const originalWorkspaceId = workspaceIds[0]; const originalWorkspaceRow = page.getByTestId( `sidebar-workspace-row-${serverId}:${originalWorkspaceId}`, ); await originalWorkspaceRow.waitFor({ state: "visible", timeout: timeoutMs }); await originalWorkspaceRow.click(); const created = await callBrowserTool(client, "browser_new_tab", { url: targetUrl }); const browserId = created.browserId; assert(typeof browserId === "string", "browser_new_tab returned no browserId"); const originalDeck = page.getByTestId(`workspace-deck-entry-${serverId}:${originalWorkspaceId}`); await originalDeck.getByTestId(`workspace-tab-browser_${browserId}`).click(); await page.waitForFunction( (id) => { const webview = document.querySelector(`[data-paseo-browser-id="${id}"]`); return webview && webview.parentElement?.id !== "paseo-browser-resident-webviews"; }, browserId, { timeout: timeoutMs }, ); const firstGuest = await readGuest(page, browserId); assert(firstGuest, "Original browser guest was not attached to its workspace pane"); for (const workspaceId of workspaceIds.slice(1)) { await page.getByTestId(`sidebar-workspace-row-${serverId}:${workspaceId}`).click(); await page .getByTestId(`workspace-deck-entry-${serverId}:${workspaceId}`) .waitFor({ state: "visible" }); } await page.waitForFunction( ({ id, previousWebContentsId }) => { const webview = document.querySelector(`[data-paseo-browser-id="${id}"]`); return ( webview?.parentElement?.id === "paseo-browser-resident-webviews" && typeof webview.getWebContentsId === "function" && webview.getWebContentsId() !== previousWebContentsId ); }, { id: browserId, previousWebContentsId: firstGuest.webContentsId }, { timeout: timeoutMs }, ); const replacementGuest = await readGuest(page, browserId); assert(replacementGuest, "Replacement browser guest was not parked after workspace eviction"); const listed = await callBrowserTool(client, "browser_list_tabs"); assert( listed.tabs.some((tab) => tab.browserId === browserId), "browser_list_tabs lost the original tab after guest replacement", ); const snapshot = await callBrowserTool(client, "browser_snapshot", { browserId }); const ref = snapshot.snapshot.match(/button "Bridge target" \[ref=(@e\d+)\]/)?.[1]; assert(ref, `browser_snapshot did not expose the target button: ${snapshot.snapshot}`); const clicked = await callBrowserTool(client, "browser_click", { browserId, ref }); assert( clicked.browserId === browserId && clicked.ref === ref, "browser_click targeted another tab", ); await callBrowserTool(client, "browser_wait", { browserId, text: "Clicked", timeoutMs: 5_000, }); return { browserId, originalWebContentsId: firstGuest.webContentsId, replacementWebContentsId: replacementGuest.webContentsId, list: "passed", snapshot: "passed", click: "passed", }; } async function main() { const artifactDir = process.env.PASEO_TAB_BRIDGE_E2E_ARTIFACT_DIR ?? fs.mkdtempSync(path.join(os.tmpdir(), "paseo-tab-bridge-e2e-artifacts-")); const runtimeDir = fs.mkdtempSync(path.join(os.tmpdir(), "paseo-tab-bridge-e2e-")); fs.mkdirSync(artifactDir, { recursive: true }); const paseoHome = path.join(runtimeDir, "paseo-home"); const userData = path.join(runtimeDir, "electron-user-data"); const workspaceRoot = path.join(runtimeDir, "workspaces"); fs.mkdirSync(paseoHome, { recursive: true }); const [daemonPort, expoPort, cdpPort] = await Promise.all([ reservePort(), reservePort(), reservePort(), ]); const listen = `127.0.0.1:${daemonPort}`; seedPaseoHome(paseoHome, listen, workspaceRoot); const target = await startTargetPage(); const children = []; let browser = null; let client = null; try { const commonEnv = { ...process.env, PASEO_HOME: paseoHome, PASEO_LISTEN: listen, PASEO_DAEMON_ENDPOINT: `localhost:${daemonPort}`, PASEO_CORS_ORIGINS: "*", PASEO_LOCAL_SPEECH_AUTO_DOWNLOAD: "0", PASEO_DICTATION_ENABLED: "0", PASEO_VOICE_MODE_ENABLED: "0", FORCE_COLOR: "0", NO_COLOR: "1", }; const daemon = spawnLogged( "daemon", process.execPath, ["--import", "tsx", path.join(rootDir, "packages/server/scripts/dev-runner.ts")], { cwd: rootDir, env: { ...commonEnv, PASEO_NODE_ENV: "development" } }, artifactDir, ); children.push(daemon.child); await waitForPort(daemonPort, "daemon", daemon); const desktopArgs = [ process.execPath, devRunner, ...(process.platform === "linux" ? ["--no-sandbox"] : []), ]; const desktopCommand = process.platform === "linux" ? "xvfb-run" : desktopArgs.shift(); const desktopCommandArgs = process.platform === "linux" ? ["-a", "--server-args=-screen 0 1280x800x24", ...desktopArgs] : desktopArgs; const desktop = spawnLogged( "desktop", desktopCommand, desktopCommandArgs, { cwd: rootDir, env: { ...commonEnv, EXPO_PORT: String(expoPort), EXPO_DEV_URL: `http://localhost:${expoPort}`, PASEO_ELECTRON_REMOTE_DEBUGGING_PORT: String(cdpPort), PASEO_ELECTRON_USER_DATA_DIR: userData, PASEO_ELECTRON_FLAGS: `--remote-debugging-address=127.0.0.1 --remote-debugging-port=${cdpPort}`, }, }, artifactDir, ); children.push(desktop.child); await waitForPort(cdpPort, "Electron CDP", desktop); browser = await chromium.connectOverCDP(`http://127.0.0.1:${cdpPort}`); const page = await waitForAppPage(browser, expoPort); const status = await waitForDesktopStatus(page); const callerAgentId = await createCallerAgent(daemonPort); const transport = new StreamableHTTPClientTransport( new URL( `http://127.0.0.1:${daemonPort}/mcp/agents?callerAgentId=${encodeURIComponent(callerAgentId)}`, ), ); client = await experimental_createMCPClient({ transport }); const report = await runRegression({ page, client, serverId: status.serverId, targetUrl: target.url, }); writeJson(path.join(artifactDir, "result.json"), report); console.log( `Browser tab bridge E2E passed: WebContents ${report.originalWebContentsId} -> ${report.replacementWebContentsId}; list, snapshot, click passed.`, ); } catch (error) { console.error(`Browser tab bridge E2E failed. Artifacts: ${artifactDir}`); console.error(error); throw error; } finally { await client?.close().catch(() => undefined); await browser?.close().catch(() => undefined); for (const child of children.toReversed()) stopProcess(child); await closeServer(target.server); await delay(1_000); try { fs.rmSync(runtimeDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); } catch (error) { console.warn(`Failed to remove isolated E2E state ${runtimeDir}`, error); } } } await main();