Files
rea/tests/boundary/cli/electronWrappedCallPaths.test.ts
T
Kaoru0822 04fd41b288 fix(electron): bind each contextBridge helper invocation to its own API (#6284)
Wrapper substitution keyed contextBridge findings only by the inner
exposure call's range, so every helper invocation replaced the previous
one while unioning its members: the last key survived with every
invocation's members. Substituted members also listed only immediate
object keys, dropping nested paths that direct syntax retains.

Record substituted exposures per site and per world and key, and replace
the site's syntax observation once after traversal. Invocations exposing
one key at one site describe one API covering each alternative object; a
traced invocation whose key, object or world stays unresolved or guarded
keeps the site's own unresolved observation. Enumerate member paths
through resolved objects with the direct syntax depth bound, which also
retains the depth-limit unknown that substitution previously dropped.

Fixes #5871
Fixes #5870

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 01:29:39 -07:00

872 lines
31 KiB
TypeScript

import { expect } from "vitest";
import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import { z } from "zod";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { connectLocalToolsMcp } from "../../fixtures/localToolsMcp.js";
import { writeFixtureFiles } from "../../support/javascriptApplicationFixture.js";
import { cliTest, type TestCli } from "../../support/cli/cliFixture.js";
import { javascriptApplicationAnalysisResultSchema } from "../../../src/domain/javascript/javascriptApplicationAnalysis.js";
import { applicationFeatureTraceResultSchema } from "../../../src/domain/javascript/javascriptFeatureTraceSchemas.js";
import { analyzeJavaScriptDirectory } from "../../support/cli/javascriptApplication.js";
const sourcePositionSchema = z.object({ line: z.number(), column: z.number() });
const callPathConditionSchema = z.object({
expression: z.string(),
location: z.object({
start: sourcePositionSchema,
end: sourcePositionSchema,
}),
});
const callPathTerminalObservationSchema = z.object({
properties: z.object({ conditions: z.array(callPathConditionSchema) }),
evidence: z.object({ coverage: z.object({ status: z.string() }) }),
});
const restArrayPropertyCases = [
{
name: "leading-zero index",
expression: 'items["01"] ?? "FALLBACK"',
parameters: "",
channel: null,
},
{
name: "double-zero key",
expression: 'items["00"] ?? "FALLBACK"',
parameters: "",
channel: null,
},
{
name: "decimal-form key",
expression: 'items["1.0"] ?? "FALLBACK"',
parameters: "",
channel: null,
},
{
name: "canonical index",
expression: 'items["1"] ?? "FALLBACK"',
parameters: "",
channel: "SECOND",
},
{
name: "literal fallback",
expression: '"FALLBACK"',
parameters: "",
channel: "FALLBACK",
},
{
name: "dynamic key",
expression: 'items[key] ?? "FALLBACK"',
parameters: "key",
channel: null,
},
] as const;
for (const fixture of restArrayPropertyCases)
cliTest(
`preserves rest-array property identity for ${fixture.name} through the public CLI`,
async ({ cli }) => {
const root = await createTestTempDirectory("rea-electron-rest-key-");
await writeFixtureFiles(root, {
"main.cjs": `const e = require("electron");
function make(...items) {
return (${fixture.parameters}) => e.ipcRenderer.send(${fixture.expression});
}
e.contextBridge.exposeInMainWorld("desktop", { read: make("FIRST", "SECOND") });`,
});
const application = await analyzeJavaScriptDirectory("CLI", cli, root);
expect(application.statistics).toMatchObject({
parsed_javascript_files: 1,
parse_failures: 0,
});
const observations = application.graph.nodes.flatMap(
(node) => node.observations,
);
const terminals = observations.filter(
(observation) =>
observation.properties.entry === "desktop.read" &&
observation.properties.step_kind === "terminal",
);
const channels = application.graph.nodes
.filter((node) => node.kind === "ipc-channel")
.flatMap((node) => node.observations);
expect(
terminals.map((observation) => observation.properties.channel),
).toEqual(fixture.channel === null ? [] : [fixture.channel]);
expect(
channels
.filter(
(observation) => observation.properties.resolution === "literal",
)
.map((observation) => observation.properties.channel),
).toEqual(fixture.channel === null ? [] : [fixture.channel]);
for (const terminal of terminals)
expect(terminal).toMatchObject({
properties: { resolution: "candidate" },
evidence: {
state: "inferred",
authority: "static-relationship-inference",
},
});
if (fixture.expression !== '"FALLBACK"')
expect(channels).toContainEqual(
expect.objectContaining({
properties: expect.objectContaining({
channel: null,
resolution: "dynamic",
channel_expression: fixture.expression,
}),
evidence: expect.objectContaining({ state: "observed" }),
}),
);
},
);
const analyze = async (cli: TestCli, extra = "") => {
const root = await createTestTempDirectory("rea-electron-wrapped-paths-");
await writeFixtureFiles(root, {
"preload.js": `const e=require("electron");
const sender=channel=>(...args)=>e.ipcRenderer.send(channel,...args);
e.contextBridge.exposeInMainWorld("desktop",{copy:sender("test:copy"),cut:sender("test:cut")});`,
"main.js": `const e=require("electron");
function register(channel,handler){e.ipcMain.addListener(channel,event=>allowed(event)&&handler(event))}
register("test:copy",event=>event.sender.copy());
register("test:cut",event=>event.sender.cut());${extra}`,
});
const output = await cli.run({
arguments: ["analyze-javascript-application", root, "--json"],
});
expect(output.exitCode, output.stderr).toBe(0);
const { normalized_result: result } = z
.object({
normalized_result: javascriptApplicationAnalysisResultSchema,
})
.parse(output.json);
return { root, application: output.json, ...result };
};
cliTest(
"traces a wrapped bridge member through a unique IPC channel using the public CLI",
async ({ cli }) => {
const { root, application, graph } = await analyze(cli);
const byId = new Map(graph.nodes.map((n) => [n.node_id, n]));
const joins = graph.edges.filter(
(e) => e.evidence.extractor.operation === "join-electron-wrapper-paths",
);
expect(joins).toHaveLength(2);
for (const edge of joins) {
expect(edge.evidence).toMatchObject({
state: "inferred",
authority: "static-relationship-inference",
});
expect(edge.properties.pairing_basis).toBe(
"unique-exact-literal-channel",
);
expect(
byId.get(edge.source_node_id)?.observations[0]?.properties.channel,
).toBe(edge.properties.channel);
expect(
byId.get(edge.target_node_id)?.observations[0]?.properties.channel,
).toBe(edge.properties.channel);
expect(edge.properties.bridge_conditions).toEqual([]);
expect(edge.properties.handler_conditions).toEqual([
expect.objectContaining({ expression: "allowed(event)" }),
]);
}
const start = graph.nodes.find((n) =>
n.observations.some(
(o) =>
o.properties.entry === "desktop.copy" &&
o.properties.step_kind === "exposure",
),
);
expect(start).toBeDefined();
const visited = new Set<string>(),
pending = start ? [start.node_id] : [];
while (pending.length) {
const id = pending.pop();
if (!id || visited.has(id)) continue;
visited.add(id);
for (const edge of graph.edges)
if (edge.source_node_id === id) pending.push(edge.target_node_id);
}
expect(
[...visited].some((id) =>
byId
.get(id)
?.observations.some((o) => o.label === "ipc-event.sender.copy"),
),
).toBe(true);
expect(
[...visited].some((id) =>
byId
.get(id)
?.observations.some((o) => o.label === "ipc-event.sender.cut"),
),
).toBe(false);
const input = join(root, "trace-input.json");
await writeFile(
input,
JSON.stringify({
application,
seed: { kind: "api", value: "desktop.copy", match: "exact" },
direction: "outgoing",
}),
);
const traced = await cli.run({
arguments: ["trace-application-feature", input, "--json"],
});
expect(traced.exitCode).toBe(0);
const { normalized_result: trace } = z
.object({
normalized_result: applicationFeatureTraceResultSchema,
})
.parse(traced.json);
expect(trace.seed_matches).toHaveLength(1);
expect(
trace.paths.some(
(path) =>
path.contains_inference &&
path.node_ids.length > 8 &&
byId
.get(path.end_node_id)
?.observations.some((o) => o.label === "ipc-event.sender.copy"),
),
).toBe(true);
},
);
cliTest(
"preserves early-return predicates on recovered bridge paths through CLI and stdio MCP",
async ({ cli }) => {
const cases = [
{
name: "conditional early return",
body: 'if(x)return;e.contextBridge.exposeInMainWorld("api",{copy:()=>e.ipcRenderer.send("x")});',
conditions: ["!(x)"],
coverage: "partial",
},
{
name: "equivalent nested branch",
body: 'if(!x){e.contextBridge.exposeInMainWorld("api",{copy:()=>e.ipcRenderer.send("x")});}',
conditions: ["!x"],
coverage: "partial",
},
{
name: "definite early return",
body: 'return;e.contextBridge.exposeInMainWorld("api",{copy:()=>e.ipcRenderer.send("x")});',
conditions: [],
coverage: "complete",
},
{
name: "unguarded exposure",
body: 'e.contextBridge.exposeInMainWorld("api",{copy:()=>e.ipcRenderer.send("x")});',
conditions: [],
coverage: "complete",
},
] as const;
for (const surface of ["CLI", "stdio MCP"] as const) {
const connection =
surface === "stdio MCP" ? await connectLocalToolsMcp() : undefined;
const artifacts = new Set<string>();
for (const fixture of cases) {
const root = await createTestTempDirectory(
"rea-electron-early-return-",
);
await writeFixtureFiles(root, {
"index.js": `const e=require("electron");function f(x){${fixture.body}}f(globalThis.blocked);`,
});
const application = await analyzeJavaScriptDirectory(
surface,
cli,
root,
{ mcpCall: connection?.call },
);
expect(application.input_path).toBe(root);
expect(artifacts.has(application.root_artifact_sha256)).toBe(false);
artifacts.add(application.root_artifact_sha256);
const observations = application.graph.nodes.flatMap(
(node) => node.observations,
);
const terminal = observations.find(
(observation) =>
observation.properties.entry === "api.copy" &&
observation.properties.step_kind === "terminal",
);
if (fixture.name === "definite early return") {
expect(terminal).toBeUndefined();
continue;
}
const terminalEvidence =
callPathTerminalObservationSchema.parse(terminal);
expect(
terminalEvidence.properties.conditions.map(
({ expression }) => expression,
),
).toEqual(fixture.conditions);
expect(terminalEvidence.evidence.coverage.status).toBe(
fixture.coverage,
);
expect(application.graph.coverage.status).toBe(fixture.coverage);
const source = observations.find(
(observation) => observation.properties.file_kind === "javascript",
);
expect(source?.properties.parse_status).toBe(fixture.coverage);
}
}
},
);
cliTest(
"traces a nested contextBridge member through a unique IPC channel",
async ({ cli }) => {
const root = await createTestTempDirectory("rea-electron-nested-bridge-");
await writeFixtureFiles(root, {
"preload.js": `const { contextBridge, ipcRenderer } = require("electron");
contextBridge.exposeInMainWorld("api", {
nested: { write: (value) => ipcRenderer.invoke("nested:write", value) }
});`,
"main.js": `const { ipcMain, clipboard } = require("electron");
ipcMain.handle("nested:write", (event, value) => clipboard.writeText(value));`,
});
const output = await cli.run({
arguments: ["analyze-javascript-application", root, "--json"],
});
expect(output.exitCode, output.stderr).toBe(0);
const { normalized_result: application } = z
.object({ normalized_result: javascriptApplicationAnalysisResultSchema })
.parse(output.json);
const { graph } = application;
const paths = graph.nodes
.flatMap((node) => node.observations)
.filter(
(observation) =>
observation.properties.entry === "api.nested.write" &&
observation.properties.entry_kind === "bridge-member",
);
expect(
paths.some(
(observation) => observation.properties.step_kind === "exposure",
),
).toBe(true);
expect(
paths.some(
(observation) => observation.properties.step_kind === "terminal",
),
).toBe(true);
const joins = graph.edges.filter(
(edge) =>
edge.evidence.extractor.operation === "join-electron-wrapper-paths",
);
expect(joins.map((edge) => edge.properties.channel)).toEqual([
"nested:write",
]);
const traceInput = join(root, "trace-nested-input.json");
await writeFile(
traceInput,
JSON.stringify({
application: output.json,
seed: { kind: "api", value: "api.nested.write", match: "exact" },
direction: "outgoing",
}),
);
const traced = await cli.run({
arguments: ["trace-application-feature", traceInput, "--json"],
});
expect(traced.exitCode).toBe(0);
const { normalized_result: trace } = z
.object({ normalized_result: applicationFeatureTraceResultSchema })
.parse(traced.json);
expect(trace.seed_matches).toHaveLength(1);
expect(trace.paths.some((path) => path.contains_inference)).toBe(true);
},
);
cliTest(
"preserves exact bridge member segments through the public CLI",
async ({ cli }) => {
const root = await createTestTempDirectory("rea-electron-bridge-segments-");
await writeFixtureFiles(root, {
"preload.js": `const { contextBridge, ipcRenderer } = require("electron");
const write = () => ipcRenderer.send("write");
contextBridge.exposeInMainWorld("desktop", {
"nested.write": write,
nested: { write },
__proto__: { inherited: write },
["__proto__"]: { own: write },
});`,
"main.js": `const { ipcMain } = require("electron");
ipcMain.on("write", (event) => event.sender.copy());`,
});
const output = await cli.run({
arguments: ["analyze-javascript-application", root, "--json"],
});
expect(output.exitCode).toBe(0);
const { normalized_result: application } = z
.object({ normalized_result: javascriptApplicationAnalysisResultSchema })
.parse(output.json);
const observations = application.graph.nodes.flatMap((node) =>
node.observations.filter(
(observation) =>
observation.properties.entry_kind === "bridge-member" &&
observation.properties.step_kind === "exposure",
),
);
const bridge = application.graph.nodes
.filter((node) => node.kind === "context-bridge-api")
.flatMap((node) => node.observations)
.find((observation) => observation.properties.api_key === "desktop");
expect(bridge?.properties.members).not.toContain("__proto__.inherited");
expect(bridge?.properties.members).toContain("__proto__.own");
const paths = observations.map(
(observation) => observation.properties.entry_path,
);
expect(paths).toEqual(
expect.arrayContaining([
["desktop", "nested.write"],
["desktop", "nested", "write"],
["desktop", "__proto__", "own"],
]),
);
expect(paths).not.toContainEqual(["desktop", "__proto__", "inherited"]);
const handlerPaths = application.graph.nodes
.flatMap((node) => node.observations)
.filter(
(observation) => observation.properties.entry_kind === "ipc-handler",
);
expect(handlerPaths.length).toBeGreaterThan(0);
expect(
handlerPaths.every(
(observation) => !("entry_path" in observation.properties),
),
).toBe(true);
},
);
cliTest(
"retains a nested bridge path when its IPC handler has no external terminal",
async ({ cli }) => {
const root = await createTestTempDirectory("rea-electron-nested-return-");
await writeFixtureFiles(root, {
"preload.js": `const { contextBridge, ipcRenderer } = require("electron");
contextBridge.exposeInMainWorld("api", {
nested: { write: (value) => ipcRenderer.invoke("nested:write", value) }
});`,
"main.js": `const { ipcMain } = require("electron");
ipcMain.handle("nested:write", (event, value) => value);`,
});
const output = await cli.run({
arguments: ["analyze-javascript-application", root, "--json"],
});
expect(output.exitCode).toBe(0);
const { normalized_result: application } = z
.object({ normalized_result: javascriptApplicationAnalysisResultSchema })
.parse(output.json);
const { graph } = application;
expect(
graph.nodes.some((node) =>
node.observations.some(
(observation) =>
observation.properties.entry === "api.nested.write" &&
observation.properties.step_kind === "exposure",
),
),
).toBe(true);
expect(
graph.edges.some(
(edge) =>
edge.evidence.extractor.operation === "join-electron-wrapper-paths",
),
).toBe(false);
const traceInput = join(root, "trace-nested-input.json");
await writeFile(
traceInput,
JSON.stringify({
application: output.json,
seed: { kind: "api", value: "api.nested.write", match: "exact" },
direction: "outgoing",
}),
);
const traced = await cli.run({
arguments: ["trace-application-feature", traceInput, "--json"],
});
expect(traced.exitCode).toBe(0);
const { normalized_result: trace } = z
.object({ normalized_result: applicationFeatureTraceResultSchema })
.parse(traced.json);
expect(trace.seed_matches).toHaveLength(1);
expect(trace.graph?.nodes.length).toBeGreaterThan(1);
},
);
for (const surface of ["CLI", "stdio MCP"] as const) {
cliTest(
`retains explicit nested contextBridge members after an unknown spread through ${surface}`,
async ({ cli }) => {
const connection =
surface === "stdio MCP" ? await connectLocalToolsMcp() : undefined;
const analyzeBridge = async (api: string) => {
const root = await createTestTempDirectory(
"rea-electron-spread-bridge-members-",
);
await writeFixtureFiles(root, {
"preload.js": `const { contextBridge } = require("electron");
const extra = getExtra();
contextBridge.exposeInMainWorld("api", ${api});`,
});
const application = await analyzeJavaScriptDirectory(
surface,
cli,
root,
{ mcpCall: connection?.call },
);
expect(application.input_path).toBe(root);
return application;
};
const withSpread = await analyzeBridge("{ ...extra, a: { b: 1 }, c: 2 }");
const control = await analyzeBridge("{ a: { b: 1 }, c: 2 }");
expect(control.root_artifact_sha256).not.toBe(
withSpread.root_artifact_sha256,
);
const bridgeObservation = (application: typeof withSpread) =>
application.graph.nodes
.filter((node) => node.kind === "context-bridge-api")
.flatMap((node) => node.observations)
.find((observation) => observation.properties.api_key === "api");
expect(bridgeObservation(withSpread)).toMatchObject({
properties: {
api_status: "dynamic",
members: ["a", "a.b", "c"],
unknown_members: 1,
},
});
expect(bridgeObservation(control)).toMatchObject({
properties: {
api_status: "object-literal",
members: ["a", "a.b", "c"],
unknown_members: 0,
},
});
expect(withSpread.summary.exposed_api_members).toBe(3);
expect(control.summary.exposed_api_members).toBe(3);
},
);
cliTest(
`binds each contextBridge helper invocation's own key and members through ${surface}`,
async ({ cli }) => {
const root = await createTestTempDirectory(
"rea-electron-bridge-helper-calls-",
);
await writeFixtureFiles(root, {
"preload.js": `const { contextBridge, ipcRenderer } = require("electron");
function expose(key, api) { contextBridge.exposeInMainWorld(key, api); }
function relay(key, label, api) { expose(key, api); }
function named(label, key) { expose(key, { hidden: () => ipcRenderer.send("hidden") }); }
expose("one", { ping: () => ipcRenderer.send("one:ping") });
expose("two", { nested: { pong: () => ipcRenderer.send("two:pong") } });
relay("three", "label", { pang: () => ipcRenderer.send("three:pang") });
contextBridge.exposeInMainWorld("direct", { d: () => ipcRenderer.send("direct:d") });
named("label", globalThis.bridgeName);`,
});
const application = await analyzeJavaScriptDirectory(surface, cli, root);
const bridges = application.graph.nodes
.filter((node) => node.kind === "context-bridge-api")
.flatMap((node) => node.observations)
.map(({ properties }) => ({
api_key: properties.api_key,
api_key_expression: properties.api_key_expression,
members: properties.members,
unknown_members: properties.unknown_members,
resolution_basis: properties.resolution_basis,
}));
expect(bridges).toHaveLength(5);
expect(bridges).toEqual(
expect.arrayContaining([
{
api_key: "one",
api_key_expression: null,
members: ["ping"],
unknown_members: 0,
resolution_basis: "wrapper-substitution",
},
{
api_key: "two",
api_key_expression: null,
members: ["nested", "nested.pong"],
unknown_members: 0,
resolution_basis: "wrapper-substitution",
},
{
api_key: "three",
api_key_expression: null,
members: ["pang"],
unknown_members: 0,
resolution_basis: "wrapper-substitution",
},
{
api_key: "direct",
api_key_expression: null,
members: ["d"],
unknown_members: 0,
resolution_basis: "direct-syntax",
},
// The traced invocation with an unresolved key stays unknown rather
// than lending its members to a resolved key.
{
api_key: null,
api_key_expression: "key",
members: [],
unknown_members: 1,
resolution_basis: "direct-syntax",
},
]),
);
expect(application.summary.context_bridge_apis).toBe(5);
expect(application.summary.exposed_api_members).toBe(5);
const exposureEntries = new Set(
application.graph.nodes
.flatMap((node) => node.observations)
.filter(({ properties }) => properties.step_kind === "exposure")
.map(({ properties }) => properties.entry),
);
expect(exposureEntries).toEqual(
new Set(["one.ping", "two.nested.pong", "three.pang", "direct.d"]),
);
},
);
cliTest(
`preserves handler ambiguity and short-circuit factory paths through ${surface}`,
async ({ cli }) => {
const root = await createTestTempDirectory("rea-electron-branch-paths-");
await writeFixtureFiles(root, {
"preload.js": `const e=require("electron");
e.contextBridge.exposeInMainWorld("empty", {});
function choose(){
if(flag)return ()=>e.ipcRenderer.send("test:copy");
else return ()=>e.ipcRenderer.send("test:cut");
return ()=>e.ipcRenderer.send("test:dead");
}
e.contextBridge.exposeInMainWorld("desktop", {
choose:choose(),
ambiguous:()=>e.ipcRenderer.send("test:ambiguous"),
competing:()=>e.ipcRenderer.send("test:competing"),
conditional:()=>e.ipcRenderer.send("test:conditional"),
fallback:()=>e.ipcRenderer.send("test:fallback"),
});`,
"main.js": `const e=require("electron");
const copy=event=>event.sender.copy(), cut=event=>event.sender.cut();
function register(channel,handler){e.ipcMain.on(channel,handler)}
register("test:copy",copy);register("test:cut",cut);
register("test:ambiguous",flag?copy:cut);
register("test:competing",copy);
register("test:competing",flag?copy:unknownHandler);
if(flag) register("test:conditional",copy);
register("test:fallback",event=>{false ?? event.sender.copy()});`,
});
const { graph } = await analyzeJavaScriptDirectory(surface, cli, root);
const empty = graph.nodes
.filter((node) => node.kind === "context-bridge-api")
.flatMap((node) => node.observations)
.find((observation) => observation.properties.api_key === "empty");
expect(empty).toMatchObject({
properties: {
api_status: "object-literal",
members: [],
unknown_members: 0,
resolution_basis: "direct-syntax",
},
evidence: { state: "observed" },
});
const joins = graph.edges.filter(
(edge) =>
edge.evidence.extractor.operation === "join-electron-wrapper-paths",
);
expect(joins.map((edge) => edge.properties.channel).sort()).toEqual([
"test:copy",
"test:cut",
]);
const steps = graph.nodes
.flatMap((node) => node.observations)
.filter(
(observation) => observation.properties.step_kind === "terminal",
);
expect(
[
...new Set(
steps
.filter((step) => step.properties.entry === "desktop.choose")
.map((step) => step.properties.channel),
),
].sort(),
).toEqual(["test:copy", "test:cut"]);
for (const [channel, expression] of [
["test:copy", "flag"],
["test:cut", "!(flag)"],
])
expect(
steps.find(
(step) =>
step.properties.entry === "desktop.choose" &&
step.properties.channel === channel,
)?.properties.conditions,
).toEqual([expect.objectContaining({ expression })]);
expect(
steps.some(
(step) =>
step.properties.entry_kind === "ipc-handler" &&
["test:ambiguous", "test:fallback"].includes(
String(step.properties.channel),
),
),
).toBe(false);
},
);
}
cliTest(
"does not join a channel with another untraced compatible handler",
async ({ cli }) => {
const { graph } = await analyze(
cli,
'e.ipcMain.on("test:copy",event=>unresolved(event));',
);
const joins = graph.edges.filter(
(e) => e.evidence.extractor.operation === "join-electron-wrapper-paths",
);
expect(joins.map((e) => e.properties.channel)).toEqual(["test:cut"]);
},
);
cliTest(
"keeps shallow bridge members when a sibling exceeds the nesting limit",
async ({ cli }) => {
const root = await createTestTempDirectory("rea-electron-deep-bridge-");
let deepMember = '() => e.ipcRenderer.send("deep:run")';
for (let index = 0; index < 129; index += 1)
deepMember = `{ level${index}: ${deepMember} }`;
await writeFixtureFiles(root, {
"preload.js": `const e=require("electron");
e.contextBridge.exposeInMainWorld("desktop", {
deep: ${deepMember},
shallow: () => e.ipcRenderer.send("shallow:run")
});`,
"main.js": `const e=require("electron");
e.ipcMain.on("shallow:run", event => event.sender.copy());
e.ipcMain.on("deep:run", event => event.sender.copy());`,
});
const output = await cli.run({
arguments: ["analyze-javascript-application", root, "--json"],
});
expect(output.exitCode).toBe(0);
const { normalized_result: application } = z
.object({ normalized_result: javascriptApplicationAnalysisResultSchema })
.parse(output.json);
const { graph } = application;
const joins = graph.edges.filter(
(edge) =>
edge.evidence.extractor.operation === "join-electron-wrapper-paths",
);
expect(joins.map((edge) => edge.properties.channel)).toEqual([
"shallow:run",
]);
expect(
graph.nodes
.flatMap((node) => node.observations)
.some(
(observation) =>
observation.properties.entry === "desktop.shallow" &&
observation.properties.step_kind === "exposure",
),
).toBe(true);
},
);
cliTest(
"preserves empty bridge member segments in nested paths",
async ({ cli }) => {
const root = await createTestTempDirectory(
"rea-electron-empty-bridge-key-",
);
await writeFixtureFiles(root, {
"preload.js": `const { contextBridge, ipcRenderer } = require("electron");
contextBridge.exposeInMainWorld("desktop", {
"": { nested: { call: () => ipcRenderer.send("empty:key") } }
});`,
"main.js": `const { ipcMain } = require("electron");
ipcMain.on("empty:key", event => event.sender.copy());`,
});
const output = await cli.run({
arguments: ["analyze-javascript-application", root, "--json"],
});
expect(output.exitCode).toBe(0);
const { normalized_result: application } = z
.object({ normalized_result: javascriptApplicationAnalysisResultSchema })
.parse(output.json);
const observations = application.graph.nodes.flatMap(
(node) => node.observations,
);
expect(
observations.some(
(observation) =>
observation.properties.entry === "desktop..nested.call" &&
observation.properties.step_kind === "exposure",
),
).toBe(true);
expect(
application.graph.edges
.filter(
(edge) =>
edge.evidence.extractor.operation === "join-electron-wrapper-paths",
)
.map((edge) => edge.properties.channel),
).toEqual(["empty:key"]);
},
);
cliTest(
"retains bridge and handler conditions on Electron IPC join edges",
async ({ cli }) => {
const root = await createTestTempDirectory("rea-electron-join-conditions-");
await writeFixtureFiles(root, {
"preload.js": `const e=require("electron");
e.contextBridge.exposeInMainWorld("desktop", {
choose: flag
? () => e.ipcRenderer.send("test:copy")
: () => e.ipcRenderer.send("test:cut")
});`,
"main.js": `const e=require("electron");
if(!flag)e.ipcMain.on("test:copy",event=>allowed(event)&&event.sender.copy());
if(flag)e.ipcMain.on("test:cut",event=>allowed(event)&&event.sender.cut());`,
});
const { graph } = await analyzeJavaScriptDirectory("CLI", cli, root);
const joins = graph.edges.filter(
(edge) =>
edge.evidence.extractor.operation === "join-electron-wrapper-paths",
);
expect(joins).toHaveLength(2);
for (const [channel, bridgeCondition] of [
["test:copy", "flag"],
["test:cut", "!(flag)"],
]) {
const edge = joins.find(
(candidate) => candidate.properties.channel === channel,
);
expect(edge?.properties.bridge_conditions).toEqual([
expect.objectContaining({ expression: bridgeCondition }),
]);
expect(edge?.properties.handler_conditions).toEqual([
expect.objectContaining({
expression: channel === "test:copy" ? "!flag" : "flag",
}),
expect.objectContaining({ expression: "allowed(event)" }),
]);
}
},
);