Files
rea/tests/boundary/filesystem/javascript-applications/electronApplicationAnalysis.test.ts
T
morluto b58adafa4f fix(javascript): align module lookup and source evidence
Preserve CommonJS literal paths and confined ESM URL semantics, including package main fallback, directory intent, Unicode identity, and present opaque modules. Parse extraction inputs at their AST/source owner, retain skipped HTML and source-map coverage, and distinguish endpoint candidates from proven receiver facts. Version structural fingerprints with computed literal contents and propagate them through worker contracts.

Reviewed fixes include #1810, #1840, #2642, #2700, #2702, #2714, #2803, #2960, #2968, #2986, #3035, and #3036.

Validation: 158 resolver tests passed; public CLI/MCP module lookup controls, package-main/Unicode workflows, and six builtin workflows passed in the current compiled lane. Worker schema/fingerprint regressions passed. Large public pdf.js completion and real Electron analysis remain unverified.
2026-10-14 08:46:50 +08:00

658 lines
21 KiB
TypeScript

import { mkdir, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
import { createTestTempDirectory } from "../../../fixtures/temporaryDirectory.js";
import { analyzeJavaScriptApplication } from "../../../support/javascriptApplicationScope.js";
import { reconstructJavaScriptArtifact } from "../../../support/javascriptApplicationScope.js";
import { parseEvidence } from "../../../../src/domain/evidence.js";
import { parseJavaScriptApplicationGraph } from "../../../../src/domain/javascript/javascriptApplicationGraph.js";
import { analyzeJavaScriptApplicationInputSchema } from "../../../../src/domain/javascript/javascriptApplicationAnalysis.js";
import { writeElectronBoundaryFixture } from "../../../fixtures/electronBoundaryApplication.js";
describe("static Electron application analysis", () => {
it("accepts relative paths and rejects caller-selected analysis bounds", () => {
expect(
analyzeJavaScriptApplicationInputSchema.safeParse({
input_path: "relative/app.asar",
}).success,
).toBe(true);
expect(
analyzeJavaScriptApplicationInputSchema.safeParse({
input_path: "/tmp/app.asar",
limits: { max_findings: 200_000 },
}).success,
).toBe(false);
});
it("maps windows, preload, contextBridge, IPC, validations, utility, and native boundaries without execution", async () => {
const root = await fixtureDirectory();
Reflect.deleteProperty(globalThis, "__rea_electron_fixture_executed");
const first = await reconstructJavaScriptArtifact({ input_path: root });
const second = await reconstructJavaScriptArtifact({ input_path: root });
const graph = parseJavaScriptApplicationGraph(first.graph);
expect(
Reflect.get(globalThis, "__rea_electron_fixture_executed"),
).toBeUndefined();
expect(first.graph).toEqual(second.graph);
expectElectronSummary(first);
expectElectronRoles(graph);
expectElectronBoundaries(graph);
});
it("analyzes the selected local application directory directly", async () => {
const root = await fixtureDirectory();
const result = await analyzeJavaScriptApplication({
input_path: root,
});
if (!result.ok) throw result.error;
const evidence = parseEvidence(result.value);
expect(evidence).toMatchObject({
operation: "analyze_javascript_application",
predicate_type: "rea.javascript-application-analysis",
provider: { id: "rea-javascript-application" },
authority: "shipped-artifact",
confidence: "derived",
subject: { local_path: root, format: "directory" },
normalized_result: {
input_path: root,
summary: { browser_windows: 3 },
},
});
});
it(
"keeps Electron runtime entrypoints on their declared filenames",
exactRuntimeEntrypointFixture,
);
it("keeps computed identifier member keys unknown", async () => {
const root = await computedMemberFixtureDirectory();
const result = await reconstructJavaScriptArtifact({ input_path: root });
const graph = parseJavaScriptApplicationGraph(result.graph);
const nativeBindings = graph.nodes.filter(
(node) => node.kind === "native-export",
);
expect(result.electron_summary).toMatchObject({
sender_validation_observations: 0,
native_addon_bindings: 1,
resolved_native_addon_bindings: 1,
});
expect(nativeBindings).toMatchObject([
{
observations: [
{
properties: {
requested_members: [],
namespace_access: false,
dynamic_member_access: true,
native_export_verification: "not-performed",
},
},
],
},
]);
});
it("maps Electron boundaries through bundler-renamed bindings", async () => {
const root = await renamedBindingFixtureDirectory();
const result = await reconstructJavaScriptArtifact({ input_path: root });
const graph = parseJavaScriptApplicationGraph(result.graph);
const handledChannels = graph.nodes.flatMap(({ kind, observations }) =>
kind === "ipc-handler"
? observations.flatMap(({ properties }) =>
properties.side === "main" ? [properties.channel] : [],
)
: [],
);
expect(result.electron_summary).toMatchObject({
browser_windows: 1,
context_bridge_apis: 1,
exposed_api_members: 2,
ipc: {
main_handlers: 2,
paired_renderer_transmissions: 2,
unpaired_literal_renderer_transmissions: 0,
},
utility_processes: 1,
});
expect(handledChannels.sort()).toEqual(["rea:read", "rea:write"]);
});
it("maps unchanged var and let require aliases while excluding reassigned bindings", async () => {
const root = await requireAliasFixtureDirectory();
const result = await reconstructJavaScriptArtifact({ input_path: root });
expect(result.electron_summary.ipc).toMatchObject({
main_handlers: 2,
paired_renderer_transmissions: 2,
unpaired_literal_renderer_transmissions: 0,
});
});
it("returns a tagged cancellation without executing application code", async () => {
const root = await fixtureDirectory();
const controller = new AbortController();
controller.abort();
Reflect.deleteProperty(globalThis, "__rea_electron_fixture_executed");
const result = await analyzeJavaScriptApplication(
{ input_path: root },
{ signal: controller.signal },
);
expect(result).toMatchObject({
ok: false,
error: { _tag: "ArtifactOperationError", reason: "cancelled" },
});
expect(
Reflect.get(globalThis, "__rea_electron_fixture_executed"),
).toBeUndefined();
});
});
const exactRuntimeEntrypointFixture = async (): Promise<void> => {
const root = await createTestTempDirectory("rea-electron-exact-entrypoints-");
await mkdir(join(root, "renderer/page"), { recursive: true });
await Promise.all([
writeFile(
join(root, "main.cjs"),
String.raw`
const { BrowserWindow, utilityProcess } = require("electron");
const path = require("node:path");
new BrowserWindow({ webPreferences: { preload: "./entry" } });
new BrowserWindow().loadURL("file:///renderer/index.html");
new BrowserWindow().loadURL("renderer/bare.html");
new BrowserWindow().loadURL("#home");
new BrowserWindow().loadURL("");
new BrowserWindow().loadURL("./renderer/other.html?theme=dark#home");
new BrowserWindow().loadURL("./renderer/missing.html");
new BrowserWindow().loadFile("./renderer/page");
new BrowserWindow({ webPreferences: { preload: path.join(__dirname, "preload") } });
new BrowserWindow({ webPreferences: { preload: "./explicit-preload.js" } });
utilityProcess.fork("./worker");
utilityProcess.fork("./worker#v1.js");
utilityProcess.fork("./service.js");
`,
),
writeFile(join(root, "entry.html"), "<title>sibling</title>"),
writeFile(join(root, "preload.html"), "<title>sibling</title>"),
writeFile(join(root, "worker.js"), "module.exports = {};"),
writeFile(join(root, "renderer/index.html"), "<title>renderer</title>"),
writeFile(
join(root, "renderer/bare.html"),
"<title>bare renderer URL</title>",
),
writeFile(join(root, "renderer/other.html"), "<title>other</title>"),
writeFile(join(root, "renderer/missing.html.js"), "module.exports = {};"),
writeFile(
join(root, "renderer/page/index.html"),
"<title>directory</title>",
),
writeFile(join(root, "explicit-preload.js"), "module.exports = {};"),
writeFile(join(root, "service.js"), "module.exports = {};"),
]);
const result = await reconstructJavaScriptArtifact({ input_path: root });
const graph = parseJavaScriptApplicationGraph(result.graph);
expectSuffixOnlyRenderersUnresolved(graph);
expectExactRendererPaths(graph);
const roles = graph.nodes.filter(({ kind }) => kind === "electron-preload");
const roleByDeclaration = new Map(
roles.map((role) => [role.observations[0]?.properties.declared_path, role]),
);
expect(result.electron_summary.resolved_utility_entrypoints).toBe(2);
for (const [declaredPath, candidatePath] of [
["./entry", "entry"],
["preload", "preload"],
]) {
const role = roleByDeclaration.get(declaredPath);
expect(role?.observations[0]?.properties).toMatchObject({
declared_path: declaredPath,
resolved_path: null,
resolution_status: "not-found",
candidate_path: candidatePath,
limitations: [
expect.stringContaining(
"extension and directory fallback were not attempted",
),
],
});
expect(
graph.edges.some(
(edge) =>
edge.source_node_id === role?.node_id && edge.relation === "maps_to",
),
).toBe(false);
}
expect(
roleByDeclaration.get("./explicit-preload.js")?.observations[0]?.properties,
).toMatchObject({
resolved_path: "explicit-preload.js",
resolution_status: "resolved",
});
const utilityLaunches = graph.edges.filter(
(edge) =>
edge.evidence.extractor.operation === "map-utility-process-launch",
);
const resolvedWorker = utilityLaunches.find(
(edge) => edge.properties.declared_path === "./worker",
);
expect(resolvedWorker?.evidence).toMatchObject({
authority: "static-relationship-inference",
state: "inferred",
limitations: expect.arrayContaining([
"Static syntax does not prove that this relationship executes at runtime.",
]),
});
expect(utilityLaunches).toEqual(
expect.arrayContaining([
expect.objectContaining({
properties: expect.objectContaining({
declared_path: "./worker",
resolved_path: "worker.js",
resolution_status: "resolved",
}),
}),
expect.objectContaining({
properties: expect.objectContaining({
declared_path: "./worker#v1.js",
resolved_path: null,
resolution_status: "not-found",
}),
}),
expect.objectContaining({
properties: expect.objectContaining({
declared_path: "./service.js",
resolved_path: "service.js",
resolution_status: "resolved",
}),
}),
]),
);
expect(
graph.edges.some(
(edge) =>
edge.evidence.extractor.operation ===
"resolve-utility-process-entrypoint" &&
edge.properties.resolved_path === "worker.html",
),
).toBe(false);
};
type Reconstruction = Awaited<ReturnType<typeof reconstructJavaScriptArtifact>>;
type ApplicationGraph = ReturnType<typeof parseJavaScriptApplicationGraph>;
const expectSuffixOnlyRenderersUnresolved = (graph: ApplicationGraph): void => {
const renderers = new Map(
graph.nodes
.filter(({ kind }) => kind === "electron-renderer")
.map((renderer) => [
renderer.observations[0]?.properties.declared_path,
renderer,
]),
);
expect(
renderers.get("file:///renderer/index.html")?.observations[0]?.properties,
).toMatchObject({
resolved_path: "renderer/index.html",
resolution_status: "resolved",
});
for (const declaredPath of [
"./renderer/other.html?theme=dark#home",
"renderer/bare.html",
"./renderer/missing.html",
"#home",
"",
]) {
const renderer = renderers.get(declaredPath);
expect(renderer?.observations[0]?.properties).toMatchObject({
resolved_path: null,
resolution_status: "unavailable",
});
expect(renderer?.observations[0]?.properties.limitations).toEqual(
expect.arrayContaining([
expect.stringContaining("requires a URL protocol"),
]),
);
expect(
graph.edges.some(
(edge) =>
edge.source_node_id === renderer?.node_id &&
edge.relation === "maps_to",
),
).toBe(false);
}
};
const expectExactRendererPaths = (graph: ApplicationGraph): void => {
const renderers = new Map(
graph.nodes
.filter(({ kind }) => kind === "electron-renderer")
.map((renderer) => [
renderer.observations[0]?.properties.declared_path,
renderer,
]),
);
for (const [declaredPath, candidatePath] of [
["./renderer/page", "renderer/page"],
]) {
const renderer = renderers.get(declaredPath);
expect(renderer?.observations[0]?.properties).toMatchObject({
resolved_path: null,
resolution_status: "not-found",
candidate_path: candidatePath,
});
expect(
graph.edges.some(
(edge) =>
edge.source_node_id === renderer?.node_id &&
edge.relation === "maps_to",
),
).toBe(false);
}
};
const expectElectronSummary = (result: Reconstruction): void => {
expect(result.electron_summary).toMatchObject({
browser_windows: 3,
explicit_web_preferences: 8,
preload_entrypoints: 1,
context_bridge_apis: 2,
exposed_api_members: 5,
ipc: {
literal_channels: 5,
dynamic_channel_operations: 2,
paired_renderer_transmissions: 4,
ambiguous_renderer_transmissions: 1,
unpaired_literal_renderer_transmissions: 1,
},
sender_validation_observations: 2,
utility_processes: 1,
resolved_utility_entrypoints: 1,
native_addon_bindings: 4,
resolved_native_addon_bindings: 4,
});
};
const expectElectronRoles = (graph: ApplicationGraph): void => {
expect(graph.nodes.map(({ kind }) => kind)).toEqual(
expect.arrayContaining([
"browser-window",
"electron-preload",
"context-bridge-api",
"ipc-channel",
"ipc-handler",
"electron-utility",
"native-addon",
"native-export",
]),
);
const roles = graph.nodes.filter(({ kind }) =>
["electron-main", "electron-preload", "electron-renderer"].includes(kind),
);
expect(roles.length).toBeGreaterThanOrEqual(3);
for (const role of roles) {
for (const observation of role.observations)
expect(observation.properties).toMatchObject({
declared_path: expect.any(String),
resolution_context: expect.stringMatching(
/^(package-entrypoint|filesystem-expression|module-specifier|html-reference)$/u,
),
resolution_status: expect.stringMatching(
/^(resolved|not-found|unavailable|external|rejected)$/u,
),
limitations: expect.any(Array),
});
if (
role.observations.some(
({ properties }) => properties.resolution_status === "resolved",
)
)
expect(
graph.edges.some(
(edge) =>
edge.source_node_id === role.node_id && edge.relation === "maps_to",
),
).toBe(true);
}
const dirnamePreload = roles.find(
({ kind, observations }) =>
kind === "electron-preload" &&
observations.some(
({ properties }) =>
properties.declared_path === "preload.js" &&
properties.resolution_context === "filesystem-expression",
),
);
expect(dirnamePreload?.observations[0]?.properties).toMatchObject({
declared_path: "preload.js",
resolution_context: "filesystem-expression",
resolved_path: "preload.js",
resolution_status: "resolved",
limitations: [],
});
};
const expectElectronBoundaries = (graph: ApplicationGraph): void => {
expect(graph.nodes).toEqual(
expect.arrayContaining([
expect.objectContaining({
kind: "browser-window",
observations: expect.arrayContaining([
expect.objectContaining({
properties: expect.objectContaining({
preload_path: "preload.js",
absence_means_default: false,
web_preferences: expect.arrayContaining([
expect.objectContaining({
name: "nodeIntegration",
value: expect.objectContaining({
status: "literal",
value: true,
}),
}),
]),
}),
}),
]),
}),
expect.objectContaining({
kind: "browser-window",
observations: expect.arrayContaining([
expect.objectContaining({
properties: expect.objectContaining({
web_preferences: expect.arrayContaining([
expect.objectContaining({
name: "nodeIntegration",
value: expect.objectContaining({
status: "literal",
value: false,
}),
}),
expect.objectContaining({
name: "contextIsolation",
value: expect.objectContaining({
status: "literal",
value: true,
}),
}),
]),
}),
}),
]),
}),
expect.objectContaining({
kind: "context-bridge-api",
observations: expect.arrayContaining([
expect.objectContaining({
properties: expect.objectContaining({
api_key: "reaApi",
members: expect.arrayContaining([
"nested",
"nested.write",
"read",
]),
}),
}),
]),
}),
expect.objectContaining({
kind: "ipc-handler",
observations: expect.arrayContaining([
expect.objectContaining({
properties: expect.objectContaining({
fact_kind: "sender-validation-candidate",
enforcement: "unknown",
}),
}),
]),
}),
expect.objectContaining({
kind: "native-export",
observations: expect.arrayContaining([
expect.objectContaining({
properties: expect.objectContaining({
native_export_verification: "not-performed",
}),
}),
]),
}),
]),
);
const pairingEdges = graph.edges.filter(
({ properties }) =>
properties.pairing_basis === "unique-exact-literal-channel",
);
expect(pairingEdges).toHaveLength(4);
expect(
pairingEdges.some(
({ properties }) => properties.channel === "rea:ambiguous",
),
).toBe(false);
expect(graph.limitations.join(" ")).toMatch(/dynamic IPC|ambiguous/iu);
};
const fixtureDirectory = async (): Promise<string> => {
const root = await createTestTempDirectory("rea-electron-boundaries-");
await writeElectronBoundaryFixture(root);
return root;
};
const requireAliasFixtureDirectory = async (): Promise<string> => {
const root = await createTestTempDirectory("rea-electron-require-aliases-");
await Promise.all([
writeFile(
join(root, "package.json"),
JSON.stringify({
name: "rea-electron-require-aliases",
main: "main.cjs",
}),
),
writeFile(
join(root, "main.cjs"),
String.raw`
var { ipcMain: main } = require("electron");
let listener = require("electron/main").ipcMain;
main.handle("rea:read", async () => "value");
listener.on("rea:write", () => undefined);
let replaced = require("electron").ipcMain;
replaced = { handle() {} };
replaced.handle("rea:reassigned", () => undefined);
var assignedLater;
assignedLater.handle("rea:before-assignment", () => undefined);
assignedLater = require("electron").ipcMain;
if (globalThis.registerIpc) {
var conditional = require("electron").ipcMain;
}
conditional.handle("rea:conditional", () => undefined);
`,
),
writeFile(
join(root, "preload.cjs"),
String.raw`
const { ipcRenderer } = require("electron");
ipcRenderer.invoke("rea:read");
ipcRenderer.send("rea:write", "value");
`,
),
]);
return root;
};
const renamedBindingFixtureDirectory = async (): Promise<string> => {
const root = await createTestTempDirectory("rea-electron-renamed-");
await Promise.all([
writeFile(
join(root, "package.json"),
JSON.stringify({ name: "rea-electron-renamed", main: "main.mjs" }),
),
// Bundlers rename colliding imports, e.g. esbuild's ipcMain2.
writeFile(
join(root, "main.mjs"),
String.raw`
import { BrowserWindow as BrowserWindow2, ipcMain as ipcMain2 } from "electron";
import { ipcMain as ipcMain3, utilityProcess as utility } from "electron/main";
new BrowserWindow2({ webPreferences: { preload: "./preload.cjs" } });
ipcMain2.handle("rea:read", async () => "value");
ipcMain3.on("rea:write", () => undefined);
utility.fork("./worker.js");
function register(ipcMain2) {
ipcMain2.handle("rea:shadowed", async () => "local");
}
export { register };
`,
),
writeFile(
join(root, "preload.cjs"),
String.raw`
const { contextBridge: bridge, ipcRenderer: ipc } = require("electron");
bridge.exposeInMainWorld("reaApi", {
read: () => ipc.invoke("rea:read"),
write: (value) => ipc.send("rea:write", value),
});
`,
),
writeFile(join(root, "worker.js"), "module.exports = {};"),
]);
return root;
};
const computedMemberFixtureDirectory = async (): Promise<string> => {
const root = await createTestTempDirectory("rea-electron-computed-members-");
await mkdir(join(root, "native"));
await Promise.all([
writeFile(
join(root, "main.js"),
String.raw`
const member = "read";
const selected = require("./native/addon.node")[member];
const startsWith = "includes";
function validate(event) {
return event.senderFrame.url[startsWith]("file://");
}
module.exports = { selected, validate };
`,
),
writeFile(join(root, "native", "addon.node"), Buffer.from([0, 1, 2, 3])),
]);
return root;
};