mirror of
https://github.com/morluto/rea
synced 2026-10-27 22:45:19 -04:00
fix(electron): scope IPC pairing to application ownership
This commit is contained in:
@@ -43,49 +43,72 @@ export const collectElectronIpcRecords = (
|
||||
)
|
||||
.sort((left, right) => compareUnicodeCodePoints(left.key, right.key));
|
||||
|
||||
/** Match only exact literal channels and retain ambiguous matches separately. */
|
||||
export const classifyElectronIpcPairings = (
|
||||
records: readonly ElectronIpcRecord[],
|
||||
): readonly ElectronIpcPairingState[] => {
|
||||
const handlers = records.filter(isMainHandler);
|
||||
return records.filter(isPairableRendererTransmission).map((transmission) => {
|
||||
const candidates =
|
||||
transmission.finding.channel === null
|
||||
? []
|
||||
: handlers.filter(
|
||||
(handler) =>
|
||||
(handler.finding.channel === transmission.finding.channel ||
|
||||
(handler.finding.receiver !== undefined &&
|
||||
handler.finding.channel === null)) &&
|
||||
compatibleIpcModes(transmission.finding, handler.finding),
|
||||
);
|
||||
return {
|
||||
transmission,
|
||||
candidates,
|
||||
status:
|
||||
candidates.length === 1 && candidates[0]?.finding.receiver === undefined
|
||||
? "paired"
|
||||
: candidates.length > 0
|
||||
? "ambiguous"
|
||||
: "unpaired",
|
||||
};
|
||||
});
|
||||
};
|
||||
/** One interpretation shared by summary and every graph pairing consumer. */
|
||||
export interface ElectronIpcContext {
|
||||
readonly records: readonly ElectronIpcRecord[];
|
||||
readonly owners: ReadonlyMap<string, string | null>;
|
||||
readonly states: readonly ElectronIpcPairingState[];
|
||||
readonly pairings: readonly ElectronIpcPairing[];
|
||||
}
|
||||
|
||||
/** Return only pairings supported by one unique literal main handler. */
|
||||
export const unambiguousElectronIpcPairings = (
|
||||
records: readonly ElectronIpcRecord[],
|
||||
): readonly ElectronIpcPairing[] =>
|
||||
classifyElectronIpcPairings(records).flatMap((state) => {
|
||||
/** Equal channel text only establishes a pairing within one proven application owner. */
|
||||
export const createElectronIpcContext = (
|
||||
analysis: JavaScriptArtifactAnalysis,
|
||||
applicationOwner: (file: JavaScriptArtifactFile) => string | null,
|
||||
): ElectronIpcContext => {
|
||||
const records = collectElectronIpcRecords(analysis);
|
||||
const owners = new Map(
|
||||
records.map((record) => [record.key, applicationOwner(record.file)]),
|
||||
);
|
||||
const handlers = records.filter(isMainHandler);
|
||||
const states: ElectronIpcPairingState[] = records
|
||||
.filter(isPairableRendererTransmission)
|
||||
.map((transmission) => {
|
||||
const owner = owners.get(transmission.key);
|
||||
const matching =
|
||||
transmission.finding.channel === null
|
||||
? []
|
||||
: handlers.filter(
|
||||
(handler) =>
|
||||
(handler.finding.channel === transmission.finding.channel ||
|
||||
(handler.finding.receiver !== undefined &&
|
||||
handler.finding.channel === null)) &&
|
||||
compatibleIpcModes(transmission.finding, handler.finding),
|
||||
);
|
||||
const candidates = matching.filter(
|
||||
(handler) => owners.get(handler.key) === owner,
|
||||
);
|
||||
// Unknown ownership can conceal competing dispatch contexts. Different owners
|
||||
// do not prove runtime isolation, so retain ambiguity rather than absence.
|
||||
const uncertain =
|
||||
owner == null ||
|
||||
matching.some((handler) => owners.get(handler.key) == null);
|
||||
return {
|
||||
transmission,
|
||||
candidates,
|
||||
status:
|
||||
!uncertain &&
|
||||
candidates.length === 1 &&
|
||||
candidates[0]?.finding.receiver === undefined
|
||||
? "paired"
|
||||
: uncertain || matching.length > 0
|
||||
? "ambiguous"
|
||||
: "unpaired",
|
||||
};
|
||||
});
|
||||
const pairings = states.flatMap((state) => {
|
||||
const handler = state.candidates[0];
|
||||
return state.status === "paired" && handler !== undefined
|
||||
? [{ transmission: state.transmission, handler }]
|
||||
: [];
|
||||
});
|
||||
return { records, owners, states, pairings };
|
||||
};
|
||||
|
||||
/** Summarize static Electron findings without treating gaps as absence. */
|
||||
export const summarizeElectronBoundaries = (
|
||||
analysis: JavaScriptArtifactAnalysis,
|
||||
ipcContext: ElectronIpcContext,
|
||||
directoryPaths?: ReadonlySet<string>,
|
||||
): ElectronBoundarySummary => {
|
||||
const files = new Map(analysis.files.map(({ file }) => [file.path, file]));
|
||||
@@ -113,8 +136,8 @@ export const summarizeElectronBoundaries = (
|
||||
finding,
|
||||
})),
|
||||
);
|
||||
const ipc = collectElectronIpcRecords(analysis);
|
||||
const pairing = classifyElectronIpcPairings(ipc);
|
||||
const ipc = ipcContext.records;
|
||||
const pairing = ipcContext.states;
|
||||
const literalChannels = new Set(
|
||||
ipc.flatMap(({ finding }) =>
|
||||
finding.channel === null ? [] : [finding.channel],
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { ElectronIpcContext } from "./ElectronBoundaryAnalysis.js";
|
||||
import type { JavaScriptArtifactGraphContext } from "./JavaScriptArtifactGraphContext.js";
|
||||
import { addElectronIpcBoundaries } from "./ElectronBoundaryGraphIpc.js";
|
||||
import { addElectronNativeBoundaries } from "./ElectronBoundaryGraphNative.js";
|
||||
@@ -7,9 +8,10 @@ import { addElectronCallPaths } from "./ElectronBoundaryGraphCallPaths.js";
|
||||
/** Add every static Electron process and security-boundary fact to the JAG. */
|
||||
export const addElectronBoundaries = (
|
||||
context: JavaScriptArtifactGraphContext,
|
||||
ipc: ElectronIpcContext,
|
||||
): void => {
|
||||
addElectronWindowBoundaries(context);
|
||||
addElectronIpcBoundaries(context);
|
||||
addElectronIpcBoundaries(context, ipc);
|
||||
addElectronNativeBoundaries(context);
|
||||
addElectronCallPaths(context);
|
||||
addElectronCallPaths(context, ipc);
|
||||
};
|
||||
|
||||
@@ -3,10 +3,9 @@ import type { ApplicationNode } from "../../domain/javascript/javascriptApplicat
|
||||
import type { ElectronCallPath } from "../../domain/javascript/electronStaticAnalysisTypes.js";
|
||||
import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptArtifactFiles.js";
|
||||
import { sha256Text } from "../../domain/javascript/javascriptStaticAnalysisHelpers.js";
|
||||
import {
|
||||
collectElectronIpcRecords,
|
||||
unambiguousElectronIpcPairings,
|
||||
type ElectronIpcRecord,
|
||||
import type {
|
||||
ElectronIpcContext,
|
||||
ElectronIpcRecord,
|
||||
} from "./ElectronBoundaryAnalysis.js";
|
||||
import {
|
||||
addElectronInferenceEdge,
|
||||
@@ -30,13 +29,14 @@ interface ProjectedPath {
|
||||
/** Retain context-specific wrapper paths, then join only unique IPC records. */
|
||||
export const addElectronCallPaths = (
|
||||
context: JavaScriptArtifactGraphContext,
|
||||
ipc: ElectronIpcContext,
|
||||
): void => {
|
||||
const projected: ProjectedPath[] = [];
|
||||
for (const { file, javascript } of context.analysis.files) {
|
||||
if (javascript === null) continue;
|
||||
const coverage = javascriptAnalysisCoverage(javascript);
|
||||
for (const path of javascript.electron.call_paths ?? []) {
|
||||
const pathId = sha256Text(JSON.stringify(path));
|
||||
const pathId = sha256Text(JSON.stringify([file.path, path]));
|
||||
const nodes = path.steps.map((step, index) =>
|
||||
context.accumulator.addNode({
|
||||
kind: "javascript-expression",
|
||||
@@ -137,9 +137,7 @@ export const addElectronCallPaths = (
|
||||
projected.push({ file, path, coverage, nodes });
|
||||
}
|
||||
}
|
||||
for (const { transmission, handler } of unambiguousElectronIpcPairings(
|
||||
collectElectronIpcRecords(context.analysis),
|
||||
)) {
|
||||
for (const { transmission, handler } of ipc.pairings) {
|
||||
const sources = projected.filter(
|
||||
(p) => p.path.entry_kind === "bridge-member" && matches(p, transmission),
|
||||
);
|
||||
|
||||
@@ -15,10 +15,9 @@ import {
|
||||
astObservationEvidence,
|
||||
staticInferenceEvidence,
|
||||
} from "./JavaScriptArtifactGraphEvidence.js";
|
||||
import {
|
||||
collectElectronIpcRecords,
|
||||
unambiguousElectronIpcPairings,
|
||||
type ElectronIpcRecord,
|
||||
import type {
|
||||
ElectronIpcContext,
|
||||
ElectronIpcRecord,
|
||||
} from "./ElectronBoundaryAnalysis.js";
|
||||
import {
|
||||
addElectronAstEdge,
|
||||
@@ -30,6 +29,7 @@ import {
|
||||
interface IpcGraphState {
|
||||
readonly context: JavaScriptArtifactGraphContext;
|
||||
readonly records: readonly ElectronIpcRecord[];
|
||||
readonly ipc: ElectronIpcContext;
|
||||
readonly channels: Map<string, ApplicationNode>;
|
||||
readonly handlers: Map<string, ApplicationNode>;
|
||||
readonly sources: Map<string, ApplicationNode>;
|
||||
@@ -54,10 +54,12 @@ interface ValidationObservationInput {
|
||||
/** Project IPC syntax, handler locations, validations, and exact pairings. */
|
||||
export const addElectronIpcBoundaries = (
|
||||
context: JavaScriptArtifactGraphContext,
|
||||
ipc: ElectronIpcContext,
|
||||
): void => {
|
||||
const state: IpcGraphState = {
|
||||
context,
|
||||
records: collectElectronIpcRecords(context.analysis),
|
||||
records: ipc.records,
|
||||
ipc,
|
||||
channels: new Map(),
|
||||
handlers: new Map(),
|
||||
sources: new Map(),
|
||||
@@ -82,7 +84,12 @@ const addIpcRecord = (
|
||||
);
|
||||
if (source === undefined) return;
|
||||
state.sources.set(record.key, source);
|
||||
const channel = addChannelNode(context, record, coverage);
|
||||
const channel = addChannelNode(
|
||||
context,
|
||||
record,
|
||||
coverage,
|
||||
state.ipc.owners.get(record.key) ?? null,
|
||||
);
|
||||
state.channels.set(record.key, channel);
|
||||
if (record.finding.mode === "listen" || record.finding.mode === "handle") {
|
||||
const handler = addHandlerNode({
|
||||
@@ -117,6 +124,7 @@ const addChannelNode = (
|
||||
context: JavaScriptArtifactGraphContext,
|
||||
record: ElectronIpcRecord,
|
||||
coverage: JavaScriptArtifactGraphCoverage,
|
||||
owner: string | null,
|
||||
): ApplicationNode => {
|
||||
const { finding, file } = record;
|
||||
const channelKey =
|
||||
@@ -125,7 +133,10 @@ const addChannelNode = (
|
||||
: `literal:${finding.channel}`;
|
||||
const identityKey =
|
||||
finding.receiver === undefined
|
||||
? channelKey
|
||||
? JSON.stringify([
|
||||
owner === null ? ["unknown", file.path] : ["application", owner],
|
||||
channelKey,
|
||||
])
|
||||
: JSON.stringify([
|
||||
file.path,
|
||||
file.sha256,
|
||||
@@ -204,7 +215,7 @@ const addHandlerNode = (input: IpcHandlerInput): ApplicationNode => {
|
||||
const handlerKey = `${finding.side}:${finding.operation}:${finding.channel ?? finding.channel_expression}:${javascriptSourceRangeKey(handlerRange)}`;
|
||||
const identityKey =
|
||||
finding.receiver === undefined
|
||||
? handlerKey
|
||||
? JSON.stringify([file.path, handlerKey])
|
||||
: JSON.stringify([
|
||||
file.path,
|
||||
finding.module_runtime ?? null,
|
||||
@@ -280,7 +291,7 @@ const addHandlerNode = (input: IpcHandlerInput): ApplicationNode => {
|
||||
};
|
||||
|
||||
const addLiteralPairings = (state: IpcGraphState): void => {
|
||||
for (const pairing of unambiguousElectronIpcPairings(state.records)) {
|
||||
for (const pairing of state.ipc.pairings) {
|
||||
const source = state.sources.get(pairing.transmission.key);
|
||||
const handler = state.handlers.get(pairing.handler.key);
|
||||
const coverage = coverageFor(state.context, pairing.transmission.file);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { ElectronBoundarySummary } from "../../domain/javascript/javascriptApplicationAnalysis.js";
|
||||
import { ELECTRON_IDENTITY_LIMITATION } from "../../domain/javascript/javascriptElectronMemberWrites.js";
|
||||
import type { ArtifactInventorySnapshot } from "../../domain/artifactInventorySnapshot.js";
|
||||
import {
|
||||
@@ -13,6 +14,7 @@ import {
|
||||
selfReferenceOmissions,
|
||||
dependencyManifest,
|
||||
applicationContainerIdentity,
|
||||
applicationRootPath,
|
||||
type JavaScriptArtifactGraphContext,
|
||||
} from "./JavaScriptArtifactGraphContext.js";
|
||||
import {
|
||||
@@ -45,8 +47,9 @@ import {
|
||||
import { addJavaScriptBundlerNodes } from "./JavaScriptArtifactGraphBundlers.js";
|
||||
import { addElectronBoundaries } from "./ElectronBoundaryGraph.js";
|
||||
import {
|
||||
classifyElectronIpcPairings,
|
||||
collectElectronIpcRecords,
|
||||
createElectronIpcContext,
|
||||
summarizeElectronBoundaries,
|
||||
type ElectronIpcContext,
|
||||
} from "./ElectronBoundaryAnalysis.js";
|
||||
|
||||
/**
|
||||
@@ -58,21 +61,33 @@ export function* buildImmutableJavaScriptArtifactGraphSteps(
|
||||
snapshot: ArtifactInventorySnapshot,
|
||||
fileSet: JavaScriptArtifactFileSet,
|
||||
analysis: JavaScriptArtifactAnalysis,
|
||||
): Generator<void, JavaScriptApplicationGraph> {
|
||||
): Generator<
|
||||
void,
|
||||
{
|
||||
readonly graph: JavaScriptApplicationGraph;
|
||||
readonly electronSummary: ElectronBoundarySummary;
|
||||
}
|
||||
> {
|
||||
const input = yield* buildJavaScriptArtifactGraphInputSteps(
|
||||
snapshot,
|
||||
fileSet,
|
||||
analysis,
|
||||
);
|
||||
// The input was built here and is not retained, so it can be transferred.
|
||||
return yield* sealTransferredJavaScriptApplicationGraphSteps(input);
|
||||
return {
|
||||
graph: yield* sealTransferredJavaScriptApplicationGraphSteps(input.graph),
|
||||
electronSummary: input.electronSummary,
|
||||
};
|
||||
}
|
||||
|
||||
function* buildJavaScriptArtifactGraphInputSteps(
|
||||
snapshot: ArtifactInventorySnapshot,
|
||||
fileSet: JavaScriptArtifactFileSet,
|
||||
analysis: JavaScriptArtifactAnalysis,
|
||||
): Generator<void, unknown> {
|
||||
): Generator<
|
||||
void,
|
||||
{ readonly graph: unknown; readonly electronSummary: ElectronBoundarySummary }
|
||||
> {
|
||||
const accumulator = new JavaScriptArtifactGraphAccumulator();
|
||||
const root = createJavaScriptArtifactRootNode(accumulator, snapshot);
|
||||
const parsedPackages = new Set(
|
||||
@@ -183,7 +198,10 @@ function* buildJavaScriptArtifactGraphInputSteps(
|
||||
yield* addJavaScriptModuleRelationshipsSteps(projectionContext);
|
||||
const findingLimitations =
|
||||
yield* addJavaScriptStaticFindingsSteps(projectionContext);
|
||||
addElectronBoundaries(projectionContext);
|
||||
const ipc = createElectronIpcContext(analysis, (file) =>
|
||||
applicationRootPath(projectionContext, file),
|
||||
);
|
||||
addElectronBoundaries(projectionContext, ipc);
|
||||
yield;
|
||||
addJavaScriptHtmlRoles(projectionContext);
|
||||
yield;
|
||||
@@ -191,19 +209,31 @@ function* buildJavaScriptArtifactGraphInputSteps(
|
||||
yield;
|
||||
const coverage = graphCoverage(context);
|
||||
return {
|
||||
schema: "JavaScriptApplicationGraph",
|
||||
root_node_ids:
|
||||
packageRoots.length === 0
|
||||
? [root.node_id]
|
||||
: packageRoots.map(({ node_id: id }) => id),
|
||||
nodes: accumulator.nodes(),
|
||||
edges: accumulator.edges(),
|
||||
coverage,
|
||||
limitations: [
|
||||
...bundlerLimitations,
|
||||
...findingLimitations,
|
||||
...graphLimitations(context, coverage.status, relationshipOmissions),
|
||||
],
|
||||
electronSummary: summarizeElectronBoundaries(
|
||||
analysis,
|
||||
ipc,
|
||||
context.directoryPaths,
|
||||
),
|
||||
graph: {
|
||||
schema: "JavaScriptApplicationGraph",
|
||||
root_node_ids:
|
||||
packageRoots.length === 0
|
||||
? [root.node_id]
|
||||
: packageRoots.map(({ node_id: id }) => id),
|
||||
nodes: accumulator.nodes(),
|
||||
edges: accumulator.edges(),
|
||||
coverage,
|
||||
limitations: [
|
||||
...bundlerLimitations,
|
||||
...findingLimitations,
|
||||
...graphLimitations(
|
||||
context,
|
||||
coverage.status,
|
||||
relationshipOmissions,
|
||||
ipc,
|
||||
),
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -295,9 +325,10 @@ const graphLimitations = (
|
||||
context: JavaScriptArtifactGraphContext,
|
||||
coverage: "complete" | "partial" | "unknown" | "unavailable",
|
||||
relationshipOmissions: JavaScriptModuleRelationshipOmissions,
|
||||
ipcContext: ElectronIpcContext,
|
||||
): string[] => {
|
||||
const ipc = collectElectronIpcRecords(context.analysis);
|
||||
const pairings = classifyElectronIpcPairings(ipc);
|
||||
const ipc = ipcContext.records;
|
||||
const pairings = ipcContext.states;
|
||||
const electronFindings = context.analysis.files.reduce(
|
||||
(count, { javascript }) =>
|
||||
count +
|
||||
@@ -348,7 +379,7 @@ const graphLimitations = (
|
||||
: []),
|
||||
...(pairings.some(({ status }) => status === "ambiguous")
|
||||
? [
|
||||
"Some literal IPC channels have multiple compatible main handlers; ambiguous calls are not paired to any handler.",
|
||||
"Some IPC transmissions have competing handlers or unresolved application ownership; ambiguous calls are not paired to any handler.",
|
||||
]
|
||||
: []),
|
||||
...(context.analysis.files.some(
|
||||
|
||||
@@ -46,7 +46,6 @@ import { readJavaScriptArtifactFiles } from "../../artifacts/javascript/JavaScri
|
||||
import { buildImmutableJavaScriptArtifactGraphSteps } from "./JavaScriptArtifactGraphBuilder.js";
|
||||
import { scanCanonicalArtifactInventoryInScope } from "../../artifacts/inventory/scanCanonical.js";
|
||||
import { createJavaScriptNextAppContext } from "./JavaScriptNextAppContext.js";
|
||||
import { summarizeElectronBoundaries } from "./ElectronBoundaryAnalysis.js";
|
||||
import { createJavaScriptSemanticGraphProjection } from "./JavaScriptSemanticGraphBuilder.js";
|
||||
import type { ProgressReporter } from "../ProgressReporter.js";
|
||||
import {
|
||||
@@ -281,7 +280,7 @@ export const reconstructJavaScriptArtifact = async (
|
||||
"seal_javascript_application_graph",
|
||||
"Sealing the validated application graph",
|
||||
);
|
||||
const graph = await completeJavaScriptAnalysisSteps(
|
||||
const { graph, electronSummary } = await completeJavaScriptAnalysisSteps(
|
||||
applicationGraphSteps,
|
||||
);
|
||||
await reportCompletedPhase(
|
||||
@@ -366,7 +365,7 @@ export const reconstructJavaScriptArtifact = async (
|
||||
: { source_selection: files.source_selection }),
|
||||
graph,
|
||||
semantic_graph: semanticGraph,
|
||||
electron_summary: summarizeElectronBoundaries(analysis, directoryPaths),
|
||||
electron_summary: electronSummary,
|
||||
statistics: {
|
||||
relevant_files:
|
||||
files.files.length + files.unavailable_relevant_files.length,
|
||||
|
||||
@@ -112,3 +112,119 @@ cliTest.for(["CLI", "stdio MCP"] as const)(
|
||||
).toEqual([]);
|
||||
},
|
||||
);
|
||||
|
||||
cliTest.for(["CLI", "stdio MCP"] as const)(
|
||||
"pairs literal IPC within proven application owners through %s",
|
||||
async (surface, { cli }) => {
|
||||
const root = await createTestTempDirectory("rea-application-ipc-");
|
||||
await writeFixtureFiles(root, {
|
||||
"a/package.json": JSON.stringify({ name: "a", main: "main.cjs" }),
|
||||
"a/main.cjs": `const { ipcMain, clipboard } = require("electron");
|
||||
ipcMain.handle("shared", (event, value) => clipboard.writeText(value));`,
|
||||
"a/preload.cjs": `const { ipcRenderer, contextBridge } = require("electron");
|
||||
contextBridge.exposeInMainWorld("api", { read: (value) => ipcRenderer.invoke("shared", value) });`,
|
||||
"b/package.json": JSON.stringify({ name: "b", main: "main.cjs" }),
|
||||
"b/main.cjs": `const { ipcMain, clipboard } = require("electron");
|
||||
ipcMain.handle("shared", (event, value) => clipboard.writeText(value));`,
|
||||
"b/preload.cjs": `const { ipcRenderer, contextBridge } = require("electron");
|
||||
contextBridge.exposeInMainWorld("api", { read: (value) => ipcRenderer.invoke("shared", value) });`,
|
||||
});
|
||||
const application = await analyzeJavaScriptDirectory(surface, cli, root);
|
||||
expect(application.summary.ipc).toMatchObject({
|
||||
operations: 4,
|
||||
paired_renderer_transmissions: 2,
|
||||
ambiguous_renderer_transmissions: 0,
|
||||
});
|
||||
const pairs = application.graph.edges.filter(
|
||||
({ evidence }) =>
|
||||
evidence.extractor.operation === "pair-ipc-literal-channel",
|
||||
);
|
||||
const joins = application.graph.edges.filter(
|
||||
({ evidence }) =>
|
||||
evidence.extractor.operation === "join-electron-wrapper-paths",
|
||||
);
|
||||
expect(joins).toHaveLength(2);
|
||||
expect(
|
||||
new Set(joins.map(({ target_node_id }) => target_node_id)).size,
|
||||
).toBe(2);
|
||||
const nodes = new Map(
|
||||
application.graph.nodes.map((node) => [node.node_id, node]),
|
||||
);
|
||||
for (const join of joins) {
|
||||
const target = nodes.get(join.target_node_id);
|
||||
expect(target?.observations).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
evidence: expect.objectContaining({
|
||||
location: expect.objectContaining({
|
||||
available: true,
|
||||
value: expect.objectContaining({
|
||||
source: join.properties.handler_path,
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
);
|
||||
}
|
||||
expect(pairs).toHaveLength(2);
|
||||
expect(
|
||||
new Set(pairs.map(({ target_node_id }) => target_node_id)).size,
|
||||
).toBe(2);
|
||||
expect(
|
||||
application.graph.nodes.filter(({ kind }) => kind === "ipc-handler"),
|
||||
).toHaveLength(2);
|
||||
for (const edge of pairs) {
|
||||
expect(edge.evidence.location.available).toBe(true);
|
||||
const location = edge.evidence.location;
|
||||
if (!location.available || location.value.kind !== "source-range")
|
||||
throw new Error("IPC pairing omitted its authored source location");
|
||||
expect(edge.properties.handler_path).toBe(
|
||||
location.value.source.startsWith("a/") ? "a/main.cjs" : "b/main.cjs",
|
||||
);
|
||||
}
|
||||
expect(
|
||||
application.graph.nodes.filter(({ kind }) => kind === "ipc-channel"),
|
||||
).toHaveLength(2);
|
||||
},
|
||||
);
|
||||
|
||||
cliTest.for(["CLI", "stdio MCP"] as const)(
|
||||
"retains unknown IPC applicability across distinct or shared application owners through %s",
|
||||
async (surface, { cli }) => {
|
||||
for (const sharedSource of [false, true]) {
|
||||
const root = await createTestTempDirectory(
|
||||
"rea-unknown-application-ipc-",
|
||||
);
|
||||
await writeFixtureFiles(root, {
|
||||
"a/package.json": JSON.stringify({ name: "a", main: "main.cjs" }),
|
||||
"a/main.cjs": sharedSource
|
||||
? `require("../shared.cjs");`
|
||||
: `module.exports = {};`,
|
||||
"b/package.json": JSON.stringify({ name: "b", main: "main.cjs" }),
|
||||
"b/main.cjs": `const { ipcMain } = require("electron");
|
||||
${sharedSource ? 'require("../shared.cjs");' : ""}
|
||||
ipcMain.handle("shared", () => "result");`,
|
||||
[sharedSource ? "shared.cjs" : "a/preload.cjs"]:
|
||||
`const { ipcRenderer } = require("electron"); ipcRenderer.invoke("shared");`,
|
||||
});
|
||||
const application = await analyzeJavaScriptDirectory(surface, cli, root);
|
||||
expect(application.summary.ipc).toMatchObject({
|
||||
operations: 2,
|
||||
paired_renderer_transmissions: 0,
|
||||
ambiguous_renderer_transmissions: 1,
|
||||
unpaired_literal_renderer_transmissions: 0,
|
||||
});
|
||||
expect(
|
||||
application.graph.edges.filter(({ evidence }) =>
|
||||
["pair-ipc-literal-channel", "join-electron-wrapper-paths"].includes(
|
||||
evidence.extractor.operation,
|
||||
),
|
||||
),
|
||||
).toEqual([]);
|
||||
expect(
|
||||
application.graph.nodes.filter(({ kind }) => kind === "ipc-handler"),
|
||||
).toHaveLength(1);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
+1
-1
@@ -8,4 +8,4 @@ export const immutableArtifactGraph = (
|
||||
): Promise<JavaScriptApplicationGraph> =>
|
||||
completeJavaScriptAnalysisSteps(
|
||||
buildImmutableJavaScriptArtifactGraphSteps(...args),
|
||||
);
|
||||
).then(({ graph }) => graph);
|
||||
|
||||
Reference in New Issue
Block a user