diff --git a/docs/electron-observation.md b/docs/electron-observation.md index 2b8309f0db..0bb70d69d2 100644 --- a/docs/electron-observation.md +++ b/docs/electron-observation.md @@ -233,7 +233,9 @@ initialization, the hook leaves listener callbacks unwrapped and records a callback and receiver; callback execution remains unobserved. This comparison assumes Node's EventEmitter prototype has not itself been globally decorated before hook initialization. The provider loads the hook before the application -and excludes `NODE_OPTIONS` from the child environment. +and excludes `NODE_OPTIONS` from the child environment. It preserves selected +`DISPLAY`, `XAUTHORITY` and `WAYLAND_DISPLAY` values, including the caller-selected +X11 authorization-file path. An opaque later listener or unsupported emitter state can prevent proof of removal order. The hook then delegates removal diff --git a/scripts/verify/electron/tool-contracts.mjs b/scripts/verify/electron/tool-contracts.mjs index 87813076f5..6541a679e9 100644 --- a/scripts/verify/electron/tool-contracts.mjs +++ b/scripts/verify/electron/tool-contracts.mjs @@ -74,6 +74,11 @@ bridge.exposeInMainWorld("fixture", { command: process.execPath, args: [entrypoint, "mcp"], env: { + ...Object.fromEntries( + ["DISPLAY", "XAUTHORITY", "WAYLAND_DISPLAY"].flatMap((name) => + process.env[name] === undefined ? [] : [[name, process.env[name]]], + ), + ), PATH: process.env.PATH, REA_LOG_LEVEL: "silent", HOPPER_LAUNCHER_PATH: "/rea-unconfigured/hopper", @@ -221,15 +226,21 @@ bridge.exposeInMainWorld("fixture", { { name: "capture_electron_scenario", arguments: input }, { timeout: 150_000 }, ); - const capture = active.isError - ? requireMcpToolError(active).details.partial_observation.capture - : active.structuredContent.normalized_result; - if (active.isError) { + const activeError = active.isError + ? requireMcpToolError(active) + : undefined; + if (activeError !== undefined) assert.equal( - requireMcpToolError(active).code, + activeError.code, "cleanup_incomplete", - JSON.stringify(active), + JSON.stringify(activeError), ); + const capture = + activeError === undefined + ? active.structuredContent.normalized_result + : activeError.details?.partial_observation?.capture; + assert.ok(capture, JSON.stringify(activeError ?? active)); + if (activeError !== undefined) { assert.equal(capture.application.cleanup, "unverified"); } else { assert.ok( diff --git a/src/browser/PlaywrightElectronActiveProvider.test.ts b/src/browser/PlaywrightElectronActiveProvider.test.ts index 8fa445f387..4c3e10bec5 100644 --- a/src/browser/PlaywrightElectronActiveProvider.test.ts +++ b/src/browser/PlaywrightElectronActiveProvider.test.ts @@ -28,11 +28,15 @@ afterEach(() => { it("passes the selected environment and ownership token to Electron launch", async () => { vi.stubEnv("LANG", "ambient-language"); + vi.stubEnv("XAUTHORITY", "/ambient/.Xauthority"); let launchedEnvironment: unknown; const provider = new PlaywrightElectronActiveProvider( { LANG: "selected-language", PATH: "/selected/bin", + DISPLAY: ":selected-display", + XAUTHORITY: "/selected/display-authorization", + WAYLAND_DISPLAY: "selected-wayland", REA_PRIVATE_VALUE: "not-a-launch-setting", }, async (options) => { @@ -52,6 +56,9 @@ it("passes the selected environment and ownership token to Electron launch", asy expect(launchedEnvironment).toEqual({ LANG: "selected-language", PATH: "/selected/bin", + DISPLAY: ":selected-display", + XAUTHORITY: "/selected/display-authorization", + WAYLAND_DISPLAY: "selected-wayland", REA_PROCESS_RUN_ID: expect.any(String), }); }); diff --git a/src/browser/PlaywrightElectronActiveProvider.ts b/src/browser/PlaywrightElectronActiveProvider.ts index 2ecc2a9527..8ec944753c 100644 --- a/src/browser/PlaywrightElectronActiveProvider.ts +++ b/src/browser/PlaywrightElectronActiveProvider.ts @@ -384,6 +384,7 @@ export const safeElectronEnvironment = ( "TMPDIR", "USER", "DISPLAY", + "XAUTHORITY", "WAYLAND_DISPLAY", ...(platform === "win32" ? [