Files
rea/tests/boundary/browser/electronActiveHook.test.ts
T

3336 lines
118 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;
app.moveToApplicationsFolder = () => {
throw new Error("moveToApplicationsFolder must not run");
};
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;
},
};
globalThis.__nativeShellCalls = [];
const shell = {
openExternal: () => Promise.resolve(),
openPath: () => Promise.resolve(),
showItemInFolder: (path) => { globalThis.__nativeShellCalls.push(["showItemInFolder", path]); },
writeShortcutLink: (path) => { globalThis.__nativeShellCalls.push(["writeShortcutLink", path]); return true; },
};
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();
const moveResult = electron.app.moveToApplicationsFolder({
conflictHandler: () => {
throw new Error("conflictHandler must not run");
},
});
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, move_result: moveResult }) + "\\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 move_result: boolean;
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: "app-lifecycle",
event: "moveToApplicationsFolder",
phase: "blocked",
argument_shapes: ["object"],
error: true,
}),
expect.objectContaining({
kind: "updater",
event: "relaunch",
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(result.move_result).toBe(false);
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("blocks synchronous shell effects with their native return contracts and retains argument shapes only", async () => {
const root = await createTestTempDirectory("rea-electron-hook-shell-");
temporary.push(root);
await writeElectronFixture(root);
const script = join(root, "shell.cjs");
await writeFile(
script,
`const { shell } = require("electron");
const revealed = shell.showItemInFolder("/host/secret-folder/file.txt");
const written = shell.writeShortcutLink("C:\\\\Users\\\\host\\\\app.lnk", "create", { target: "C:\\\\payload.exe" });
const snapshot = globalThis.__reaElectronActiveSnapshot();
process.stdout.write(JSON.stringify({
revealed: revealed === undefined ? "undefined" : revealed,
written,
native_calls: globalThis.__nativeShellCalls,
snapshot,
}) + "\\n");
`,
);
const execution = await runNode(script, root);
expect(execution.code, execution.stderr).toBe(0);
const result = JSON.parse(execution.stdout.trim()) as {
readonly revealed: string;
readonly written: boolean;
readonly native_calls: readonly unknown[];
readonly snapshot: {
readonly hook_error: boolean;
readonly events: readonly Record<string, unknown>[];
};
};
expect(result.revealed).toBe("undefined");
expect(result.written).toBe(false);
expect(result.native_calls).toEqual([]);
expect(result.snapshot.hook_error).toBe(false);
const attempts = result.snapshot.events.filter(
(event) => event.kind === "shell-attempt",
);
expect(attempts).toEqual([
expect.objectContaining({
event: "showItemInFolder",
phase: "blocked",
argument_shapes: ["string"],
}),
expect.objectContaining({
event: "writeShortcutLink",
phase: "blocked",
argument_shapes: ["string", "string", "object"],
}),
]);
expect(JSON.stringify(result.snapshot)).not.toContain("secret-folder");
expect(JSON.stringify(result.snapshot)).not.toContain("payload.exe");
electronActiveTimelineEventSchema.array().parse(result.snapshot.events);
});
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("matches native once dispatch and retains only established invocations", async () => {
const root = await createTestTempDirectory("rea-electron-hook-native-once-");
temporary.push(root);
await writeElectronFixture(root);
const script = join(root, "electron-once-runtime.mjs");
await copyFile(
fileURLToPath(
new URL(
"../../../experiments/analysis-efficiency/electron-once-runtime.mjs",
import.meta.url,
),
),
script,
);
const execution = await runNode(script, root);
expect(execution.code, execution.stderr).toBe(0);
const result = JSON.parse(execution.stdout) as {
cases: number;
disagreements: number;
rows: readonly {
mode: string;
native: Record<string, unknown>;
hooked: Record<string, unknown> & { body_calls: number };
observation: readonly { kind: string; event: string | null }[];
}[];
};
expect(result.cases).toBe(39);
expect(result.disagreements).toBe(0);
for (const row of result.rows) {
expect(row.hooked, row.mode).toEqual(row.native);
const invocations = row.observation.filter(
(event) => event.kind === "main-event-invocation",
);
if (
row.mode === "inherited-listener-setter" ||
row.mode === "on-getter-listener-setter" ||
row.mode.startsWith("on-getter-native-alias")
) {
expect(invocations).toEqual([]);
expect(row.observation).toEqual([
{
kind: "ipc-observation-gap",
event: "once-metadata",
error: row.mode === "on-getter-native-alias-throws",
},
]);
} else {
expect(invocations.length, row.mode).toBe(row.hooked.body_calls);
if (row.hooked.body_calls === 0)
expect(
row.observation.some((event) => event.kind === "ipc-observation-gap"),
row.mode,
).toBe(true);
if (row.mode.includes("non-callable") && row.hooked.body_calls === 0)
expect(row.observation, row.mode).toEqual([
expect.objectContaining({
kind: "ipc-observation-gap",
error: true,
}),
]);
}
}
});
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>;
};
// The native dependency owns whether it consults call/apply. Compare the
// complete trace rather than requiring one pinned Node implementation.
expect(plain.remaining).toBe(
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,
// Newer runtimes bypass custom apply entirely. If native calls it, the
// callback's guard still requires its exact Arguments representation.
argumentObject: expect.any(Boolean),
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("uses the captured Array.isArray intrinsic while selecting a native listener entry", async () => {
const root = await createTestTempDirectory(
"rea-electron-hook-captured-array-is-array-",
);
temporary.push(root);
await writeElectronFixture(root);
const script = join(root, "captured-array-is-array.cjs");
await writeFile(
script,
`const { EventEmitter } = require("node:events");
const { ipcMain } = require("electron");
const scenario = (emitter) => {
const listener = function original() {};
const marker = function marker() {};
const foreign = function foreign() {};
emitter.on("ch", listener);
emitter.on("ch", marker);
emitter.rawListeners("ch")[0].listener = foreign;
const originalIsArray = Array.isArray;
let failure = null;
Array.isArray = () => { throw new Error("late application Array.isArray"); };
try { emitter.removeListener("ch", listener); }
catch (cause) { failure = cause.message; }
finally { Array.isArray = originalIsArray; }
return { failure, remaining: emitter.listenerCount("ch"), eventNames: emitter.eventNames() };
};
console.log(JSON.stringify({ plain: scenario(new EventEmitter()), hooked: scenario(ipcMain),
hook_error: globalThis.__reaElectronActiveSnapshot().hook_error }));
`,
);
const execution = await runNode(script, root);
expect(execution.code, execution.stderr).toBe(0);
const result = JSON.parse(execution.stdout);
expect(result.hook_error).toBe(false);
expect(result.hooked).toEqual(result.plain);
expect(result.plain).toEqual({
failure: null,
remaining: 1,
eventNames: ["ch"],
});
});
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 {
const onListener = () => seen.push("on");
target.on("ch", onListener);
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("does not read an application rawListeners accessor during hook setup", async () => {
const root = await createTestTempDirectory(
"rea-electron-hook-raw-listeners-accessor-",
);
temporary.push(root);
const source = electronModuleSource.replace(
"const ipcMain = new EventEmitter();",
`const ipcMain = new EventEmitter();
let rawListenersReads = 0;
Object.defineProperty(ipcMain, "rawListeners", { configurable: true, get() {
rawListenersReads++;
throw new Error("application rawListeners getter");
}});
globalThis.__rawListenersReads = () => rawListenersReads;`,
);
await writeElectronFixture(root, source);
const script = join(root, "raw-listeners-accessor.cjs");
await writeFile(
script,
`const { ipcMain } = require("electron");
let calls = 0;
ipcMain.on("ch", () => calls++);
ipcMain.emit("ch");
ipcMain.removeAllListeners("ch");
console.log(JSON.stringify({ calls, reads: globalThis.__rawListenersReads(),
hook_error: globalThis.__reaElectronActiveSnapshot().hook_error }));
`,
);
const execution = await runNode(script, root);
expect(execution.code, execution.stderr).toBe(0);
expect(JSON.parse(execution.stdout)).toEqual({
calls: 1,
reads: 0,
hook_error: false,
});
});
it("leaves an accessor removal method untouched and suppresses uncertain invocation evidence", async () => {
const root = await createTestTempDirectory(
"rea-electron-hook-removal-method-accessor-",
);
temporary.push(root);
const source = electronModuleSource.replace(
"const ipcMain = new EventEmitter();",
`const ipcMain = new EventEmitter();
let removeListenerReads = 0;
Object.defineProperty(ipcMain, "removeListener", { configurable: true, get() {
removeListenerReads++;
return EventEmitter.prototype.removeListener;
}});
globalThis.__removeListenerReads = () => removeListenerReads;`,
);
await writeElectronFixture(root, source);
const script = join(root, "removal-method-accessor.cjs");
await writeFile(
script,
`const { ipcMain } = require("electron");
const listener = function original() {};
ipcMain.on("ch", listener);
ipcMain.emit("ch");
ipcMain.removeAllListeners("ch");
const snapshot = globalThis.__reaElectronActiveSnapshot();
console.log(JSON.stringify({ reads: globalThis.__removeListenerReads(),
invocations: snapshot.events.filter((event) => event.kind === "main-event-invocation"),
gaps: snapshot.events.filter((event) => event.kind === "ipc-observation-gap"),
remaining: ipcMain.listenerCount("ch"), hook_error: snapshot.hook_error }));
`,
);
const execution = await runNode(script, root);
expect(execution.code, execution.stderr).toBe(0);
const result = JSON.parse(execution.stdout) as {
readonly reads: number;
readonly invocations: readonly unknown[];
readonly gaps: readonly {
readonly event: string;
readonly channel: string | null;
}[];
readonly remaining: number;
readonly hook_error: boolean;
};
expect(result).toMatchObject({
reads: 0,
invocations: [],
remaining: 0,
hook_error: false,
});
expect(result.gaps).toEqual([
expect.objectContaining({ event: "listener-registration", channel: "ch" }),
]);
});
it("preserves native last-registration order when the last raw listener link is mutated", async () => {
const root = await createTestTempDirectory(
"rea-electron-hook-removal-mutated-lifo-",
);
temporary.push(root);
await writeElectronFixture(root);
const script = join(root, "removal-mutated-lifo.cjs");
await writeFile(
script,
`const { EventEmitter } = require("node:events");
const { ipcMain } = require("electron");
const scenario = (emitter) => {
const calls = [];
const listener = () => calls.push("listener");
const marker = () => calls.push("marker");
const foreign = () => calls.push("foreign");
const nativeRemove = emitter.removeListener;
let removals = 0;
let failure = null;
emitter.on("removeListener", () => {});
emitter.on("ch", listener);
emitter.on("ch", marker);
emitter.on("ch", listener);
emitter.rawListeners("ch").at(-1).listener = foreign;
emitter.removeListener = function(channel, candidate) {
if (channel === "ch" && ++removals === 2)
throw new Error("second callback removal rejected");
return Reflect.apply(nativeRemove, this, arguments);
};
try { emitter.removeAllListeners("ch"); } catch (cause) { failure = cause.message; }
emitter.removeListener = nativeRemove;
calls.length = 0;
emitter.emit("ch");
return { failure, removals, remaining: emitter.listenerCount("ch"), calls };
};
process.stdout.write(JSON.stringify({
plain: scenario(new EventEmitter()),
hooked: scenario(ipcMain),
hook_error: globalThis.__reaElectronActiveSnapshot().hook_error,
}) + "\\n");
`,
);
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 plain: {
readonly failure: string;
readonly removals: number;
readonly remaining: number;
readonly calls: readonly string[];
};
readonly hooked: {
readonly failure: string;
readonly removals: number;
readonly remaining: number;
readonly calls: readonly string[];
};
readonly hook_error: boolean;
};
expect(result.hook_error).toBe(false);
expect(result.hooked).toEqual(result.plain);
expect(result.plain).toEqual({
failure: "second callback removal rejected",
removals: 2,
remaining: 2,
calls: ["listener", "marker"],
});
});
it("lets a later data listener link win over an earlier owned callback", async () => {
const root = await createTestTempDirectory(
"rea-electron-hook-removal-later-data-link-",
);
temporary.push(root);
await writeElectronFixture(root);
const script = join(root, "removal-later-data-link.cjs");
await writeFile(
script,
`const { EventEmitter } = require("node:events");
const { ipcMain } = require("electron");
const scenario = (emitter) => {
const calls = [];
const original = function original() { calls.push("original"); };
const later = function later() { calls.push("later"); };
const removed = [];
emitter.on("removeListener", (_channel, callback) => removed.push(callback === original ? "original" : callback === later ? "later" : "other"));
emitter.on("ch", original);
emitter.on("ch", later);
emitter.rawListeners("ch")[1].listener = original;
emitter.removeListener("ch", original);
return { remaining: emitter.listenerCount("ch"), removed,
originalStillRegistered: emitter.listeners("ch").includes(original),
laterStillRegistered: emitter.listeners("ch").includes(later) };
};
console.log(JSON.stringify({ plain: scenario(new EventEmitter()), hooked: scenario(ipcMain),
hook_error: globalThis.__reaElectronActiveSnapshot().hook_error }));
`,
);
const execution = await runNode(script, root);
expect(execution.code, execution.stderr).toBe(0);
const result = JSON.parse(execution.stdout) as {
readonly plain: {
readonly remaining: number;
readonly removed: readonly string[];
readonly originalStillRegistered: boolean;
readonly laterStillRegistered: boolean;
};
readonly hooked: {
readonly remaining: number;
readonly removed: readonly string[];
readonly originalStillRegistered: boolean;
readonly laterStillRegistered: boolean;
};
readonly hook_error: boolean;
};
expect(result.hook_error).toBe(false);
expect(result.hooked).toEqual(result.plain);
expect(result.plain).toEqual({
remaining: 1,
removed: ["original"],
originalStillRegistered: true,
laterStillRegistered: false,
});
});
it("normalizes a later raw link to an earlier owned registration", async () => {
const root = await createTestTempDirectory(
"rea-electron-hook-removal-owned-link-chain-",
);
temporary.push(root);
await writeElectronFixture(root);
const script = join(root, "removal-owned-link-chain.cjs");
await writeFile(
script,
`const { EventEmitter } = require("node:events");
const { ipcMain } = require("electron");
const scenario = (emitter) => {
const calls = [];
const original = function original() { calls.push("original"); };
const later = function later() { calls.push("later"); };
const foreign = function foreign() {};
const removed = [];
emitter.on("removeListener", (_channel, callback) => removed.push(callback === original ? "original" : callback === later ? "later" : "other"));
emitter.on("ch", original);
emitter.on("ch", later);
const first = emitter.rawListeners("ch")[0];
const second = emitter.rawListeners("ch")[1];
first.listener = foreign;
second.listener = first;
emitter.removeListener("ch", original);
emitter.emit("ch");
return { remaining: emitter.listenerCount("ch"), removed, calls };
};
console.log(JSON.stringify({ plain: scenario(new EventEmitter()), hooked: scenario(ipcMain),
hook_error: globalThis.__reaElectronActiveSnapshot().hook_error }));
`,
);
const execution = await runNode(script, root);
expect(execution.code, execution.stderr).toBe(0);
const result = JSON.parse(execution.stdout) as {
readonly plain: {
readonly remaining: number;
readonly removed: readonly string[];
readonly calls: readonly string[];
};
readonly hooked: {
readonly remaining: number;
readonly removed: readonly string[];
readonly calls: readonly string[];
};
readonly hook_error: boolean;
};
expect(result.hook_error).toBe(false);
expect(result.hooked).toEqual(result.plain);
expect(result.plain).toEqual({
remaining: 1,
removed: ["original"],
calls: ["original"],
});
});
it("preserves native direct-removal cleanup with throwing listener metadata", async () => {
const root = await createTestTempDirectory(
"rea-electron-hook-removal-accessor-gap-",
);
temporary.push(root);
await writeElectronFixture(root);
const script = join(root, "removal-accessor-gap.cjs");
await writeFile(
script,
`const { ipcMain } = require("electron");
const marker = new Error("listener metadata getter");
let reads = 0;
const listener = function original() {};
ipcMain.on("ch", listener);
Object.defineProperty(ipcMain.rawListeners("ch")[0], "listener", { get() {
reads++;
throw marker;
}});
let failure;
try { ipcMain.removeListener("ch", listener); } catch (cause) { failure = cause; }
const snapshot = globalThis.__reaElectronActiveSnapshot();
console.log(JSON.stringify({ reads, sameFailure: failure === marker,
remaining: ipcMain.listenerCount("ch"),
gaps: snapshot.events.filter((event) => event.kind === "ipc-observation-gap"),
hook_error: snapshot.hook_error }));
`,
);
const execution = await runNode(script, root);
expect(execution.code, execution.stderr).toBe(0);
const result = JSON.parse(execution.stdout) as {
readonly reads: number;
readonly sameFailure: boolean;
readonly remaining: number;
readonly gaps: readonly Record<string, unknown>[];
readonly hook_error: boolean;
};
// Pinned Node24.18 removes the exact callback without reading its public
// metadata when no removeListener observer is registered.
expect(result).toMatchObject({
reads: 0,
sameFailure: false,
remaining: 0,
hook_error: false,
});
expect(result.gaps).toEqual([]);
});
it("forwards native direct-removal bookkeeping to the real emitter", async () => {
const root = await createTestTempDirectory(
"rea-electron-hook-removal-bookkeeping-",
);
temporary.push(root);
await writeElectronFixture(root);
const script = join(root, "removal-bookkeeping.cjs");
await writeFile(
script,
`const { EventEmitter } = require("node:events");
const { ipcMain } = require("electron");
const scenario = (emitter) => {
const listener = function original() {};
const foreign = function foreign() {};
const payloads = [];
emitter.on("removeListener", (_channel, callback) => payloads.push(callback === foreign ? "foreign" : callback === listener ? "original" : "other"));
emitter.on("ch", listener);
emitter.rawListeners("ch")[0].listener = foreign;
emitter.removeListener("ch", listener);
const afterRemoval = { eventCount: emitter._eventsCount, names: emitter.eventNames().map(String) };
emitter.on("after", () => {});
const afterRegister = { eventCount: emitter._eventsCount, names: emitter.eventNames().map(String) };
return { payloads, afterRemoval, afterRegister };
};
console.log(JSON.stringify({ plain: scenario(new EventEmitter()), hooked: scenario(ipcMain),
hook_error: globalThis.__reaElectronActiveSnapshot().hook_error }));
`,
);
const execution = await runNode(script, root);
expect(execution.code, execution.stderr).toBe(0);
const result = JSON.parse(execution.stdout) as {
readonly plain: {
readonly payloads: readonly string[];
readonly afterRemoval: {
readonly eventCount: number;
readonly names: readonly string[];
};
readonly afterRegister: {
readonly eventCount: number;
readonly names: readonly string[];
};
};
readonly hooked: {
readonly payloads: readonly string[];
readonly afterRemoval: {
readonly eventCount: number;
readonly names: readonly string[];
};
readonly afterRegister: {
readonly eventCount: number;
readonly names: readonly string[];
};
};
readonly hook_error: boolean;
};
expect(result.hook_error).toBe(false);
expect(result.hooked).toEqual(result.plain);
expect(result.plain).toEqual({
payloads: ["foreign"],
afterRemoval: { eventCount: 1, names: ["removeListener"] },
afterRegister: { eventCount: 2, names: ["removeListener", "after"] },
});
});
it.each([1, 2] as const)(
"preserves owned listener accessor meta-event behavior with %s registration(s)",
async (registrationCount) => {
const root = await createTestTempDirectory(
"rea-electron-hook-removal-meta-accessor-",
);
temporary.push(root);
await writeElectronFixture(root);
const script = join(root, "removal-meta-accessor.cjs");
await writeFile(
script,
`const { EventEmitter } = require("node:events");
const { ipcMain } = require("electron");
const registrationCount = ${JSON.stringify(registrationCount)};
const scenario = (emitter) => {
const stages = [];
const listener = function original() {};
const marker = function marker() {};
let reads = 0;
emitter.on("removeListener", (channel, callback) => {
stages.push({ kind: "meta", channel, callbackIsOriginal: callback === listener,
callbackIsMarker: callback === marker, remaining: emitter.listenerCount("ch") });
});
for (let index = 0; index < registrationCount; index++) emitter.on("ch", listener);
if (registrationCount === 2) emitter.on("ch", marker);
Object.defineProperty(emitter.rawListeners("ch")[0], "listener", { configurable: true, get() {
reads++;
stages.push({ kind: "getter" });
return listener;
}});
let failure = null;
try { emitter.removeListener("ch", listener); } catch (cause) { failure = cause.message; }
return { reads, failure, remaining: emitter.listenerCount("ch"), stages };
};
const plain = scenario(new EventEmitter());
const hooked = scenario(ipcMain);
console.log(JSON.stringify({ plain, hooked,
hook_error: globalThis.__reaElectronActiveSnapshot().hook_error }));
`,
);
const execution = await runNode(script, root);
expect(execution.code, execution.stderr).toBe(0);
const result = JSON.parse(execution.stdout) as {
readonly plain: {
readonly reads: number;
readonly failure: string | null;
readonly remaining: number;
readonly stages: readonly Record<string, unknown>[];
};
readonly hooked: {
readonly reads: number;
readonly failure: string | null;
readonly remaining: number;
readonly stages: readonly Record<string, unknown>[];
};
readonly hook_error: boolean;
};
expect(result.hook_error).toBe(false);
expect(result.hooked).toEqual(result.plain);
expect(result.plain.reads).toBe(registrationCount === 1 ? 1 : 0);
expect(result.plain.failure).toBeNull();
expect(result.plain.stages[0]).toMatchObject(
registrationCount === 1 ? { kind: "getter" } : { kind: "meta" },
);
expect(result.plain.stages.at(-1)).toMatchObject({
kind: "meta",
channel: "ch",
callbackIsOriginal: true,
remaining: registrationCount === 1 ? 0 : 2,
});
},
);
it("keeps listener ownership when native function prototype metadata changes", async () => {
const root = await createTestTempDirectory(
"rea-electron-hook-wrapper-constructor-",
);
temporary.push(root);
await writeElectronFixture(root);
const script = join(root, "wrapper-constructor.cjs");
await writeFile(
script,
`const { EventEmitter } = require("node:events");
const { ipcMain } = require("electron");
const scenario = (emitter) => {
const listener = function original() {};
const foreign = function foreign() {};
emitter.on("ch", listener);
const entry = emitter.rawListeners("ch")[0];
const constructor = entry.prototype.constructor;
const constructorIsPublicEntry = constructor === entry;
constructor.listener = foreign;
emitter.removeListener("ch", listener);
return { constructorIsPublicEntry, remaining: emitter.listenerCount("ch") };
};
console.log(JSON.stringify({ plain: scenario(new EventEmitter()), hooked: scenario(ipcMain),
hook_error: globalThis.__reaElectronActiveSnapshot().hook_error }));
`,
);
const execution = await runNode(script, root);
expect(execution.code, execution.stderr).toBe(0);
const result = JSON.parse(execution.stdout);
expect(result.hook_error).toBe(false);
expect(result.hooked).toEqual(result.plain);
expect(result.plain).toEqual({
constructorIsPublicEntry: true,
remaining: 0,
});
});
it("matches native custom inspection that mutates the raw listener", async () => {
const root = await createTestTempDirectory(
"rea-electron-hook-removal-inspect-mutation-",
);
temporary.push(root);
await writeElectronFixture(root);
const script = join(root, "removal-inspect-mutation.cjs");
await writeFile(
script,
`const { inspect } = require("node:util");
const { EventEmitter } = require("node:events");
const { ipcMain } = require("electron");
const scenario = (emitter) => {
const listener = function original() {};
const foreign = function foreign() {};
emitter.on("ch", listener);
const raw = emitter.rawListeners("ch")[0];
let inspectorReceiverIsRaw = false;
raw[inspect.custom] = function() {
inspectorReceiverIsRaw = this === raw;
this.listener = foreign;
return "listener inspected";
};
const description = inspect(raw, { showProxy: true });
emitter.removeListener("ch", listener);
return { inspectorReceiverIsRaw, description,
remaining: emitter.listenerCount("ch") };
};
console.log(JSON.stringify({ plain: scenario(new EventEmitter()), hooked: scenario(ipcMain),
hook_error: globalThis.__reaElectronActiveSnapshot().hook_error }));
`,
);
const execution = await runNode(script, root);
expect(execution.code, execution.stderr).toBe(0);
const result = JSON.parse(execution.stdout);
expect(result.hook_error).toBe(false);
expect(result.hooked).toEqual(result.plain);
expect(result.plain).toEqual({
inspectorReceiverIsRaw: true,
description: "listener inspected",
remaining: 0,
});
});
it("normalizes an earlier raw owner in a later removal meta payload", async () => {
const root = await createTestTempDirectory(
"rea-electron-hook-removal-owned-meta-link-",
);
temporary.push(root);
await writeElectronFixture(root);
const script = join(root, "removal-owned-meta-link.cjs");
await writeFile(
script,
`const { EventEmitter } = require("node:events");
const { ipcMain } = require("electron");
const scenario = (emitter) => {
const original = function original() {};
const later = function later() {};
const payloads = [];
emitter.on("removeListener", (_channel, callback) => payloads.push(
callback === original ? "original" : callback === later ? "later" : "other"));
emitter.on("ch", original);
const firstRaw = emitter.rawListeners("ch")[0];
emitter.removeListener("ch", original);
emitter.on("ch", later);
emitter.rawListeners("ch")[0].listener = firstRaw;
emitter.removeListener("ch", original);
return { payloads, remaining: emitter.listenerCount("ch") };
};
console.log(JSON.stringify({ plain: scenario(new EventEmitter()), hooked: scenario(ipcMain),
hook_error: globalThis.__reaElectronActiveSnapshot().hook_error }));
`,
);
const execution = await runNode(script, root);
expect(execution.code, execution.stderr).toBe(0);
const result = JSON.parse(execution.stdout);
expect(result.hook_error).toBe(false);
expect(result.hooked).toEqual(result.plain);
expect(result.plain).toEqual({
payloads: ["original", "original"],
remaining: 0,
});
});
it("preserves the native argument that proves a chained removal match", async () => {
const root = await createTestTempDirectory(
"rea-electron-hook-removal-chain-argument-",
);
temporary.push(root);
await writeElectronFixture(root);
const script = join(root, "removal-chain-argument.cjs");
await writeFile(
script,
`const { EventEmitter } = require("node:events");
const { ipcMain } = require("electron");
const scenario = (emitter) => {
const calls = [];
const original = function original() { calls.push("original"); };
const g = function g() { calls.push("g"); };
const h = function h() { calls.push("h"); };
const removed = [];
emitter.on("removeListener", (_channel, callback) => removed.push(
callback === original ? "original" : callback === g ? "g" : callback === h ? "h" : "other"));
emitter.on("ch", original);
const firstRaw = emitter.rawListeners("ch")[0];
g.listener = firstRaw;
emitter.on("ch", g);
h.listener = g;
emitter.on("ch", h);
emitter.removeListener("ch", original);
emitter.emit("ch");
return { remaining: emitter.listenerCount("ch"), removed, calls };
};
console.log(JSON.stringify({ plain: scenario(new EventEmitter()), hooked: scenario(ipcMain),
hook_error: globalThis.__reaElectronActiveSnapshot().hook_error }));
`,
);
const execution = await runNode(script, root);
expect(execution.code, execution.stderr).toBe(0);
const result = JSON.parse(execution.stdout);
expect(result.hook_error).toBe(false);
expect(result.hooked).toEqual(result.plain);
expect(result.plain).toEqual({
remaining: 2,
removed: ["original"],
calls: ["original", "h"],
});
});
it("falls back to native errors for a revoked listener-array proxy", async () => {
const root = await createTestTempDirectory(
"rea-electron-hook-revoked-listener-array-",
);
temporary.push(root);
await writeElectronFixture(root);
const script = join(root, "revoked-listener-array.cjs");
await writeFile(
script,
`const { EventEmitter } = require("node:events");
const { ipcMain } = require("electron");
const scenario = (emitter) => {
const listener = function original() {};
const marker = function marker() {};
emitter.on("ch", listener);
emitter.on("ch", marker);
const entry = emitter.rawListeners("ch")[0];
entry.listener = function foreign() {};
const revocable = Proxy.revocable(emitter._events.ch, {});
emitter._events.ch = revocable.proxy;
revocable.revoke();
let failure = null;
try { emitter.removeListener("ch", listener); } catch (cause) { failure = { name: cause.name, message: cause.message }; }
return failure;
};
const plain = scenario(new EventEmitter());
const hooked = scenario(ipcMain);
console.log(JSON.stringify({ plain, hooked,
gaps: globalThis.__reaElectronActiveSnapshot().events.filter((event) => event.kind === "ipc-observation-gap"),
hook_error: globalThis.__reaElectronActiveSnapshot().hook_error }));
`,
);
const execution = await runNode(script, root);
expect(execution.code, execution.stderr).toBe(0);
const result = JSON.parse(execution.stdout) as {
readonly plain: { readonly name: string; readonly message: string };
readonly hooked: { readonly name: string; readonly message: string };
readonly gaps: readonly {
readonly event: string;
readonly target: string;
}[];
readonly hook_error: boolean;
};
expect(result.hook_error).toBe(false);
expect(result.hooked).toEqual(result.plain);
expect(result.gaps).toContainEqual(
expect.objectContaining({
event: "listener-removal",
target: "ipcMain.removeListener",
}),
);
});
it("records a gap when an opaque later registration blocks ownership ordering", async () => {
const root = await createTestTempDirectory(
"rea-electron-hook-removal-opaque-gap-",
);
temporary.push(root);
await writeElectronFixture(root);
const script = join(root, "removal-opaque-gap.cjs");
await writeFile(
script,
`const { ipcMain } = require("electron");
const listener = function original() {};
const opaque = function opaque() {};
ipcMain.on("ch", listener);
ipcMain.on("ch", opaque);
ipcMain.rawListeners("ch")[0].listener = listener;
Object.defineProperty(ipcMain.rawListeners("ch")[1], "listener", { get() { return undefined; } });
ipcMain.removeListener("ch", listener);
const snapshot = globalThis.__reaElectronActiveSnapshot();
console.log(JSON.stringify({ remaining: ipcMain.listenerCount("ch"),
gaps: snapshot.events.filter((event) => event.kind === "ipc-observation-gap"),
hook_error: snapshot.hook_error }));
`,
);
const execution = await runNode(script, root);
expect(execution.code, execution.stderr).toBe(0);
const result = JSON.parse(execution.stdout) as {
readonly remaining: number;
readonly gaps: readonly {
readonly event: string;
readonly channel: string | null;
}[];
readonly hook_error: boolean;
};
expect(result.remaining).toBe(1);
expect(result.gaps).toContainEqual(
expect.objectContaining({
kind: "ipc-observation-gap",
event: "listener-removal",
phase: "observed",
channel: "ch",
target: "ipcMain.removeListener",
process_type: "main",
error: false,
}),
);
expect(result.hook_error).toBe(false);
});
it("preserves pre-hook removeListener decorator receiver and callback identity", async () => {
const root = await createTestTempDirectory(
"rea-electron-hook-preinstalled-removal-decorator-",
);
temporary.push(root);
const source = electronModuleSource.replace(
"const ipcMain = new EventEmitter();",
`const ipcMain = new EventEmitter();
globalThis.__preHookRemovalCalls = [];
const originalRemoveListener = EventEmitter.prototype.removeListener;
ipcMain.removeListener = function(channel, callback) {
globalThis.__preHookRemovalCalls.push({
receiverIsEmitter: this === ipcMain,
callbackName: callback.name,
});
return Reflect.apply(originalRemoveListener, this, arguments);
};`,
);
await writeElectronFixture(root, source);
const script = join(root, "preinstalled-removal-decorator.cjs");
await writeFile(
script,
`const { EventEmitter } = require("node:events");
const { ipcMain } = require("electron");
const observations = globalThis.__preHookRemovalCalls;
const decorate = (emitter) => {
const native = EventEmitter.prototype.removeListener;
emitter.removeListener = function(channel, callback) {
observations.push({ receiverIsEmitter: this === emitter, callbackName: callback.name });
return Reflect.apply(native, this, arguments);
};
return emitter;
};
const scenario = (emitter) => {
const original = function original() {};
emitter.on("ch", original);
emitter.rawListeners("ch")[0].listener = () => {};
emitter.removeListener("ch", original);
return emitter.listenerCount("ch");
};
const plain = scenario(decorate(new EventEmitter()));
const hooked = scenario(ipcMain);
console.log(JSON.stringify({ plain, hooked, observations,
hook_error: globalThis.__reaElectronActiveSnapshot().hook_error }));
`,
);
const execution = await runNode(script, root);
expect(execution.code, execution.stderr).toBe(0);
const result = JSON.parse(execution.stdout) as {
readonly plain: number;
readonly hooked: number;
readonly observations: readonly {
readonly receiverIsEmitter: boolean;
readonly callbackName: string;
}[];
readonly hook_error: boolean;
};
expect(result.hook_error).toBe(false);
expect(result.plain).toBe(0);
expect(result.hooked).toBe(0);
expect(result.observations[0]).toEqual({
receiverIsEmitter: true,
callbackName: "original",
});
expect(result.observations[1]).toEqual(result.observations[0]);
});
it("forwards pre-hook on and once decorators exact registrations with an explicit gap", async () => {
const root = await createTestTempDirectory(
"rea-electron-hook-preinstalled-registration-decorators-",
);
temporary.push(root);
const source = electronModuleSource.replace(
"const ipcMain = new EventEmitter();",
`const ipcMain = new EventEmitter();
globalThis.__preHookRegistrationCalls = [];
const install = (emitter) => {
const nativeOn = EventEmitter.prototype.on;
const nativeOnce = EventEmitter.prototype.once;
emitter.on = function(channel, callback) {
globalThis.__preHookRegistrationCalls.push({
method: "on", receiverIsEmitter: this === emitter, callbackName: callback.name,
});
return Reflect.apply(nativeOn, this, arguments);
};
emitter.once = function(channel, callback) {
globalThis.__preHookRegistrationCalls.push({
method: "once", receiverIsEmitter: this === emitter, callbackName: callback.name,
});
return Reflect.apply(nativeOnce, this, arguments);
};
};
install(ipcMain);`,
);
await writeElectronFixture(root, source);
const script = join(root, "preinstalled-registration-decorators.cjs");
await writeFile(
script,
`const { EventEmitter } = require("node:events");
const { ipcMain } = require("electron");
const observations = globalThis.__preHookRegistrationCalls;
const decorate = (emitter) => {
const nativeOn = EventEmitter.prototype.on;
const nativeOnce = EventEmitter.prototype.once;
emitter.on = function(channel, callback) {
observations.push({ method: "on", receiverIsEmitter: this === emitter, callbackName: callback.name });
return Reflect.apply(nativeOn, this, arguments);
};
emitter.once = function(channel, callback) {
observations.push({ method: "once", receiverIsEmitter: this === emitter, callbackName: callback.name });
return Reflect.apply(nativeOnce, this, arguments);
};
return emitter;
};
const scenario = (emitter) => {
const calls = [];
const ordinary = function ordinary() { calls.push("ordinary"); };
const oneShot = function oneShot() { calls.push("oneShot"); };
emitter.on("ch", ordinary);
emitter.once("ch", oneShot);
emitter.emit("ch");
emitter.removeAllListeners("ch");
return calls;
};
const plain = scenario(decorate(new EventEmitter()));
const hooked = scenario(ipcMain);
const snapshot = globalThis.__reaElectronActiveSnapshot();
console.log(JSON.stringify({ plain, hooked, observations,
calls: snapshot.events.filter((event) => event.kind === "main-event-invocation"),
gaps: snapshot.events.filter((event) => event.kind === "ipc-observation-gap"),
hook_error: snapshot.hook_error }));
`,
);
const execution = await runNode(script, root);
expect(execution.code, execution.stderr).toBe(0);
const result = JSON.parse(execution.stdout) as {
readonly plain: readonly string[];
readonly hooked: readonly string[];
readonly observations: readonly {
readonly method: string;
readonly receiverIsEmitter: boolean;
readonly callbackName: string;
}[];
readonly calls: readonly unknown[];
readonly gaps: readonly {
readonly event: string;
readonly channel: string | null;
}[];
readonly hook_error: boolean;
};
expect(result.hook_error).toBe(false);
expect(result.plain).toEqual(["ordinary", "oneShot"]);
expect(result.hooked).toEqual(result.plain);
expect(result.observations).toEqual([
{ method: "on", receiverIsEmitter: true, callbackName: "ordinary" },
{ method: "once", receiverIsEmitter: true, callbackName: "oneShot" },
{
method: "on",
receiverIsEmitter: true,
callbackName: "bound onceWrapper",
},
{ method: "on", receiverIsEmitter: true, callbackName: "ordinary" },
{ method: "once", receiverIsEmitter: true, callbackName: "oneShot" },
{
method: "on",
receiverIsEmitter: true,
callbackName: "bound onceWrapper",
},
]);
expect(result.calls).toEqual([]);
expect(
result.gaps.filter((gap) => gap.event === "listener-registration"),
).toHaveLength(2);
expect(result.gaps.map((gap) => gap.channel)).toEqual(["ch", "ch"]);
});
it("forwards borrowed on and once registrations without attributing calls to ipcMain", async () => {
const root = await createTestTempDirectory(
"rea-electron-hook-borrowed-registration-",
);
temporary.push(root);
await writeElectronFixture(root);
const script = join(root, "borrowed-registration.cjs");
await writeFile(
script,
`const { EventEmitter } = require("node:events");
const { ipcMain } = require("electron");
const scenario = (hooked) => {
const target = new EventEmitter();
const registrations = [];
const calls = [];
const originalOn = hooked ? ipcMain.on : EventEmitter.prototype.on;
const originalOnce = hooked ? ipcMain.once : EventEmitter.prototype.once;
const onListener = function onListener() { calls.push("on"); };
const onceListener = function onceListener() { calls.push("once"); };
const foreign = function foreign() {};
EventEmitter.prototype.on.call(target, "newListener", (channel, callback) => {
if (channel === "on") registrations.push(callback === onListener);
if (channel === "once") registrations.push(callback === onceListener);
});
Reflect.apply(originalOn, target, ["on", onListener]);
target.rawListeners("on")[0].listener = foreign;
target.removeListener("on", onListener);
target.emit("on");
Reflect.apply(originalOnce, target, ["once", onceListener]);
target.rawListeners("once")[0].listener = foreign;
target.removeListener("once", onceListener);
target.emit("once");
return { registrations, calls, remaining: target.eventNames().map(String) };
};
const plain = scenario(false);
const hooked = scenario(true);
const invalid = (method, target) => {
try { Reflect.apply(target[method], null, ["invalid", () => {}]); return null; }
catch (cause) { return { name: cause.name, message: cause.message }; }
};
const failures = { plain: {}, hooked: {} };
for (const method of ["on", "once"]) {
failures.plain[method] = invalid(method, EventEmitter.prototype);
failures.hooked[method] = invalid(method, ipcMain);
}
const snapshot = globalThis.__reaElectronActiveSnapshot();
console.log(JSON.stringify({ plain, hooked, failures,
gaps: snapshot.events.filter((event) => event.kind === "ipc-observation-gap").map((event) => ({
channel: event.channel, event: event.event, target: event.target, error: event.error,
})),
invocations: snapshot.events.filter((event) => event.kind === "main-event-invocation").length,
hook_error: snapshot.hook_error }));
`,
);
const execution = await runNode(script, root);
expect(execution.code, execution.stderr).toBe(0);
const result = JSON.parse(execution.stdout);
expect(result.hook_error).toBe(false);
expect(result.hooked).toEqual(result.plain);
expect(result.plain).toEqual({
registrations: [true, true],
calls: ["once"],
remaining: ["newListener"],
});
expect(result.failures.hooked).toEqual(result.failures.plain);
expect(result.failures.plain.on.name).toBe("TypeError");
expect(result.failures.plain.once.name).toBe("TypeError");
expect(result.gaps).toEqual([
{
channel: "on",
event: "listener-registration",
target: "ipcMain.on",
error: false,
},
{
channel: "once",
event: "listener-registration",
target: "ipcMain.once",
error: false,
},
{
channel: "invalid",
event: "listener-registration",
target: "ipcMain.on",
error: true,
},
{
channel: "invalid",
event: "listener-registration",
target: "ipcMain.once",
error: true,
},
]);
expect(result.invocations).toBe(0);
});
it("does not inspect object event channels while recording registration gaps", async () => {
const root = await createTestTempDirectory(
"rea-electron-hook-object-channel-gap-",
);
temporary.push(root);
await writeElectronFixture(root);
const script = join(root, "object-channel-gap.cjs");
await writeFile(
script,
`const { EventEmitter } = require("node:events");
const { ipcMain } = require("electron");
const scenario = (hooked) => {
const target = new EventEmitter();
const effects = [];
const channel = {
[Symbol.toPrimitive](hint) { effects.push("coerce:" + hint); return "object-channel"; },
get toJSON() { effects.push("get:toJSON"); throw new Error("unexpected channel serialization"); },
};
const listener = function original() {};
const registration = hooked ? ipcMain.on : EventEmitter.prototype.on;
Reflect.apply(registration, target, [channel, listener]);
return { effects, eventNames: target.eventNames() };
};
const plain = scenario(false);
const hooked = scenario(true);
const snapshot = globalThis.__reaElectronActiveSnapshot();
console.log(JSON.stringify({ plain, hooked,
gaps: snapshot.events.filter((event) => event.kind === "ipc-observation-gap").map((event) => ({
channel: event.channel, event: event.event, target: event.target,
})), hook_error: snapshot.hook_error }));
`,
);
const execution = await runNode(script, root);
expect(execution.code, execution.stderr).toBe(0);
const result = JSON.parse(execution.stdout);
expect(result.hook_error).toBe(false);
expect(result.hooked).toEqual(result.plain);
expect(result.plain.effects).toEqual(["coerce:string", "coerce:string"]);
expect(result.plain.eventNames).toEqual(["object-channel"]);
expect(result.gaps).toEqual([
{ channel: null, event: "listener-registration", target: "ipcMain.on" },
]);
});
it("preserves borrowed removeAllListeners callback identity", async () => {
const root = await createTestTempDirectory(
"rea-electron-hook-borrowed-remove-all-identity-",
);
temporary.push(root);
await writeElectronFixture(root);
const script = join(root, "borrowed-remove-all-identity.cjs");
await writeFile(
script,
`const { EventEmitter } = require("node:events");
const { ipcMain } = require("electron");
const publicListener = (() => {
const listener = function original() {};
ipcMain.on("source", listener);
return ipcMain.rawListeners("source")[0];
})();
const makeForeignEmitter = () => {
const emitter = new EventEmitter();
const received = [];
emitter.on("removeListener", () => {});
emitter.on("ch", publicListener);
const nativeRemove = EventEmitter.prototype.removeListener;
emitter.removeListener = function(channel, callback) {
if (channel === "ch") {
received.push(callback);
if (callback !== publicListener) throw new Error("wrong removal callback");
}
return Reflect.apply(nativeRemove, this, arguments);
};
return { emitter, received };
};
const control = makeForeignEmitter();
const borrowed = makeForeignEmitter();
EventEmitter.prototype.removeAllListeners.call(control.emitter, "ch");
let failure = null;
try { Reflect.apply(ipcMain.removeAllListeners, borrowed.emitter, ["ch"]); }
catch (cause) { failure = cause.message; }
console.log(JSON.stringify({
controlReceivedPublic: control.received.length === 1 && control.received[0] === publicListener,
borrowedReceivedPublic: borrowed.received.length === 1 && borrowed.received[0] === publicListener,
controlRemaining: control.emitter.listenerCount("ch"),
borrowedRemaining: borrowed.emitter.listenerCount("ch"),
failure,
hook_error: globalThis.__reaElectronActiveSnapshot().hook_error,
}));
`,
);
const execution = await runNode(script, root);
expect(execution.code, execution.stderr).toBe(0);
expect(JSON.parse(execution.stdout)).toEqual({
controlReceivedPublic: true,
borrowedReceivedPublic: true,
controlRemaining: 0,
borrowedRemaining: 0,
failure: null,
hook_error: false,
});
});
it("does not coerce object channels before native listener removal", async () => {
const root = await createTestTempDirectory(
"rea-electron-hook-object-channel-",
);
temporary.push(root);
await writeElectronFixture(root);
const script = join(root, "object-channel.cjs");
await writeFile(
script,
`const { EventEmitter } = require("node:events");
const { ipcMain } = require("electron");
const scenario = (emitter) => {
let conversions = 0;
const listener = () => {};
emitter.on("ch", listener);
const rawListener = emitter.rawListeners("ch")[0];
rawListener.listener = () => {};
rawListener.listener = listener;
const channel = { [Symbol.toPrimitive]() { conversions++; return "ch"; } };
emitter.removeListener(channel, listener);
return { conversions, remaining: emitter.listenerCount("ch") };
};
console.log(JSON.stringify({ plain: scenario(new EventEmitter()), hooked: scenario(ipcMain),
hook_error: globalThis.__reaElectronActiveSnapshot().hook_error }));
`,
);
const execution = await runNode(script, root);
expect(execution.code, execution.stderr).toBe(0);
const result = JSON.parse(execution.stdout) as {
readonly plain: {
readonly conversions: number;
readonly remaining: number;
};
readonly hooked: {
readonly conversions: number;
readonly remaining: number;
};
readonly hook_error: boolean;
};
expect(result.hook_error).toBe(false);
expect(result.hooked).toEqual(result.plain);
expect(result.plain).toEqual({ conversions: 1, remaining: 0 });
});
it("removes observed ipcMain registrations after the public listener link is deleted", async () => {
const root = await createTestTempDirectory(
"rea-electron-hook-removal-deleted-link-",
);
temporary.push(root);
await writeElectronFixture(root);
const script = join(root, "removal-deleted-link.cjs");
await writeFile(
script,
`const { EventEmitter } = require("node:events");
const { ipcMain } = require("electron");
const scenario = (emitter) => {
const listener = () => {};
const removed = [];
const nativeRemove = emitter.removeListener;
let seenOriginal = false;
emitter.on("removeListener", (_channel, callback) => removed.push(callback === listener));
emitter.on("ch", listener);
delete emitter.rawListeners("ch")[0].listener;
emitter.removeListener = function(channel, callback) {
if (channel === "ch") seenOriginal = callback === listener;
return Reflect.apply(nativeRemove, this, arguments);
};
emitter.removeAllListeners("ch");
emitter.removeListener = nativeRemove;
return { seenOriginal, remaining: emitter.listenerCount("ch"), removed };
};
process.stdout.write(JSON.stringify({
plain: scenario(new EventEmitter()),
hooked: scenario(ipcMain),
hook_error: globalThis.__reaElectronActiveSnapshot().hook_error,
}) + "\\n");
`,
);
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 plain: {
readonly seenOriginal: boolean;
readonly remaining: number;
readonly removed: readonly boolean[];
};
readonly hooked: {
readonly seenOriginal: boolean;
readonly remaining: number;
readonly removed: readonly boolean[];
};
readonly hook_error: boolean;
};
expect(result.hook_error).toBe(false);
expect(result.hooked).toEqual(result.plain);
expect(result.plain).toEqual({
seenOriginal: true,
remaining: 0,
removed: [true],
});
});
it("passes canonical owned callbacks through post-hook removeAll decorators despite accessor metadata", async () => {
const root = await createTestTempDirectory(
"rea-electron-hook-remove-all-decorator-accessor-",
);
temporary.push(root);
await writeElectronFixture(root);
const script = join(root, "remove-all-decorator-accessor.cjs");
await writeFile(
script,
`const { EventEmitter } = require("node:events");
const { ipcMain } = require("electron");
const scenario = (emitter, hooked) => {
const listener = function original() {};
const removals = [];
let reads = 0;
emitter.on("removeListener", () => {});
emitter.on("ch", listener);
Object.defineProperty(emitter.rawListeners("ch")[0], "listener", { configurable: true, get() {
reads++;
throw new Error("metadata getter");
}});
const capturedRemove = hooked ? emitter.removeListener : EventEmitter.prototype.removeListener;
emitter.removeListener = function(channel, callback) {
if (channel === "ch") {
removals.push({ receiverIsEmitter: this === emitter, callbackIsOriginal: callback === listener });
if (callback !== listener) throw new Error("decorator received noncanonical callback");
}
return Reflect.apply(capturedRemove, this, arguments);
};
let failure = null;
try { emitter.removeAllListeners("ch"); } catch (cause) { failure = cause.message; }
return { reads, failure, remaining: emitter.listenerCount("ch"), removals };
};
const plain = scenario(new EventEmitter(), false);
const hooked = scenario(ipcMain, true);
console.log(JSON.stringify({ plain, hooked,
hook_error: globalThis.__reaElectronActiveSnapshot().hook_error }));
`,
);
const execution = await runNode(script, root);
expect(execution.code, execution.stderr).toBe(0);
const result = JSON.parse(execution.stdout) as {
readonly plain: {
readonly reads: number;
readonly failure: string | null;
readonly remaining: number;
readonly removals: readonly {
readonly receiverIsEmitter: boolean;
readonly callbackIsOriginal: boolean;
}[];
};
readonly hooked: {
readonly reads: number;
readonly failure: string | null;
readonly remaining: number;
readonly removals: readonly {
readonly receiverIsEmitter: boolean;
readonly callbackIsOriginal: boolean;
}[];
};
readonly hook_error: boolean;
};
expect(result.hook_error).toBe(false);
expect(result.hooked).toEqual(result.plain);
expect(result.plain).toEqual({
reads: 1,
failure: "metadata getter",
remaining: 0,
removals: [{ receiverIsEmitter: true, callbackIsOriginal: true }],
});
});
it.each(["removeListener", "off"] as const)(
"preserves direct %s cleanup for changed and deleted raw listener links",
async (method) => {
const root = await createTestTempDirectory(
"rea-electron-hook-removal-direct-link-",
);
temporary.push(root);
await writeElectronFixture(root);
const script = join(root, "removal-direct-link.cjs");
await writeFile(
script,
`const { EventEmitter } = require("node:events");
const { ipcMain } = require("electron");
const method = ${JSON.stringify(method)};
const scenario = (emitter, deletion) => {
const listener = () => {};
emitter.on("ch", listener);
const entry = emitter.rawListeners("ch")[0];
if (deletion) delete entry.listener;
else entry.listener = () => {};
const returned = emitter[method]("ch", listener);
return { remaining: emitter.listenerCount("ch"), returnedEmitter: returned === emitter };
};
const outcomes = {};
for (const deletion of [false, true]) {
const plain = new EventEmitter();
outcomes[deletion ? "deleted" : "changed"] = {
plain: scenario(plain, deletion),
hooked: scenario(ipcMain, deletion),
};
ipcMain.removeAllListeners();
}
process.stdout.write(JSON.stringify({
outcomes,
hook_error: globalThis.__reaElectronActiveSnapshot().hook_error,
}) + "\\n");
`,
);
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 outcomes: Record<
string,
{
readonly plain: {
readonly remaining: number;
readonly returnedEmitter: boolean;
};
readonly hooked: {
readonly remaining: number;
readonly returnedEmitter: boolean;
};
}
>;
readonly hook_error: boolean;
};
expect(result.hook_error).toBe(false);
expect(result.outcomes.changed?.hooked).toEqual(
result.outcomes.changed?.plain,
);
expect(result.outcomes.deleted?.hooked).toEqual(
result.outcomes.deleted?.plain,
);
expect(result.outcomes).toEqual({
changed: {
plain: { remaining: 0, returnedEmitter: true },
hooked: { remaining: 0, returnedEmitter: true },
},
deleted: {
plain: { remaining: 0, returnedEmitter: true },
hooked: { remaining: 0, returnedEmitter: true },
},
});
},
);
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"),
}),
);
});
};