desktop/preload.test.ts
import { test } from "node:test";
import assert from "node:assert/strict";
import { EventEmitter } from "node:events";
import { readFileSync } from "node:fs";
import { runInNewContext } from "node:vm";
import { transformSync } from "esbuild";
test("preload navigation bridge validates directions and removes its listener", () => {
const ipcRenderer = new EventEmitter();
let bridge!: NonNullable<Window["desktop"]>;
const code = transformSync(readFileSync(new URL("./preload.ts", import.meta.url), "utf8"),
{ loader: "ts", format: "cjs" }).code;
runInNewContext(code, { exports: {}, module: { exports: {} }, process: { platform: "linux" },
require: () => ({ ipcRenderer, contextBridge: {
exposeInMainWorld: (_name: string, value: typeof bridge) => { bridge = value; },
} }),
});
const directions: string[] = [];
const unsubscribe = bridge.onNavigation((direction) => directions.push(direction));
for (const direction of ["back", "forward", "browser-backward", "invalid", null])
ipcRenderer.emit("mail-navigation", {}, direction);
assert.deepEqual(directions, ["back", "forward"]);
unsubscribe();
assert.equal(ipcRenderer.listenerCount("mail-navigation"), 0);
ipcRenderer.emit("mail-navigation", {}, "back");
assert.deepEqual(directions, ["back", "forward"]);
});
test("preload update bridge forwards only enum actions and correlates renderer drain without exposing IPC", async () => {
const ipcRenderer = new EventEmitter() as EventEmitter & {
invoke: (channel: string, ...args: unknown[]) => Promise<unknown>;
send: (channel: string, ...args: unknown[]) => void;
};
const calls: unknown[][] = [];
ipcRenderer.invoke = async (...args) => { calls.push(args); return { phase: "idle" }; };
ipcRenderer.send = (...args) => { calls.push(args); };
let bridge!: NonNullable<Window["desktop"]>;
const code = transformSync(readFileSync(new URL("./preload.ts", import.meta.url), "utf8"), { loader: "ts", format: "cjs" }).code;
runInNewContext(code, { exports: {}, module: { exports: {} }, process: { platform: "linux" },
require: () => ({ ipcRenderer, contextBridge: { exposeInMainWorld: (_name: string, value: typeof bridge) => { bridge = value; } } }),
});
const updates = bridge.updates!;
await updates.state(); await updates.command("download");
assert.deepEqual(calls, [["update-state"], ["update-command", "download"]]);
let drains = 0;
const unsubscribe = updates.onDrain(async () => { drains++; });
ipcRenderer.emit("update-drain", {}, "bad request");
const request = "a".repeat(32);
ipcRenderer.emit("update-drain", {}, request);
await Promise.resolve();
assert.equal(drains, 1); assert.deepEqual(calls.at(-1), ["update-drained", request]);
let states = 0;
const unsubscribeState = updates.onState(() => { states++; });
ipcRenderer.emit("update-state", {}, { phase: "ready" });
assert.equal(states, 1);
unsubscribe(); unsubscribeState();
assert.equal(ipcRenderer.listenerCount("update-drain"), 0);
assert.equal(ipcRenderer.listenerCount("update-state"), 0);
});
test("preload endpoint copy forwards no renderer-supplied text or generic clipboard access", async () => {
let bridge!: NonNullable<Window["desktop"]>;
const calls: unknown[][] = [];
const code = transformSync(readFileSync(new URL("./preload.ts", import.meta.url), "utf8"), { loader: "ts", format: "cjs" }).code;
runInNewContext(code, { exports: {}, module: { exports: {} }, process: { platform: "darwin" },
require: () => ({ ipcRenderer: { invoke: async (...args: unknown[]) => { calls.push(args); } },
contextBridge: { exposeInMainWorld: (_name: string, value: typeof bridge) => { bridge = value; } } }),
});
await (bridge.copyMcpEndpoint as (...args: unknown[]) => Promise<void>)("replacement");
assert.deepEqual(calls, [["copy-mcp-endpoint"]]);
assert.equal("clipboard" in bridge, false);
});
test("preload OpenCode copy forwards only the grant credential and no generic clipboard access", async () => {
let bridge!: NonNullable<Window["desktop"]>;
const calls: unknown[][] = [];
const code = transformSync(readFileSync(new URL("./preload.ts", import.meta.url), "utf8"), { loader: "ts", format: "cjs" }).code;
runInNewContext(code, { exports: {}, module: { exports: {} }, process: { platform: "darwin" },
require: () => ({ ipcRenderer: { invoke: async (...args: unknown[]) => { calls.push(args); } },
contextBridge: { exposeInMainWorld: (_name: string, value: typeof bridge) => { bridge = value; } } }),
});
await bridge.copyOpenCodeConfig!("a".repeat(64));
await bridge.setupOpenCode!("b".repeat(64));
assert.deepEqual(calls, [["copy-opencode-config", "a".repeat(64)], ["setup-opencode", "b".repeat(64)]]);
assert.equal("clipboard" in bridge, false);
});
test("isolated startup checks omit the updater bridge and perform no updater IPC", () => {
let bridge!: NonNullable<Window["desktop"]>;
const code = transformSync(readFileSync(new URL("./preload.ts", import.meta.url), "utf8"), { loader: "ts", format: "cjs" }).code;
runInNewContext(code, { exports: {}, module: { exports: {} },
process: { platform: "linux", argv: ["--emc2-disable-updates"] },
require: () => ({ ipcRenderer: { invoke: () => { throw Error("No updater IPC allowed in startup checks"); } },
contextBridge: { exposeInMainWorld: (_name: string, value: typeof bridge) => { bridge = value; } } }),
});
assert.equal(bridge.updates, undefined);
});
test("preload exposes credential fallback mode only for the exact main-process flag", () => {
for (const [argv, expected] of [
[["--emc2-credential-fallback=linux-local"], "linux-local"],
[["--emc2-credential-fallback=other"], undefined],
[[], undefined],
] as const) {
let bridge!: NonNullable<Window["desktop"]>;
const code = transformSync(readFileSync(new URL("./preload.ts", import.meta.url), "utf8"), { loader: "ts", format: "cjs" }).code;
runInNewContext(code, { exports: {}, module: { exports: {} }, process: { platform: "linux", argv },
require: () => ({ ipcRenderer: new EventEmitter(), contextBridge: { exposeInMainWorld: (_name: string, value: typeof bridge) => { bridge = value; } } }),
});
assert.equal(bridge.credentialFallback, expected);
}
});
test("notification preload correlates ready clicks and cleans replacement, stopped and ended custom audio", async () => {
const ipcRenderer = new EventEmitter() as EventEmitter & { invoke: (channel: string, ...args: unknown[]) => Promise<unknown> };
const calls: unknown[][] = [];
let imported = false;
ipcRenderer.invoke = async (...args) => {
calls.push(args);
if (args[0] === "notification-ready") return ["a:queued", null];
if (args[0] === "notification-audio") return { custom: true, ...(imported ? { imported: true } : {}) };
return true;
};
const audio: { src: string; plays: number; pauses: number; onended?: () => void; onerror?: () => void }[] = [];
const timers = new Map<number, () => void>();
let nextTimer = 0;
class Audio {
plays = 0; pauses = 0; onended?: () => void; onerror?: () => void;
constructor(public src: string) { audio.push(this); }
play() { this.plays++; return Promise.resolve(); }
pause() { this.pauses++; }
removeAttribute() { this.src = ""; }
load() {}
}
let bridge!: NonNullable<Window["desktop"]>;
const code = transformSync(readFileSync(new URL("./preload.ts", import.meta.url), "utf8"), { loader: "ts", format: "cjs" }).code;
runInNewContext(code, { exports: {}, module: { exports: {} }, process: { platform: "linux" }, Audio,
setTimeout: (callback: () => void, duration: number) => { assert.equal(duration, 30000); timers.set(++nextTimer, callback); return nextTimer; },
clearTimeout: (id: number) => timers.delete(id),
require: () => ({ ipcRenderer, contextBridge: { exposeInMainWorld: (_name: string, value: typeof bridge) => { bridge = value; } } }),
});
const ids: string[] = [];
const unsubscribe = bridge.notifications!.onOpen((id) => ids.push(id));
await Promise.resolve();
for (const id of ["b:live", null, "", "x".repeat(1025)]) ipcRenderer.emit("notification-open", {}, id);
assert.deepEqual(ids, ["a:queued", "b:live"]);
await bridge.notifications!.configure((await import("../shared/notifications.js")).notificationDefaults, true);
assert.deepEqual(await bridge.notifications!.audio("choose"), { custom: true });
imported = true;
assert.deepEqual(await bridge.notifications!.audio("choose"), { custom: true, imported: true });
bridge.notifications!.preview();
assert.equal(calls.some((args) => args[0] === "notification-audio" && args[1] === "choose"), true);
for (const value of ["file:///private.wav", "https://example.com/sound", "data:text/html;base64,AAAA"]) ipcRenderer.emit("notification-sound", {}, value);
assert.equal(audio.length, 0);
ipcRenderer.emit("notification-sound", {}, "data:audio/wav;base64,UklGRg==");
assert.equal(audio[0].plays, 1);
const endedBeforeReplacement = audio[0].onended!;
ipcRenderer.emit("notification-sound", {}, "data:audio/ogg;base64,T2dnUw==");
assert.equal(audio[0].src, ""); assert.ok(audio[0].pauses); assert.equal(timers.size, 1);
endedBeforeReplacement(); assert.equal(timers.size, 1, "stale audio completion cannot clear the replacement timer");
audio[1].onended!(); assert.equal(audio[1].src, ""); assert.equal(timers.size, 0);
ipcRenderer.emit("notification-sound", {}, "data:audio/mpeg;base64,SUQz");
ipcRenderer.emit("notification-sound", {}, null);
assert.equal(audio[2].src, ""); assert.equal(timers.size, 0);
unsubscribe(); assert.equal(ipcRenderer.listenerCount("notification-open"), 0);
});