Files
reverse-engineering-anything/tests/boundary/browser/electronActiveHook.test.ts
T
bf3475867f fix(electron): preserve native IPC listener behavior (#4389)
Preserve original-function removal, native IPC dispatch and argument identity, with explicit metadata ownership and Evidence verification. Runtime cleanup limitations remain documented.

Fixes #4390.

Co-authored-by: Adolanium <94890352+Adolanium@users.noreply.github.com>
Co-authored-by: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
2026-10-17 04:17:11 +08:00

1797 lines
63 KiB
TypeScript

import { copyFile, mkdir, rm, writeFile } from "node:fs/promises";
import { dirname, join } from "node:path";
import { spawn } from "node:child_process";
import { createRequire } from "node:module";
import { fileURLToPath } from "node:url";
import { afterEach, expect, it } from "vitest";
import { electronActiveTimelineEventSchema } from "../../../src/domain/javascript/electronActiveObservation.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
const temporary: string[] = [];
const electronModuleSource = `const { EventEmitter } = require("node:events");
const app = new EventEmitter();
app.isReady = () => true;
app.relaunch = () => undefined;
app.setAsDefaultProtocolClient = () => true;
app.setLoginItemSettings = () => undefined;
const ipcMain = new EventEmitter();
ipcMain.handlers = new Map();
ipcMain.handle = (channel, listener) => ipcMain.handlers.set(channel, listener);
const ipcRenderer = {
send: () => undefined,
sendSync: () => "ok",
invoke: () => Promise.resolve("ok"),
postMessage: () => undefined,
};
class WebContents extends EventEmitter {
constructor() {
super();
this.id = 12;
}
loadFile() { return Promise.resolve(); }
loadURL() { return Promise.resolve(); }
loadDataURL() { return Promise.resolve(); }
send() { return undefined; }
postMessage() { return undefined; }
setWindowOpenHandler(handler) { this.windowOpenHandler = handler; return undefined; }
}
class BrowserWindow extends EventEmitter {
constructor(options) {
super();
this.id = 3;
this.webContents = new WebContents();
this.options = options;
this.webContents.getLastWebPreferences = () => options.webPreferences;
app.emit("browser-window-created", {}, this);
}
}
const utilityProcess = {
fork: () => {
const child = new EventEmitter();
child.pid = 44;
child.postMessage = () => undefined;
return child;
},
};
const shell = { openExternal: () => Promise.resolve(), openPath: () => Promise.resolve() };
const createSession = () => {
const value = new EventEmitter();
value.setPermissionRequestHandler = () => undefined;
value.setPermissionCheckHandler = () => undefined;
value.setCertificateVerifyProc = () => undefined;
return value;
};
const session = createSession();
const customSession = createSession();
const contextBridge = {
exposeInMainWorld: () => undefined,
exposeInIsolatedWorld: () => undefined,
};
module.exports = {
app,
get BrowserWindow() { return BrowserWindow; },
contextBridge,
ipcMain,
ipcRenderer,
session: {
defaultSession: session,
fromPartition: () => customSession,
},
shell,
utilityProcess,
};
`;
const activeCaptureSource = `(async () => {
const electron = require("electron");
const window = new electron.BrowserWindow({ webPreferences: { preload: "/approved/preload.js" } });
const channel = "x".repeat(2048);
electron.ipcMain.handle(channel, async () => ({ ok: true }));
await electron.ipcMain.handlers.get(channel)({ sender: { id: 9 }, senderFrame: { routingId: 7 } }, ...Array.from({ length: 40 }, () => ({ value: true })));
window.webContents.send("main-to-renderer", { value: true });
window.webContents.emit("render-process-gone", { reason: "crashed" });
const longEventName = "l".repeat(2048);
window.webContents.emit(longEventName);
const utility = electron.utilityProcess.fork("utility.js");
utility.emit("message", { data: { value: true } });
utility.postMessage({ value: true });
electron.session.defaultSession.emit("will-download", window.webContents, { cancel() {} });
electron.session.fromPartition("persist:custom").emit("will-download", window.webContents, { cancel() {} });
const defaultPopupDecision = window.webContents.windowOpenHandler({ url: "https://default-popup.invalid" });
window.webContents.setWindowOpenHandler(() => ({ action: "allow" }));
const popupDecision = window.webContents.windowOpenHandler({ url: "https://popup.invalid" });
electron.shell.openExternal("https://example.invalid").catch(() => undefined);
electron.app.relaunch();
electron.contextBridge.exposeInMainWorld("api", { value: true });
window.webContents.loadURL("https://example.invalid").catch(() => undefined);
for (let index = 0; index < 6000; index++) electron.app.relaunch();
electron.ipcRenderer.send("x".repeat(9 * 1024 * 1024));
electron.app.emit("r".repeat(9 * 1024 * 1024));
const manyArguments = Array(30_000).fill(true);
for (let index = 0; index < 8; index++) electron.ipcRenderer.send("shape-budget", ...manyArguments);
try { process.dlopen({}, "/missing/native.node"); } catch {}
const firstSnapshot = globalThis.__reaElectronActiveSnapshot();
const firstIpc = firstSnapshot.events.find((event) => event.kind === "main-handler-invocation");
if (firstIpc) { firstIpc.channel = "mutated"; firstIpc.argument_shapes.push("mutated"); }
firstSnapshot.events.length = 0;
const snapshot = globalThis.__reaElectronActiveSnapshot();
process.stdout.write(JSON.stringify({ snapshot, default_popup_decision: defaultPopupDecision, popup_decision: popupDecision }) + "\\n");
})();
`;
afterEach(async () => {
await Promise.all(
temporary
.splice(0)
.map(async (path) => rm(path, { force: true, recursive: true })),
);
});
it("retains active hook evidence within its event-retention budget", async () => {
const root = await createTestTempDirectory("rea-electron-hook-");
temporary.push(root);
await writeElectronFixture(root);
const script = join(root, "exercise.cjs");
await writeFile(script, activeCaptureSource);
const execution = await runNode(script, root);
expect(execution.code, execution.stderr).toBe(0);
const result = JSON.parse(
execution.stdout.trim().split("\n").at(-1) ?? "null",
) as {
readonly default_popup_decision: { readonly action: string };
readonly popup_decision: { readonly action: string };
readonly snapshot: {
readonly hook_error: boolean;
readonly events: readonly Record<string, unknown>[];
readonly observed: number;
readonly retained: number;
readonly dropped: number;
readonly dropped_ipc: number;
readonly dropped_runtime: number;
readonly retention_budget_bytes: number;
};
};
const { snapshot } = result;
expect(
electronActiveTimelineEventSchema.array().parse(snapshot.events),
).toHaveLength(snapshot.retained);
expect(snapshot.hook_error).toBe(false);
expect(snapshot.events).toHaveLength(snapshot.retained);
expect(snapshot.observed).toBe(snapshot.retained + snapshot.dropped);
expect(snapshot.observed).toBeGreaterThan(5_000);
expect(snapshot.dropped_ipc).toBeGreaterThanOrEqual(2);
expect(snapshot.dropped_runtime).toBe(1);
expect(snapshot.dropped).toBe(
snapshot.dropped_ipc + snapshot.dropped_runtime,
);
expect(
snapshot.events.some(
(event) =>
event.kind === "ipc-renderer-send" &&
Array.isArray(event.argument_shapes) &&
event.argument_shapes.length === 30_000,
),
).toBe(true);
expect(
Buffer.byteLength(JSON.stringify(snapshot), "utf8"),
).toBeLessThanOrEqual(snapshot.retention_budget_bytes);
const sequences = snapshot.events.map((event) => event.sequence as number);
expect(sequences).toEqual([...sequences].sort((left, right) => left - right));
expect(snapshot.events).toEqual(
expect.arrayContaining([
expect.objectContaining({ kind: "window-lifecycle", event: "created" }),
expect.objectContaining({ kind: "preload", event: "configured" }),
expect.objectContaining({ kind: "shell-attempt", phase: "blocked" }),
expect.objectContaining({ kind: "updater", phase: "blocked" }),
expect.objectContaining({ kind: "native-addon" }),
expect.objectContaining({ kind: "popup-attempt", phase: "blocked" }),
]),
);
expect(result.default_popup_decision).toEqual({ action: "deny" });
expect(result.popup_decision).toEqual({ action: "deny" });
expect(
snapshot.events.filter(
(event) => event.kind === "download" && event.phase === "blocked",
),
).toHaveLength(2);
const ipc = snapshot.events.find(
(event) => event.kind === "main-handler-invocation",
);
expect(ipc).toMatchObject({
correlation_id: expect.stringMatching(/^capture:\d+:\d+$/u),
direction: "renderer-to-main",
sender: "webContents:9",
frame: "frame:7",
});
expect(ipc?.channel).toBe("x".repeat(2_048));
expect(ipc?.argument_shapes).toHaveLength(40);
expect(snapshot.events).toEqual(
expect.arrayContaining([
expect.objectContaining({
kind: "web-contents-lifecycle",
event: "l".repeat(2048),
}),
]),
);
});
it("classifies a dropped browser preload as preload configuration at capture", async () => {
const root = await createTestTempDirectory(
"rea-electron-hook-browser-preload-",
);
temporary.push(root);
await writeElectronFixture(root);
const script = join(root, "browser-preload.cjs");
await writeFile(
script,
`Object.defineProperty(process, "type", { value: "browser" });
const electron = require("electron");
new electron.BrowserWindow({ webPreferences: { preload: "p".repeat(6 * 1024 * 1024) } });
const snapshot = globalThis.__reaElectronActiveSnapshot();
process.stdout.write(JSON.stringify(snapshot) + "\\n");
`,
);
const execution = await runNode(script, root);
expect(execution.code, execution.stderr).toBe(0);
const snapshot = JSON.parse(
execution.stdout.trim().split("\n").at(-1) ?? "null",
) as {
readonly dropped_event_families: readonly {
readonly family: string;
readonly count: number;
}[];
readonly dropped_event_roles: readonly {
readonly role: string;
readonly count: number;
}[];
};
expect(snapshot.dropped_event_families).toContainEqual({
family: "preload-configuration",
count: 1,
});
expect(snapshot.dropped_event_roles).toContainEqual({
role: "browser",
count: 1,
});
});
it("removes observed ipcMain listeners through their original function", async () => {
const root = await createTestTempDirectory("rea-electron-hook-ipc-removal-");
temporary.push(root);
await writeElectronFixture(root);
const script = join(root, "ipc-removal.cjs");
await writeFile(
script,
`const { ipcMain } = require("electron");
const event = { sender: { id: 9 }, senderFrame: { routingId: 7 } };
let calls = 0;
const listener = () => { calls += 1; };
ipcMain.on("repeated", listener);
ipcMain.on("repeated", listener);
ipcMain.removeListener("repeated", listener);
const repeatedAfterFirstRemoval = ipcMain.listenerCount("repeated");
ipcMain.off("repeated", listener);
ipcMain.once("pending-once", listener);
ipcMain.removeListener("pending-once", listener);
ipcMain.on("fired-once", listener);
ipcMain.once("fired-once", listener);
ipcMain.emit("fired-once", event);
ipcMain.removeListener("fired-once", listener);
ipcMain.emit("repeated", event);
ipcMain.emit("pending-once", event);
ipcMain.emit("fired-once", event);
const snapshot = globalThis.__reaElectronActiveSnapshot();
process.stdout.write(JSON.stringify({
calls,
repeated_after_first_removal: repeatedAfterFirstRemoval,
listeners: ["repeated", "pending-once", "fired-once"].map((channel) => ipcMain.listenerCount(channel)),
invocations: snapshot.events.filter((item) => item.kind === "main-event-invocation").map((item) => item.channel),
}) + "\\n");
`,
);
const execution = await runNode(script, root);
expect(execution.code, execution.stderr).toBe(0);
expect(JSON.parse(execution.stdout)).toEqual({
calls: 2,
repeated_after_first_removal: 1,
listeners: [0, 0, 0],
invocations: ["fired-once", "fired-once"],
});
});
it("removes the newest matching ipcMain registration across unpatched aliases", async () => {
const root = await createTestTempDirectory("rea-electron-hook-ipc-aliases-");
temporary.push(root);
await writeElectronFixture(root);
const script = join(root, "ipc-aliases.cjs");
await writeFile(
script,
`const { ipcMain } = require("electron");
const event = { sender: { id: 9 }, senderFrame: { routingId: 7 } };
const order = [];
const f = () => { order.push("f"); };
const g = () => { order.push("g"); };
ipcMain.on("mixed", f);
ipcMain.on("mixed", g);
ipcMain.addListener("mixed", f);
ipcMain.removeListener("mixed", f);
ipcMain.emit("mixed", event);
let onceCalls = 0;
const fn = () => { onceCalls += 1; };
ipcMain.once("once-then-add", fn);
ipcMain.addListener("once-then-add", fn);
ipcMain.removeListener("once-then-add", fn);
ipcMain.emit("once-then-add", event);
ipcMain.emit("once-then-add", event);
const snapshot = globalThis.__reaElectronActiveSnapshot();
process.stdout.write(JSON.stringify({
order,
once_calls: onceCalls,
listeners: ["mixed", "once-then-add"].map((channel) => ipcMain.listenerCount(channel)),
invocations: snapshot.events.filter((item) => item.kind === "main-event-invocation").map((item) => item.channel),
}) + "\\n");
`,
);
const execution = await runNode(script, root);
expect(execution.code, execution.stderr).toBe(0);
expect(JSON.parse(execution.stdout)).toEqual({
order: ["f", "g"],
once_calls: 1,
listeners: [2, 0],
invocations: ["mixed", "mixed", "once-then-add"],
});
});
it("records ipcMain listeners registered by a newListener callback during once", async () => {
const root = await createTestTempDirectory("rea-electron-hook-ipc-nested-");
temporary.push(root);
await writeElectronFixture(root);
const script = join(root, "ipc-nested.cjs");
await writeFile(
script,
`const { ipcMain } = require("electron");
const event = { sender: { id: 9 }, senderFrame: { routingId: 7 } };
let nestedCalls = 0;
ipcMain.addListener("newListener", (channel) => {
if (channel === "outer") ipcMain.on("nested", () => { nestedCalls += 1; });
});
ipcMain.once("outer", () => undefined);
ipcMain.emit("nested", event);
ipcMain.emit("outer", event);
const snapshot = globalThis.__reaElectronActiveSnapshot();
process.stdout.write(JSON.stringify({
nested_calls: nestedCalls,
invocations: snapshot.events.filter((item) => item.kind === "main-event-invocation").map((item) => item.channel),
}) + "\\n");
`,
);
const execution = await runNode(script, root);
expect(execution.code, execution.stderr).toBe(0);
expect(JSON.parse(execution.stdout)).toEqual({
nested_calls: 1,
invocations: ["nested", "outer"],
});
});
it("keeps removal working after many fired ipcMain once registrations", async () => {
const root = await createTestTempDirectory("rea-electron-hook-ipc-repeat-");
temporary.push(root);
await writeElectronFixture(root);
const script = join(root, "ipc-repeat.cjs");
await writeFile(
script,
`const { ipcMain } = require("electron");
const event = { sender: { id: 9 }, senderFrame: { routingId: 7 } };
let calls = 0;
const listener = () => { calls += 1; };
for (let index = 0; index < 200; index += 1) {
ipcMain.once("repeated-once", listener);
ipcMain.emit("repeated-once", event);
}
const afterOnce = ipcMain.rawListeners("repeated-once").length;
ipcMain.on("repeated-once", listener);
ipcMain.removeListener("repeated-once", listener);
ipcMain.emit("repeated-once", event);
process.stdout.write(JSON.stringify({
calls,
after_once: afterOnce,
listeners: ipcMain.listenerCount("repeated-once"),
}) + "\\n");
`,
);
const execution = await runNode(script, root);
expect(execution.code, execution.stderr).toBe(0);
expect(JSON.parse(execution.stdout)).toEqual({
calls: 200,
after_once: 0,
listeners: 0,
});
});
it("removes the newest registration of an ipcMain listener that has its own listener property", async () => {
const root = await createTestTempDirectory("rea-electron-hook-ipc-wrapped-");
temporary.push(root);
await writeElectronFixture(root);
const script = join(root, "ipc-wrapped.cjs");
await writeFile(
script,
`const { ipcMain } = require("electron");
const event = { sender: { id: 9 }, senderFrame: { routingId: 7 } };
const order = [];
const f = () => { order.push("f"); };
f.listener = () => { order.push("inner"); };
const g = () => { order.push("g"); };
ipcMain.on("wrapped", f);
ipcMain.on("wrapped", g);
ipcMain.addListener("wrapped", f);
ipcMain.removeListener("wrapped", f);
ipcMain.emit("wrapped", event);
process.stdout.write(JSON.stringify({
order,
listeners: ipcMain.listenerCount("wrapped"),
}) + "\\n");
`,
);
const execution = await runNode(script, root);
expect(execution.code, execution.stderr).toBe(0);
expect(JSON.parse(execution.stdout)).toEqual({
order: ["f", "g"],
listeners: 2,
});
});
it("reports original ipcMain listeners to newListener and removeListener observers", async () => {
const root = await createTestTempDirectory("rea-electron-hook-ipc-meta-");
temporary.push(root);
await writeElectronFixture(root);
const script = join(root, "ipc-meta.cjs");
await writeFile(
script,
`const { ipcMain } = require("electron");
const event = { sender: { id: 9 }, senderFrame: { routingId: 7 } };
const channels = new Set(["on", "removed-once", "fired-once"]);
const added = [];
const removed = [];
ipcMain.addListener("newListener", (channel, listener) => { if (channels.has(channel)) added.push([channel, listener]); });
ipcMain.addListener("removeListener", (channel, listener) => { if (channels.has(channel)) removed.push([channel, listener]); });
const onListener = () => undefined;
const removedOnce = () => undefined;
let firedCalls = 0;
const firedOnce = () => { firedCalls += 1; };
ipcMain.on("on", onListener);
ipcMain.removeListener("on", onListener);
ipcMain.once("removed-once", removedOnce);
ipcMain.removeListener("removed-once", removedOnce);
ipcMain.once("fired-once", firedOnce);
ipcMain.emit("fired-once", event);
ipcMain.emit("fired-once", event);
const expected = [["on", onListener], ["removed-once", removedOnce], ["fired-once", firedOnce]];
const same = (actual) => actual.length === expected.length &&
actual.every(([channel, listener], index) => channel === expected[index][0] && listener === expected[index][1]);
const snapshot = globalThis.__reaElectronActiveSnapshot();
process.stdout.write(JSON.stringify({
added_originals: same(added),
removed_originals: same(removed),
fired_calls: firedCalls,
invocations: snapshot.events.filter((item) => item.kind === "main-event-invocation").map((item) => item.channel),
}) + "\\n");
`,
);
const execution = await runNode(script, root);
expect(execution.code, execution.stderr).toBe(0);
expect(JSON.parse(execution.stdout)).toEqual({
added_originals: true,
removed_originals: true,
fired_calls: 1,
invocations: ["fired-once"],
});
});
it("registers and removes ipcMain once listeners through decorators installed after the hook", async () => {
const root = await createTestTempDirectory("rea-electron-hook-ipc-decor-");
temporary.push(root);
await writeElectronFixture(root);
const script = join(root, "ipc-decorators.cjs");
await writeFile(
script,
`const { ipcMain } = require("electron");
const event = { sender: { id: 9 }, senderFrame: { routingId: 7 } };
const seen = [];
const hookedOn = ipcMain.on;
const hookedRemoveListener = ipcMain.removeListener;
ipcMain.on = function decoratedOn(channel, listener) {
if (channel === "decorated") seen.push("on");
return hookedOn.call(this, channel, listener);
};
ipcMain.removeListener = function decoratedRemoveListener(channel, listener) {
if (channel === "decorated") seen.push("removeListener");
return hookedRemoveListener.call(this, channel, listener);
};
let calls = 0;
const listener = () => { calls += 1; };
const returned = ipcMain.once("decorated", listener);
const registered = ipcMain.listenerCount("decorated", listener);
ipcMain.emit("decorated", event);
ipcMain.emit("decorated", event);
const snapshot = globalThis.__reaElectronActiveSnapshot();
process.stdout.write(JSON.stringify({
seen,
returns_emitter: returned === ipcMain,
registered,
remaining: ipcMain.listenerCount("decorated"),
calls,
invocations: snapshot.events.filter((item) => item.kind === "main-event-invocation").map((item) => item.channel),
}) + "\\n");
`,
);
const execution = await runNode(script, root);
expect(execution.code, execution.stderr).toBe(0);
expect(JSON.parse(execution.stdout)).toEqual({
seen: ["on", "removeListener"],
returns_emitter: true,
registered: 1,
remaining: 0,
calls: 1,
invocations: ["decorated"],
});
});
it("keeps hooked ipcMain listener semantics identical to an unhooked EventEmitter", async () => {
const root = await createTestTempDirectory("rea-electron-hook-ipc-diff-");
temporary.push(root);
await writeElectronFixture(root);
const script = join(root, "ipc-differential.cjs");
await writeFile(
script,
`const { ipcMain } = require("electron");
const { EventEmitter } = require("node:events");
const event = { sender: { id: 9 }, senderFrame: { routingId: 7 } };
const scenario = (emitter) => {
const log = [];
const names = new Map();
const named = (name) => {
const listener = () => { log.push("call:" + name); };
names.set(listener, name);
return listener;
};
const label = (listener) => names.get(listener) ?? "unknown";
emitter.on("newListener", (channel, listener) => {
if (channel === "ch") log.push("new:" + label(listener));
});
emitter.on("removeListener", (channel, listener) => {
if (channel === "ch") log.push("remove:" + label(listener));
});
const [a, b, c, d] = ["a", "b", "c", "d"].map(named);
const state = () => {
log.push("count:" + emitter.listenerCount("ch") + ":" + [a, b, c, d].map((listener) => emitter.listenerCount("ch", listener)).join(","));
log.push("listeners:" + emitter.listeners("ch").map(label).join(","));
log.push("raw:" + emitter.rawListeners("ch").map((listener) => label(listener.listener ?? listener)).join(","));
};
emitter.on("ch", a);
emitter.once("ch", b);
emitter.on("ch", c);
emitter.addListener("ch", a);
emitter.once("ch", d);
emitter.prependListener("ch", c);
emitter.prependOnceListener("ch", d);
state();
emitter.removeListener("ch", a);
state();
emitter.emit("ch", event);
state();
emitter.off("ch", c);
emitter.once("ch", b);
emitter.removeListener("ch", b);
emitter.once("ch", d);
state();
emitter.emit("ch", event);
state();
emitter.off("ch", c);
// An app function's own listener property is not a hook wrapper.
const e = named("e");
e.listener = c;
emitter.on("ch", a);
emitter.on("ch", c);
emitter.prependOnceListener("ch", d);
emitter.prependListener("ch", e);
emitter.on("ch", a);
state();
emitter.removeAllListeners("ch");
state();
emitter.on("ch", b);
emitter.on("ch", c);
emitter.removeAllListeners();
state();
return log;
};
process.stdout.write(JSON.stringify({
plain: scenario(new EventEmitter()),
hooked: scenario(ipcMain),
}) + "\\n");
`,
);
const execution = await runNode(script, root);
expect(execution.code, execution.stderr).toBe(0);
const { plain, hooked } = JSON.parse(execution.stdout) as {
plain: string[];
hooked: string[];
};
expect(plain).not.toContain("new:unknown");
expect(hooked).toEqual(plain);
});
it("keeps a replacement registered by a removeListener observer during ipcMain removeAllListeners", async () => {
const root = await createTestTempDirectory("rea-electron-hook-ipc-replace-");
temporary.push(root);
await writeElectronFixture(root);
const script = join(root, "ipc-replacement.cjs");
await writeFile(
script,
`const { ipcMain } = require("electron");
const { EventEmitter } = require("node:events");
const event = { sender: { id: 9 }, senderFrame: { routingId: 7 } };
const scenario = (emitter) => {
const log = [];
const names = new Map();
const named = (name) => {
const listener = () => { log.push("call:" + name); };
names.set(listener, name);
return listener;
};
// A wrapper is labelled by the listener its .listener names.
const label = (listener) =>
names.get(listener) ??
(names.has(listener.listener) ? "wrapper(" + names.get(listener.listener) + ")" : "unknown");
const [a, b, c, replacement] = ["a", "b", "c", "replacement"].map(named);
let replaced = false;
emitter.on("removeListener", (channel, listener) => {
if (channel !== "ch") return;
log.push("remove:" + label(listener));
if (replaced) return;
replaced = true;
emitter.on("ch", replacement);
});
emitter.on("ch", a);
emitter.once("ch", b);
emitter.on("ch", c);
const returned = emitter.removeAllListeners("ch");
log.push("returns_emitter:" + (returned === emitter));
log.push("listeners:" + emitter.listeners("ch").map(label).join(","));
emitter.emit("ch", event);
emitter.removeAllListeners();
log.push("cleared:" + emitter.listenerCount("ch") + ":" + emitter.listenerCount("removeListener"));
return log;
};
const plain = scenario(new EventEmitter());
const hooked = scenario(ipcMain);
const snapshot = globalThis.__reaElectronActiveSnapshot();
process.stdout.write(JSON.stringify({
plain,
hooked,
// The hooked removeListener observer records its own invocations.
invocations: snapshot.events.filter((item) => item.kind === "main-event-invocation" && item.channel === "ch").map((item) => item.channel),
}) + "\\n");
`,
);
const execution = await runNode(script, root);
expect(execution.code, execution.stderr).toBe(0);
const { plain, hooked, invocations } = JSON.parse(execution.stdout) as {
plain: string[];
hooked: string[];
invocations: string[];
};
expect(plain).toEqual([
"remove:c",
"remove:wrapper(b)",
"remove:a",
"returns_emitter:true",
"listeners:replacement",
"call:replacement",
"remove:replacement",
"cleared:0:0",
]);
expect(hooked).toEqual(plain);
expect(invocations).toEqual(["ch"]);
});
it("keeps ipcMain once self-removal through an on decorator that wraps the hook", async () => {
const root = await createTestTempDirectory("rea-electron-hook-ipc-wrap-");
temporary.push(root);
await writeElectronFixture(root);
const script = join(root, "ipc-wrapping-decorator.cjs");
await writeFile(
script,
`const { ipcMain } = require("electron");
const { EventEmitter } = require("node:events");
const event = { sender: { id: 9 }, senderFrame: { routingId: 7 } };
// Installed after the hook: wraps each listener and names it via .listener.
const decorate = (emitter) => {
const decoratedOn = emitter.on;
emitter.on = function wrappingOn(channel, listener) {
const layer = function decoratedListener(...args) {
return listener.apply(this, args);
};
layer.listener = listener;
return decoratedOn.call(this, channel, layer);
};
};
const scenario = (emitter) => {
decorate(emitter);
let calls = 0;
const listener = () => { calls += 1; };
const returned = emitter.once("decorated", listener);
const registered = emitter.listenerCount("decorated");
emitter.emit("decorated", event);
const afterFirst = emitter.listenerCount("decorated");
emitter.emit("decorated", event);
emitter.once("decorated", listener);
emitter.emit("decorated", event);
return {
returns_emitter: returned === emitter,
registered,
after_first: afterFirst,
remaining: emitter.listenerCount("decorated"),
raw: emitter.rawListeners("decorated").length,
calls,
};
};
const plain = scenario(new EventEmitter());
const hooked = scenario(ipcMain);
const snapshot = globalThis.__reaElectronActiveSnapshot();
process.stdout.write(JSON.stringify({
plain,
hooked,
invocations: snapshot.events.filter((item) => item.kind === "main-event-invocation").map((item) => item.channel),
}) + "\\n");
`,
);
const execution = await runNode(script, root);
expect(execution.code, execution.stderr).toBe(0);
const { plain, hooked, invocations } = JSON.parse(execution.stdout) as {
plain: Record<string, unknown>;
hooked: Record<string, unknown>;
invocations: string[];
};
expect(plain).toEqual({
returns_emitter: true,
registered: 1,
after_first: 0,
remaining: 0,
raw: 0,
calls: 2,
});
expect(hooked).toEqual(plain);
expect(invocations).toEqual(["decorated", "decorated"]);
});
it.each(["stable", "replaced", "readonly", "getter", "proxy"])(
"normalizes raw ipcMain on entries before %s removal decorators",
async (mode) => {
const root = await createTestTempDirectory(
"rea-electron-hook-ipc-remove-decorator-",
);
temporary.push(root);
await writeElectronFixture(root);
const script = join(root, "ipc-removal-decorator.cjs");
await writeFile(
script,
`const { ipcMain } = require("electron");
const { EventEmitter } = require("node:events");
const scenario = (emitter) => {
const seen = [];
const onListener = () => undefined;
const onceListener = () => undefined;
const nativeRemove = emitter.removeListener;
const decorate = (name) => function removalDecorator(channel, listener) {
if (this !== emitter) throw new Error("decorator receiver changed");
if (channel === "ch") {
const label = listener === onListener ? "on" :
listener.listener === onceListener ? "once" : "unknown";
seen.push(name + ":" + label);
if (label === "unknown") throw new Error("unexpected recording wrapper");
}
return nativeRemove.call(this, channel, listener);
};
emitter.on("removeListener", (channel) => {
if (${JSON.stringify(mode)} === "replaced" && channel === "ch")
emitter.removeListener = decorate("replacement");
});
emitter.on("ch", onListener);
emitter.once("ch", onceListener);
emitter.removeListener = decorate("first");
if (${JSON.stringify(mode)} === "readonly") Object.defineProperty(emitter, "removeListener", {
value: emitter.removeListener, writable: false, configurable: false,
});
if (${JSON.stringify(mode)} === "getter") Object.defineProperty(emitter, "removeListener", {
get() {
if (this !== emitter) throw new Error("getter receiver changed");
return decorate("first");
},
});
if (${JSON.stringify(mode)} === "proxy") emitter.removeListener = new Proxy(emitter.removeListener, {
get() { throw new Error("decorator metadata must not be read"); },
});
let failure = null;
let returnsEmitter = false;
try { returnsEmitter = emitter.removeAllListeners("ch") === emitter; }
catch (cause) { failure = cause.message; }
return { seen, returns_emitter: returnsEmitter, remaining: emitter.listenerCount("ch"), failure };
};
process.stdout.write(JSON.stringify({ plain: scenario(new EventEmitter()), hooked: scenario(ipcMain) }) + "\\n");
`,
);
const execution = await runNode(script, root);
expect(execution.code, execution.stderr).toBe(0);
const { plain, hooked } = JSON.parse(execution.stdout) as {
plain: Record<string, unknown>;
hooked: Record<string, unknown>;
};
expect(plain).toEqual({
seen: [
"first:once",
`${mode === "replaced" ? "replacement" : "first"}:on`,
],
returns_emitter: true,
remaining: 0,
failure: null,
});
expect(hooked).toEqual(plain);
},
);
it("preserves native removal before mutated raw listener metadata throws", async () => {
const root = await createTestTempDirectory(
"rea-electron-hook-ipc-mutated-metadata-",
);
temporary.push(root);
await writeElectronFixture(root);
const script = join(root, "ipc-mutated-listener-metadata.cjs");
await writeFile(
script,
`const { ipcMain } = require("electron");
const { EventEmitter } = require("node:events");
const scenario = (emitter) => {
const marker = new Error("application metadata failure");
let reads = 0;
emitter.on("removeListener", () => undefined);
emitter.on("ch", () => undefined);
Object.defineProperty(emitter.rawListeners("ch")[0], "listener", { get() {
reads += 1;
throw marker;
}});
let failure;
try { emitter.removeAllListeners("ch"); } catch (cause) { failure = cause; }
return { reads, remaining: emitter.listenerCount("ch"), error_preserved: failure === marker };
};
process.stdout.write(JSON.stringify({ plain: scenario(new EventEmitter()), hooked: scenario(ipcMain) }) + "\\n");
`,
);
const execution = await runNode(script, root);
expect(execution.code, execution.stderr).toBe(0);
const { plain, hooked } = JSON.parse(execution.stdout) as {
plain: Record<string, unknown>;
hooked: Record<string, unknown>;
};
expect(plain).toEqual({ reads: 1, remaining: 0, error_preserved: true });
expect(hooked).toEqual(plain);
});
it.each(["single-on", "multiple-on", "empty-once", "event-once"])(
"preserves native callable method reads during %s dispatch",
async (mode) => {
const root = await createTestTempDirectory(
"rea-electron-hook-ipc-dispatch-",
);
temporary.push(root);
await writeElectronFixture(root);
const script = join(root, "ipc-native-dispatch.cjs");
await writeFile(
script,
`const { ipcMain } = require("electron");
const { EventEmitter } = require("node:events");
const mode = ${JSON.stringify(mode)};
const scenario = (emitter) => {
const calls = [];
const reads = [];
const listener = new Proxy(function() { calls.push("first"); }, {
get(target, key, receiver) {
if (key === "apply" || key === "call") {
reads.push(key);
throw new Error("application invocation metadata");
}
return Reflect.get(target, key, receiver);
},
});
if (mode.endsWith("once")) emitter.once("ch", listener);
else emitter.on("ch", listener);
if (mode === "multiple-on") emitter.on("ch", () => calls.push("second"));
let failure = null;
try {
if (mode === "empty-once") emitter.emit("ch");
else emitter.emit("ch", { sender: { id: 9 } });
} catch (cause) { failure = cause.message; }
return { calls, reads, failure, remaining: emitter.listenerCount("ch") };
};
console.log(JSON.stringify({ plain: scenario(new EventEmitter()), hooked: scenario(ipcMain) }));
`,
);
const execution = await runNode(script, root);
expect(execution.code, execution.stderr).toBe(0);
const { plain, hooked } = JSON.parse(execution.stdout) as {
plain: Record<string, unknown>;
hooked: Record<string, unknown>;
};
expect(plain).toEqual({
calls: mode === "multiple-on" ? ["first", "second"] : [],
reads:
mode === "multiple-on"
? []
: [mode === "empty-once" ? "call" : "apply"],
failure:
mode === "multiple-on" ? null : "application invocation metadata",
remaining: mode === "multiple-on" ? 2 : mode === "single-on" ? 1 : 0,
});
expect(hooked).toEqual(plain);
},
);
it.each([
"arguments",
"array-like",
"null",
"undefined",
"missing",
"primitive",
])("preserves raw callback apply with %s argument lists", async (mode) => {
const root = await createTestTempDirectory("rea-electron-hook-ipc-apply-");
temporary.push(root);
await writeElectronFixture(root);
const script = join(root, "ipc-apply.cjs");
await writeFile(
script,
`const { ipcMain } = require("electron");
const { EventEmitter } = require("node:events");
const mode = ${JSON.stringify(mode)};
const scenario = (emitter) => {
let calls = 0, reads = 0, correctReceiver = false, arity = null;
const event = { sender: { id: 9 } };
let first = null;
emitter.on("ch", function(...args) {
calls++; correctReceiver = this === emitter; arity = args.length;
first = args.length ? args[0] === event : null;
});
const listener = emitter.rawListeners("ch")[0];
let list;
if (mode === "arguments") list = (function() { return arguments; })(event, 42);
if (mode === "array-like") list = { length: 2, get 0() { reads++; return event; }, 1: 42 };
if (mode === "null") list = null;
if (mode === "primitive") list = 42;
let typeError = false;
try {
if (mode === "missing") listener.apply(emitter);
else listener.apply(emitter, list);
} catch (cause) { typeError = cause instanceof TypeError; }
return { calls, reads, correctReceiver, arity, first, typeError };
};
console.log(JSON.stringify({ plain: scenario(new EventEmitter()), hooked: scenario(ipcMain) }));
`,
);
const execution = await runNode(script, root);
expect(execution.code, execution.stderr).toBe(0);
const { plain, hooked } = JSON.parse(execution.stdout) as {
plain: Record<string, unknown>;
hooked: Record<string, unknown>;
};
const populated = mode === "arguments" || mode === "array-like";
expect(plain).toEqual({
calls: mode === "primitive" ? 0 : 1,
reads: mode === "array-like" ? 1 : 0,
correctReceiver: mode !== "primitive",
arity: mode === "primitive" ? null : populated ? 2 : 0,
first: populated ? true : null,
typeError: mode === "primitive",
});
expect(hooked).toEqual(plain);
});
it("preserves the native once argument-list object passed to custom apply", async () => {
const root = await createTestTempDirectory(
"rea-electron-hook-ipc-once-args-",
);
temporary.push(root);
await writeElectronFixture(root);
const script = join(root, "ipc-once-arguments.cjs");
await writeFile(
script,
`const { ipcMain } = require("electron");
const { EventEmitter } = require("node:events");
const scenario = (emitter) => {
const event = { sender: { id: 9 } };
let calls = 0, argumentObject = false, correctReceiver = false;
const listener = function(value) { calls++; correctReceiver = this === emitter && value === event; };
listener.apply = function(receiver, list) {
argumentObject = Object.prototype.toString.call(list) === "[object Arguments]";
if (!argumentObject) throw new Error("once changed argument-list identity");
return Reflect.apply(Function.prototype.apply, this, [receiver, list]);
};
emitter.once("ch", listener);
let failure = null;
try { emitter.emit("ch", event); } catch (cause) { failure = cause.message; }
return { calls, argumentObject, correctReceiver, failure, remaining: emitter.listenerCount("ch") };
};
console.log(JSON.stringify({ plain: scenario(new EventEmitter()), hooked: scenario(ipcMain) }));
`,
);
const execution = await runNode(script, root);
expect(execution.code, execution.stderr).toBe(0);
const { plain, hooked } = JSON.parse(execution.stdout) as {
plain: Record<string, unknown>;
hooked: Record<string, unknown>;
};
expect(plain).toEqual({
calls: 1,
argumentObject: true,
correctReceiver: true,
failure: null,
remaining: 0,
});
expect(hooked).toEqual(plain);
});
it.each([
"function-setter",
"function-readonly",
"object-setter",
"object-readonly",
])("owns on-wrapper metadata despite inherited %s properties", async (mode) => {
const root = await createTestTempDirectory(
"rea-electron-hook-ipc-prototype-",
);
temporary.push(root);
await writeElectronFixture(root);
const script = join(root, "ipc-inherited-listener.cjs");
await writeFile(
script,
`const { ipcMain } = require("electron");
const { EventEmitter } = require("node:events");
const mode = ${JSON.stringify(mode)};
const scenario = (emitter) => {
const prototype = mode.startsWith("function") ? Function.prototype : Object.prototype;
const prior = Object.getOwnPropertyDescriptor(prototype, "listener");
let calls = 0, writes = 0, failure = null;
Object.defineProperty(prototype, "listener", mode.endsWith("setter") ? {
configurable: true, set() { writes++; throw new Error("inherited metadata write"); },
} : { configurable: true, writable: false, value: undefined });
try {
emitter.on("ch", () => { calls++; });
emitter.emit("ch", { sender: { id: 9 } });
} catch (cause) { failure = cause.message; }
finally {
if (prior) Object.defineProperty(prototype, "listener", prior);
else delete prototype.listener;
}
return { calls, writes, failure, remaining: emitter.listenerCount("ch") };
};
console.log(JSON.stringify({ plain: scenario(new EventEmitter()), hooked: scenario(ipcMain) }));
`,
);
const execution = await runNode(script, root);
expect(execution.code, execution.stderr).toBe(0);
const { plain, hooked } = JSON.parse(execution.stdout) as {
plain: Record<string, unknown>;
hooked: Record<string, unknown>;
};
expect(plain).toEqual({ calls: 1, writes: 0, failure: null, remaining: 1 });
expect(hooked).toEqual(plain);
});
it.each(["on", "multiple-on", "once"])(
"records %s invocations without consulting application Array species",
async (kind) => {
const root = await createTestTempDirectory(
"rea-electron-hook-ipc-species-",
);
temporary.push(root);
await writeElectronFixture(root);
const script = join(root, "ipc-array-species.cjs");
await writeFile(
script,
`const { ipcMain } = require("electron");
const { EventEmitter } = require("node:events");
const kind = ${JSON.stringify(kind)};
let retainedUnderSpecies = 0;
const scenario = (emitter) => {
let calls = 0, reads = 0, failure = null;
emitter[kind === "once" ? "once" : "on"]("ch", () => { calls++; });
if (kind === "multiple-on") emitter.on("ch", () => { calls++; });
const prior = Object.getOwnPropertyDescriptor(Array, Symbol.species);
Object.defineProperty(Array, Symbol.species, { configurable: true, get() {
reads++; throw new Error("application array species");
}});
try {
emitter.emit("ch", { sender: { id: 9 } }, 42);
if (emitter === ipcMain) {
for (const event of globalThis.__reaElectronActiveSnapshot().events)
if (event.kind === "main-event-invocation") retainedUnderSpecies++;
}
}
catch (cause) { failure = cause.message; }
finally { Object.defineProperty(Array, Symbol.species, prior); }
return { calls, reads, failure };
};
const plain = scenario(new EventEmitter());
const hooked = scenario(ipcMain);
const invocations = globalThis.__reaElectronActiveSnapshot().events.filter(e => e.kind === "main-event-invocation").length;
console.log(JSON.stringify({ plain, hooked, invocations, retainedUnderSpecies }));
`,
);
const execution = await runNode(script, root);
expect(execution.code, execution.stderr).toBe(0);
const { plain, hooked, invocations, retainedUnderSpecies } = JSON.parse(
execution.stdout,
) as {
plain: Record<string, unknown>;
hooked: Record<string, unknown>;
invocations: number;
retainedUnderSpecies: number;
};
const expected = kind === "multiple-on" ? 2 : 1;
expect(plain).toEqual({ calls: expected, reads: 0, failure: null });
expect(hooked).toEqual(plain);
expect(invocations).toBe(expected);
expect(retainedUnderSpecies).toBe(expected);
},
);
it.each(["array", "arguments", "array-like", "null"])(
"preserves opaque custom apply inputs for raw %s lists",
async (mode) => {
const root = await createTestTempDirectory(
"rea-electron-hook-custom-apply-",
);
temporary.push(root);
await writeElectronFixture(root);
const script = join(root, "ipc-custom-apply.cjs");
await writeFile(
script,
`const { ipcMain } = require("electron");
const { EventEmitter } = require("node:events");
const mode = ${JSON.stringify(mode)};
const scenario = (emitter) => {
let calls = 0, reads = 0, exact = false, methodThis = false, arity = null;
const token = {};
const listener = function(...args) { calls++; arity = args.length; return token; };
let list = [42, 43];
if (mode === "arguments") list = (function() { return arguments; })(42, 43);
if (mode === "array-like") list = { length: 2, get 0() { reads++; return 42; }, 1: 43 };
if (mode === "null") list = null;
listener.apply = function(receiver, input) {
exact = input === list;
methodThis = this === listener;
if (!exact) throw new Error("custom apply input was replaced");
return Reflect.apply(listener, receiver, input ?? []);
};
emitter.on("ch", listener);
let returned = false, failure = null;
try { returned = emitter.rawListeners("ch")[0].apply(emitter, list) === token; }
catch (cause) { failure = cause.message; }
return { calls, reads, exact, methodThis, arity, returned, failure };
};
const plain = scenario(new EventEmitter());
const hooked = scenario(ipcMain);
const snapshot = globalThis.__reaElectronActiveSnapshot();
console.log(JSON.stringify({ plain, hooked,
invocations: snapshot.events.filter(e => e.kind === "main-event-invocation").length,
gaps: snapshot.events.filter(e => e.kind === "ipc-observation-gap").map(e => ({channel: e.channel, event: e.event})) }));
`,
);
const execution = await runNode(script, root);
expect(execution.code, execution.stderr).toBe(0);
const { plain, hooked, invocations, gaps } = JSON.parse(
execution.stdout,
) as {
plain: Record<string, unknown>;
hooked: Record<string, unknown>;
invocations: number;
gaps: unknown[];
};
expect(plain).toEqual({
calls: 1,
reads: mode === "array-like" ? 1 : 0,
exact: true,
methodThis: true,
arity: mode === "null" ? 0 : 2,
returned: true,
failure: null,
});
expect(hooked).toEqual(plain);
expect(invocations).toBe(0);
expect(gaps).toEqual([{ channel: "ch", event: "custom-apply" }]);
},
);
it.each([false, true])(
"preserves a borrowed raw apply method's receiver (custom: %s)",
async (custom) => {
const root = await createTestTempDirectory(
"rea-electron-hook-borrowed-apply-",
);
temporary.push(root);
await writeElectronFixture(root);
const script = join(root, "ipc-borrowed-apply.cjs");
await writeFile(
script,
`const { ipcMain } = require("electron");
const { EventEmitter } = require("node:events");
const scenario = (emitter) => {
let originalCalls = 0, borrowedCalls = 0, sameList = false;
const token = {}, list = [42, 43];
const listener = function() { originalCalls++; };
const other = function(...args) { borrowedCalls++; return args[0] === 42 && args[1] === 43 ? token : null; };
if (${JSON.stringify(custom)}) listener.apply = function(receiver, input) {
sameList = input === list;
return Reflect.apply(this, receiver, input);
};
emitter.on("ch", listener);
const returned = emitter.rawListeners("ch")[0].apply.call(other, emitter, list) === token;
return { originalCalls, borrowedCalls, sameList, returned };
};
const plain = scenario(new EventEmitter()), hooked = scenario(ipcMain);
const snapshot = globalThis.__reaElectronActiveSnapshot();
console.log(JSON.stringify({ plain, hooked,
invocations: snapshot.events.filter(e => e.kind === "main-event-invocation").length }));
`,
);
const execution = await runNode(script, root);
expect(execution.code, execution.stderr).toBe(0);
const { plain, hooked, invocations } = JSON.parse(execution.stdout);
expect(plain).toEqual({
originalCalls: 0,
borrowedCalls: 1,
sameList: custom,
returned: true,
});
expect(hooked).toEqual(plain);
expect(invocations).toBe(0);
},
);
it.each([
"defineProperty",
"getOwnPropertyDescriptor",
"getPrototypeOf",
"create",
"get",
"set",
"Set",
"Proxy",
"WeakMap.has",
"WeakMap.get",
"WeakMap.set",
"Set.has",
"Set.add",
])(
"keeps listener registration and removal independent of late %s replacement",
async (method) => {
const root = await createTestTempDirectory(
"rea-electron-hook-late-intrinsic-",
);
temporary.push(root);
await writeElectronFixture(root);
const script = join(root, "ipc-late-intrinsic.cjs");
await writeFile(
script,
`const { ipcMain } = require("electron");
const { EventEmitter } = require("node:events");
const specification = ${JSON.stringify(method)};
const [constructor, member] = specification.split(".");
const method = member ?? specification;
const owner = member !== undefined ? globalThis[constructor].prototype
: method === "Set" || method === "Proxy" ? globalThis
: method === "get" || method === "set" ? Reflect : Object;
const original = owner[method];
const emitter = new EventEmitter();
const scenario = (target) => {
const seen = [];
let failure = null;
try {
target.on("ch", () => seen.push("on"));
target.once("one", () => seen.push("once"));
target.on("removeListener", () => {});
target.emit("ch", null, 42);
target.emit("one", null, 43);
target.emit("one", null, 44);
target.removeAllListeners();
} catch (cause) { failure = cause.message; }
return { seen, failure, remaining: target.eventNames().length };
};
owner[method] = function() { throw new Error("late application intrinsic replacement"); };
let plain, hooked;
try { plain = scenario(emitter); hooked = scenario(ipcMain); }
finally { owner[method] = original; }
const snapshot = globalThis.__reaElectronActiveSnapshot();
console.log(JSON.stringify({ plain, hooked,
invocations: snapshot.events.filter(e => e.kind === "main-event-invocation" && (e.channel === "ch" || e.channel === "one")).length }));
`,
);
const execution = await runNode(script, root);
expect(execution.code, execution.stderr).toBe(0);
const { plain, hooked, invocations } = JSON.parse(execution.stdout);
expect(plain).toEqual({
seen: ["on", "once"],
failure: null,
remaining: 0,
});
expect(hooked).toEqual(plain);
expect(invocations).toBe(2);
},
);
it.each([
{
mode: "data",
depth: 16,
prototypeDepth: 0,
once: true,
gaps: 0,
remaining: 1,
},
{
mode: "data",
depth: 17,
prototypeDepth: 0,
once: true,
gaps: 1,
remaining: 1,
},
{
mode: "accessor",
depth: 1,
prototypeDepth: 0,
once: true,
gaps: 1,
remaining: 0,
},
{
mode: "proxy",
depth: 1,
prototypeDepth: 0,
once: true,
gaps: 1,
remaining: 0,
},
{
mode: "data",
depth: 17,
prototypeDepth: 0,
once: false,
gaps: 1,
remaining: 1,
},
{
mode: "inherited-data",
depth: 1,
prototypeDepth: 1,
once: true,
gaps: 0,
remaining: 0,
},
{
mode: "inherited-accessor",
depth: 1,
prototypeDepth: 1,
once: true,
gaps: 1,
remaining: 0,
},
{
mode: "inherited-proxy",
depth: 1,
prototypeDepth: 1,
once: true,
gaps: 1,
remaining: 0,
},
{
mode: "inherited-data",
depth: 1,
prototypeDepth: 16,
once: true,
gaps: 0,
remaining: 0,
},
{
mode: "inherited-data",
depth: 1,
prototypeDepth: 17,
once: true,
gaps: 1,
remaining: 0,
},
])(
"preserves native dispatch for $mode chains (depth $depth, prototype $prototypeDepth, once $once)",
async ({ mode, depth, prototypeDepth, once, gaps, remaining }) => {
const root = await createTestTempDirectory(
"rea-electron-hook-chain-coverage-",
);
temporary.push(root);
await writeElectronFixture(root);
const script = join(root, "ipc-chain-coverage.cjs");
await writeFile(
script,
`const { ipcMain } = require("electron");
const { EventEmitter } = require("node:events");
const mode = ${JSON.stringify(mode)}, once = ${JSON.stringify(once)};
const scenario = (target) => {
let calls = 0, reads = 0;
for (let index = 0; index < ${depth}; index++) {
const previous = target.on;
target.on = function(channel, listener) {
let outer = function(...args) { return Reflect.apply(listener, this, args); };
if (mode.startsWith("inherited-")) {
let prototype = Object.create(Function.prototype);
Object.defineProperty(prototype, "listener", mode === "inherited-accessor"
? { get() { reads++; return listener; } } : { value: listener });
if (mode === "inherited-proxy") prototype = new Proxy(prototype, {
getOwnPropertyDescriptor() { throw new Error("prototype metadata reflection"); },
getPrototypeOf() { throw new Error("prototype traversal reflection"); }
});
for (let index = 1; index < ${prototypeDepth}; index++) prototype = Object.create(prototype);
Object.setPrototypeOf(outer, prototype);
}
else if (mode === "accessor") Object.defineProperty(outer, "listener", { get() { reads++; return listener; } });
else outer.listener = listener;
if (mode === "proxy") outer = new Proxy(outer, {
getOwnPropertyDescriptor() { throw new Error("application metadata reflection"); }
});
return Reflect.apply(previous, this, [channel, outer]);
};
}
target[once ? "once" : "on"]("ch", () => calls++);
target.emit("ch", null, 42); target.emit("ch", null, 43);
return { calls, reads, remaining: target.listenerCount("ch") };
};
const plain = scenario(new EventEmitter()), hooked = scenario(ipcMain);
const snapshot = globalThis.__reaElectronActiveSnapshot();
console.log(JSON.stringify({ plain, hooked,
invocations: snapshot.events.filter(e => e.kind === "main-event-invocation").length,
gaps: snapshot.events.filter(e => e.kind === "ipc-observation-gap").map(e => ({event: e.event, channel: e.channel})) }));
`,
);
const execution = await runNode(script, root);
expect(execution.code, execution.stderr).toBe(0);
const result = JSON.parse(execution.stdout);
expect(result.plain).toEqual({
calls: once ? 1 : 2,
reads: mode.endsWith("accessor") ? 1 : 0,
remaining,
});
expect(result.hooked).toEqual(result.plain);
expect(result.invocations).toBe(once ? 1 : 0);
expect(result.gaps).toEqual(
Array.from({ length: gaps }, () => ({
event: "listener-chain",
channel: "ch",
})),
);
},
);
it.each(["array", "map"])(
"snapshots retained state without application %s iterators",
async (kind) => {
const root = await createTestTempDirectory(
"rea-electron-hook-snapshot-iterator-",
);
temporary.push(root);
await writeElectronFixture(root);
const script = join(root, "ipc-snapshot-iterator.cjs");
await writeFile(
script,
`const { ipcMain } = require("electron");
let calls = 0, reads = 0, failure = null, snapshot;
ipcMain.on("ch", () => { calls++; });
ipcMain.emit("ch", { sender: { id: 9 } }, 42);
const prototype = ${JSON.stringify(kind)} === "array" ? Array.prototype : Map.prototype;
const prior = Object.getOwnPropertyDescriptor(prototype, Symbol.iterator);
Object.defineProperty(prototype, Symbol.iterator, { configurable: true, value() {
reads++; throw new Error("application iterator observed retained state");
}});
try { snapshot = globalThis.__reaElectronActiveSnapshot(); }
catch (cause) { failure = cause.message; }
finally { Object.defineProperty(prototype, Symbol.iterator, prior); }
const copied = snapshot?.events.find(e => e.kind === "main-event-invocation");
if (copied) { copied.channel = "changed"; copied.argument_shapes[0] = "changed"; }
const retained = globalThis.__reaElectronActiveSnapshot().events.find(e => e.kind === "main-event-invocation");
console.log(JSON.stringify({ calls, reads, failure, copied: !!copied, retained: {channel: retained.channel, shapes: retained.argument_shapes} }));
`,
);
const execution = await runNode(script, root);
expect(execution.code, execution.stderr).toBe(0);
expect(JSON.parse(execution.stdout)).toEqual({
calls: 1,
reads: 0,
failure: null,
copied: true,
retained: { channel: "ch", shapes: ["number"] },
});
},
);
it("invokes handle callbacks without reading callable method properties", async () => {
const root = await createTestTempDirectory("rea-electron-hook-ipc-callable-");
temporary.push(root);
await writeElectronFixture(root);
const script = join(root, "ipc-callable-properties.cjs");
await writeFile(
script,
`"use strict";
const { ipcMain } = require("electron");
const { EventEmitter } = require("node:events");
const scenario = async (emitter, observed) => {
let calls = 0;
let reads = 0;
let correctReceiver = false;
let arity = null;
const listener = new Proxy(function(...args) {
calls += 1;
correctReceiver = this === undefined;
arity = args.length;
}, { get(target, key, receiver) {
if (key === "call" || key === "apply") {
reads += 1;
throw new Error("callable metadata must not be read");
}
return Reflect.get(target, key, receiver);
}});
const event = { sender: { id: 9 } };
let failure = null;
try {
if (observed) {
emitter.handle("ch", listener);
const handler = emitter.handlers.get("ch");
await handler(event, 42);
} else {
await listener(event, 42);
}
} catch (cause) { failure = cause.message; }
return { calls, reads, correct_receiver: correctReceiver, arity, failure };
};
(async () => {
process.stdout.write(JSON.stringify({ plain: await scenario(new EventEmitter(), false),
hooked: await scenario(ipcMain, true) }) + "\\n");
})();
`,
);
const execution = await runNode(script, root);
expect(execution.code, execution.stderr).toBe(0);
const { plain, hooked } = JSON.parse(execution.stdout) as {
plain: Record<string, unknown>;
hooked: Record<string, unknown>;
};
expect(plain).toEqual({
calls: 1,
reads: 0,
correct_receiver: true,
arity: 2,
failure: null,
});
expect(hooked).toEqual(plain);
});
it("keeps native removal traversal when an ipcMain observer prepends a listener", async () => {
const root = await createTestTempDirectory("rea-electron-hook-ipc-prepend-");
temporary.push(root);
await writeElectronFixture(root);
const script = join(root, "ipc-prepend-during-removal.cjs");
await writeFile(
script,
`const { ipcMain } = require("electron");
const { EventEmitter } = require("node:events");
const scenario = (emitter) => {
const calls = [];
const removed = [];
const a = () => calls.push("a");
const b = () => calls.push("b");
const c = () => calls.push("c");
const replacement = () => calls.push("replacement");
const names = new Map([[a, "a"], [b, "b"], [c, "c"], [replacement, "replacement"]]);
let inserted = false;
emitter.on("removeListener", (channel, listener) => {
if (channel !== "ch") return;
removed.push(names.get(listener) ?? "unknown");
if (!inserted) {
inserted = true;
emitter.prependListener("ch", replacement);
}
});
emitter.on("ch", a);
emitter.on("ch", b);
emitter.on("ch", c);
emitter.removeAllListeners("ch");
const remaining = emitter.listeners("ch").map((listener) => names.get(listener));
emitter.emit("ch", { sender: { id: 9 } });
return { removed, remaining, calls };
};
process.stdout.write(JSON.stringify({ plain: scenario(new EventEmitter()), hooked: scenario(ipcMain) }) + "\\n");
`,
);
const execution = await runNode(script, root);
expect(execution.code, execution.stderr).toBe(0);
const { plain, hooked } = JSON.parse(execution.stdout) as {
plain: Record<string, unknown>;
hooked: Record<string, unknown>;
};
expect(hooked).toEqual(plain);
});
it.each(["accessor", "proxy"])(
"does not evaluate %s listener metadata while classifying ipcMain wrappers",
async (kind) => {
const root = await createTestTempDirectory(
"rea-electron-hook-ipc-accessor-",
);
temporary.push(root);
await writeElectronFixture(root);
const script = join(root, "ipc-listener-accessor.cjs");
await writeFile(
script,
`const { ipcMain } = require("electron");
const { EventEmitter } = require("node:events");
const event = { sender: { id: 9 } };
const scenario = (emitter) => {
let reads = 0;
let calls = 0;
let listener = () => { calls += 1; };
const inspectMetadata = () => {
reads += 1;
throw new Error("listener metadata must not run during registration");
};
if (${JSON.stringify(kind)} === "accessor") {
Object.defineProperty(listener, "listener", { get: inspectMetadata });
} else {
listener = new Proxy(listener, {
getOwnPropertyDescriptor: inspectMetadata,
get(target, key, receiver) {
if (key === "listener") return inspectMetadata();
return Reflect.get(target, key, receiver);
},
});
}
let failure = null;
let registered = 0;
try {
emitter.on("accessor", listener);
emitter.emit("accessor", event);
registered = emitter.listenerCount("accessor");
emitter.removeAllListeners("accessor");
} catch (cause) { failure = cause.message; }
return { reads, calls, registered, failure };
};
process.stdout.write(JSON.stringify({ plain: scenario(new EventEmitter()), hooked: scenario(ipcMain) }) + "\\n");
`,
);
const execution = await runNode(script, root);
expect(execution.code, execution.stderr).toBe(0);
const { plain, hooked } = JSON.parse(execution.stdout) as {
plain: Record<string, unknown>;
hooked: Record<string, unknown>;
};
expect(plain).toEqual({ reads: 0, calls: 1, registered: 1, failure: null });
expect(hooked).toEqual(plain);
},
);
it("isolates hook and exercise from ancestor Electron packages", async () => {
const root = await createTestTempDirectory("rea-electron-hook-ancestor-");
temporary.push(root);
await writeElectronFixture(root, 'module.exports = "/ambient/Electron";');
const ambientHook = await stageHook(join(root, "producer", "scripts"));
expect(createRequire(ambientHook)("electron")).toBe("/ambient/Electron");
const fixture = join(root, "fixture");
await writeElectronFixture(fixture);
const script = join(fixture, "exercise.cjs");
await writeFile(
script,
`const { BrowserWindow } = require("electron");
const window = new BrowserWindow({ webPreferences: {} });
process.stdout.write(JSON.stringify({
hook_error: globalThis.__reaElectronActiveSnapshot().hook_error,
window_open_handler: typeof window.webContents.windowOpenHandler,
}) + "\\n");
`,
);
const result = await runNode(script, fixture, ambientHook);
expect(result.code, result.stderr).toBe(0);
expect(JSON.parse(result.stdout)).toEqual({
hook_error: false,
window_open_handler: "function",
});
});
it("preserves fatal process termination after recording uncaught exceptions", async () => {
const root = await createTestTempDirectory("rea-electron-hook-fatal-");
temporary.push(root);
await writeElectronFixture(
root,
'throw new Error("controlled Electron unavailable");',
);
const script = join(root, "fatal.cjs");
await writeFile(
script,
'process.stdout.write(String(globalThis.__reaElectronActiveSnapshot().hook_error) + "\\n");\nthrow new Error("fatal-hook-secret");\n',
);
const result = await runNode(script, root);
expect(result.code).not.toBe(0);
expect(result.stdout.trim()).toBe("true");
expect(result.stderr).toContain("fatal-hook-secret");
});
const writeElectronFixture = async (
root: string,
source = electronModuleSource,
) => {
const directory = join(root, "node_modules", "electron");
await mkdir(directory, { recursive: true });
await writeFile(
join(directory, "package.json"),
JSON.stringify({ name: "electron", main: "index.cjs" }),
);
await writeFile(join(directory, "index.cjs"), source);
};
const stageHook = async (
root: string,
source = fileURLToPath(
new URL("../../../scripts/electron-active-hook.cjs", import.meta.url),
),
) => {
await mkdir(root, { recursive: true });
const hook = join(root, "electron-active-hook.cjs");
await Promise.all([
copyFile(source, hook),
copyFile(
join(dirname(source), "electron-active-hook-boundaries.cjs"),
join(root, "electron-active-hook-boundaries.cjs"),
),
]);
return hook;
};
const runNode = async (
script: string,
root: string,
sourceHook?: string,
): Promise<{
readonly code: number | null;
readonly stdout: string;
readonly stderr: string;
}> => {
const hook = await stageHook(root, sourceHook);
return new Promise((resolve, reject) => {
const child = spawn(process.execPath, ["-r", hook, script], {
cwd: root,
env: process.env,
stdio: ["ignore", "pipe", "pipe"],
});
const stdout: Buffer[] = [];
const stderr: Buffer[] = [];
child.stdout.on("data", (chunk: Buffer) => stdout.push(chunk));
child.stderr.on("data", (chunk: Buffer) => stderr.push(chunk));
child.once("error", reject);
child.once("close", (code) =>
resolve({
code,
stdout: Buffer.concat(stdout).toString("utf8"),
stderr: Buffer.concat(stderr).toString("utf8"),
}),
);
});
};