Files
rea/tests/acceptance/applications/javascriptFactoryCompletion.test.ts
ShuaixinHuang e9fc8fb30c fix(javascript): preserve runtime path bytes and Electron ownership (#4941)
* fix(javascript): preserve literal percent runtime paths

* fix(javascript): ignore resolve prefixes before the final anchor

* fix(electron): retain application roots for loadFile declarations

* fix(electron): resolve loadFile from enclosing application package

* fix(electron): retain outer application root across package scopes

* fix(electron): derive application ownership from inventoried packages

* fix(electron): retain resolved package entrypoint ownership

* fix(electron): preserve competing application ownership

* fix(electron): retain invalid and omitted runtime ownership

* fix(electron): preserve omitted module ownership and runtime paths

* fix(electron): preserve proven roots before confining page paths

* fix(electron): retain possible owners behind missing imports

* fix(electron): distinguish absent imports and owned page candidates

* fix(electron): keep unavailable JSON out of owner gaps

* fix(electron): retain exact loader formats and default entry status

* fix(electron): retain unavailable entrypoint ownership candidates

* test(ghidra): preserve Windows Node probe prerequisites

* test(ghidra): assert cancellation during profile shutdown

* fix(electron): separate examined imports from frontend ownership

* fix(electron): retain unavailable directory entrypoint owners

* test(jeb): retain Linux ownership prerequisite in isolation

* fix(javascript): retain unchecked loader ownership and literal main paths

* fix(javascript): preserve runtime paths and call-site module aliases

* fix(javascript): bound module alias traversal and preserve projections

* fix(javascript): retain host module and nested artifact path boundaries

* fix(javascript): preserve ESM and archive module loader provenance

* test(javascript): distinguish unmapped containers from missing exports

* fix: retain default module aliases and Electron builtin imports

* fix: preserve host file URL and inward ASAR ownership

* fix: preserve CommonJS file URL classification in CI fixtures

* fix: retain bound module loaders and Windows entrypoint uncertainty

* fix: retain ASAR uncertainty and invalidate changed Electron app roots

* fix(javascript): retain ownership uncertainty across loader boundaries

* fix(javascript): preserve loader sources and bound alias queries

* fix(javascript): retain destructured loader sources

* fix(javascript): preserve loader uncertainty at resolution boundaries

* fix(javascript): preserve callable and package launch ownership

* fix(javascript): preserve captured values across member reads

* test(process): assert tagged filesystem cancellation failures

* fix(javascript): preserve inventory bases and invocation read timing

* fix(javascript): preserve ownership across indirect and unread loads

* test(javascript): order fixture directory creation on Windows

* test(javascript): share ordered filesystem fixture writer

* fix(javascript): unify loader provenance and package validation

* fix(javascript): preserve captured Node require loader values

* fix(javascript): preserve loader invocation authority

* fix(javascript): verify generic loader invocation authority

* fix(javascript): preserve conditional loader method alternatives

* fix(javascript): require proven factories for source association

* fix(javascript): honor replaced Electron app path setters

* fix(javascript): prove path and assignment source ownership

* fix(javascript): retain native delegate eligibility through shared writes

* fix(process): settle stale baseline errors after confirmed absence

* fix(javascript): preserve assignment and path boundary evidence

* fix(javascript): prove source-directory resolve anchors

* fix: preserve application path provenance and portable schemas

* fix(javascript): retain CommonJS exported setter effects

* fix: isolate Linux identity observations and retain native module exports

* fix(javascript): preserve published app and helper provenance

* fix(javascript): normalize typed stores and native path fixtures
2026-10-20 17:25:19 +08:00

503 lines
16 KiB
TypeScript

import { writeFile, mkdir } from "node:fs/promises";
import { join } from "node:path";
import { runInNewContext } from "node:vm";
import { isNativeError } from "node:util/types";
import { expect } from "vitest";
import { toolContract } from "../../../src/contracts/toolContracts.js";
import { javascriptApplicationAnalysisResultSchema } from "../../../src/domain/javascript/javascriptApplicationAnalysis.js";
import { connectLocalToolsMcp } from "../../fixtures/localToolsMcp.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { cliTest } from "../../support/cli/cliFixture.js";
const scenarios = [
{
name: "pre-exit-root-export",
guard: "if(flag)return;exports.unrelated=1;",
eventsFirst: true,
bindingFirst: true,
rootEvents: true,
candidate: true,
},
{
name: "pre-exit-export-replaced",
guard: "if(flag)return;exports.events=null;",
eventsFirst: true,
bindingFirst: true,
candidate: true,
guarded: true,
guardCondition: "flag",
},
{
name: "pre-exit-export",
guard: "if(flag)return;",
eventsFirst: true,
bindingFirst: true,
candidate: true,
},
{
name: "pre-exit-export-later-local",
guard: "if(flag)return;const unrelated=1;",
eventsFirst: true,
bindingFirst: true,
candidate: true,
},
{
name: "pre-exit-export-late-capture",
guard: "if(flag)return;",
eventsFirst: true,
candidate: true,
guarded: true,
},
{ name: "control", guard: "", candidate: true },
{ name: "false", guard: "if (false) return;", candidate: true },
{
name: "unknown",
guard: "if (flag) return;",
candidate: true,
guarded: true,
},
{
name: "block",
guard: "{ if (flag) return; }",
candidate: true,
guarded: true,
},
{
name: "nested-function",
guard: "function unused() { return; }",
candidate: true,
},
{
name: "async-guard",
guard: "async function stop(){return false;}if(stop())return;",
candidate: false,
},
{
name: "generator-guard",
guard: "function* stop(){return false;}if(stop())return;",
candidate: false,
},
{
name: "async-generator-guard",
guard: "async function* stop(){return false;}if(stop())return;",
candidate: false,
},
{ name: "true", guard: "if (true) return;", candidate: false },
{ name: "return", guard: "return;", candidate: false },
{ name: "both", guard: "if (flag) return; else return;", candidate: false },
{ name: "throw", guard: "throw new Error('stop');", candidate: false },
{ name: "labeled-exit", guard: "exit: { return; }", candidate: false },
{ name: "with-exit", guard: "with ({}) { return; }", candidate: false },
{
name: "export-guard",
guard: "exports.stop=true;if(exports.stop)return;exports.stop=false;",
candidate: false,
},
{
name: "module-guard",
guard:
"module.exports.stop=true;if(module.exports.stop)return;module.exports.stop=false;",
candidate: false,
},
{
name: "snapshot-guard",
guard:
"exports.stop=true;const {stop}=exports;exports.stop=false;if(stop)return;",
candidate: false,
},
{
name: "spread-guard",
guard:
"exports.stop=true;const saved={...exports};exports.stop=false;if(saved.stop)return;",
candidate: false,
},
{
name: "local-guard",
guard:
"exports.stop=true;function stop(){return exports.stop;}if(stop())return;exports.stop=false;",
candidate: false,
},
{
name: "argument-guard",
guard: "exports.stop=true;if(readStop(exports))return;exports.stop=false;",
candidate: false,
},
{
name: "false-export-guard",
guard: "exports.stop=false;if(exports.stop)return;exports.stop=true;",
candidate: true,
},
{
name: "loaded-spread-snapshot",
guard:
"exports.stop=true;const saved={...exports};exports.stop=false;exports.check=()=>saved.stop;",
callerGuard: "if(helper.check())return;",
candidate: false,
},
{
name: "loaded-destructured-snapshot",
guard:
"exports.stop=true;const {stop}=exports;exports.stop=false;exports.check=()=>stop;",
callerGuard: "if(helper.check())return;",
candidate: false,
},
{
name: "self-alias",
guard: "exports.self=exports;",
listenerPath: "self.self.events",
candidate: true,
},
{
name: "module-alias",
guard: "exports.owner=module;",
listenerPath: "owner.exports.events",
candidate: true,
},
{
name: "returned-alias",
guard: "exports.getSelf=()=>exports;",
listenerPath: "getSelf().events",
candidate: true,
},
{
name: "self-alias-after-exit",
guard: "exports.self=exports;return;",
listenerPath: "self.events",
candidate: false,
},
{
name: "imported-guard",
guard:
"exports.stop=true;if(load(1).read(exports))return;exports.stop=false;",
candidate: false,
},
{
name: "imported-false-guard",
guard:
"exports.stop=false;if(load(1).read(exports))return;exports.stop=true;",
candidate: true,
},
{
name: "imported-nested-guard",
guard:
"exports.stop=true;if(load(1).read2(exports))return;exports.stop=false;",
candidate: false,
},
{
name: "imported-snapshot-guard",
guard:
"exports.stop=true;const saved=load(1).read(exports);exports.stop=false;if(saved)return;",
candidate: false,
},
{
name: "loaded-imported-snapshot",
guard:
"exports.stop=true;const saved=load(1).read(exports);exports.stop=false;exports.check=()=>saved;",
callerGuard: "if(helper.check())return;",
candidate: false,
},
{
name: "inline-self-alias",
guard: "exports.self=exports;",
listenerPath: "self.events",
inlineEvents: true,
candidate: true,
},
{
name: "export-after-exit",
guard: "return;",
candidate: false,
bindingFirst: true,
},
] as const;
const factorySource = (scenario: (typeof scenarios)[number]) => {
const { guard } = scenario;
const bindingFirst = "bindingFirst" in scenario && scenario.bindingFirst;
const callerGuard = "callerGuard" in scenario ? scenario.callerGuard : "";
const listenerPath =
"listenerPath" in scenario ? scenario.listenerPath : "events";
const events =
"rootEvents" in scenario && scenario.rootEvents
? `module.exports={events:{addListener(channel,handler){return electron.ipcMain.addListener(channel,handler);}}};`
: "inlineEvents" in scenario && scenario.inlineEvents
? 'exports.events = require("electron").ipcMain;'
: `exports.events = { addListener(channel,handler) {
return electron.ipcMain.addListener(channel,handler);
}};`;
const binding = "const electron = load(2);";
return `function readStop(value) { return value.stop; }
(() => {
const modules = {
1(module,exports) {
exports.read = value => value.stop;
exports.read2 = value => readStop(value);
},
2(module) { module.exports = require("electron"); },
5(module,exports,load) {
${bindingFirst ? binding : ""}
${"eventsFirst" in scenario && scenario.eventsFirst ? events : ""}
${guard}
${bindingFirst ? "" : binding}
${"eventsFirst" in scenario && scenario.eventsFirst ? "" : events}
},
9(module,exports,load) {
const helper = load(5);
${callerGuard}
helper.${listenerPath}.addListener("test:copy", () => {});
}
};
function load(id) { const m={exports:{}}; modules[id].call(m.exports,m,m.exports,load); return m.exports; }
load(9);
})();`;
};
const runtimeChannels = (source: string, flag: boolean) => {
const channels: string[] = [];
try {
// Only this authored fixture executes. REA reads the same bytes as inert
// source; the stub does not establish real Electron runtime behavior.
runInNewContext(
source,
{
flag,
require: (name: string) => {
if (name !== "electron") throw new Error(`Unexpected module ${name}`);
return {
ipcMain: {
addListener: (channel: string) => channels.push(channel),
},
};
},
},
{ timeout: 1_000 },
);
} catch (error) {
// A skipped export makes the caller throw before it registers a handler.
if (
!isNativeError(error) ||
channels.length !== 0 ||
!(
error.message === "stop" ||
(error.name === "TypeError" && error.message.includes("addListener")) ||
(error.name === "ReferenceError" &&
error.message === "Cannot access 'electron' before initialization")
)
)
throw error;
}
return channels;
};
type Application = ReturnType<
typeof javascriptApplicationAnalysisResultSchema.parse
>;
const verifyApplication = (
result: Application,
candidate: boolean,
guarded: string | false,
coverage: "partial" | "complete",
) => {
expect(result.statistics.parse_failures).toBe(0);
expect(result.summary.ipc.literal_channels).toBe(
candidate && !guarded ? 1 : 0,
);
const handlers = result.graph.nodes.filter(
(node) =>
node.identity.strategy === "artifact-local-key" &&
node.identity.namespace === "electron-ipc-handler" &&
node.identity.key.includes("main:on:test:copy:"),
);
expect(handlers).toHaveLength(candidate && !guarded ? 1 : 0);
for (const handler of handlers) {
expect(handler.observations[0]?.evidence).toMatchObject({
authority: "static-relationship-inference",
state: "inferred",
location: { available: true, value: { source: "main.js" } },
coverage: { status: coverage },
});
}
expect(result.graph.coverage).toMatchObject({
status: coverage,
truncated: false,
});
const paths = result.graph.nodes
.flatMap((node) => node.observations)
.filter(
(observation) =>
observation.properties.terminal === "electron.ipcMain.addListener" &&
observation.properties.channel === "test:copy",
);
if (!candidate || !guarded) expect(paths).toEqual([]);
if (guarded) {
expect(paths.map((observation) => observation.properties)).toContainEqual(
expect.objectContaining({
resolution: "candidate",
conditions: expect.arrayContaining([
expect.objectContaining({ expression: guarded }),
]),
}),
);
expect(paths[0]?.evidence).toMatchObject({
authority: "static-relationship-inference",
state: "inferred",
location: { available: true, value: { source: "main.js" } },
});
}
};
cliTest(
"retains guarded factory fallthrough through CLI and real stdio MCP",
async ({ cli }) => {
const root = await createTestTempDirectory("rea-factory-completion-");
const { call } = await connectLocalToolsMcp();
const contract = toolContract("analyze_javascript_application");
for (const scenario of scenarios) {
const source = factorySource(scenario);
const directory = join(root, scenario.name);
await mkdir(directory);
await writeFile(join(directory, "main.js"), source);
const runtime = [false, true].map((flag) =>
runtimeChannels(source, flag),
);
const hasRuntimePath = runtime.some((channels) =>
channels.includes("test:copy"),
);
expect(hasRuntimePath, scenario.name).toBe(scenario.candidate);
if (
"guarded" in scenario &&
scenario.guarded &&
!("guardCondition" in scenario)
)
expect(runtime).toEqual([["test:copy"], []]);
const response = await call("analyze_javascript_application", {
input_path: directory,
format: "directory",
});
expect(
response.isError,
response.isError
? `${scenario.name}: ${JSON.stringify(response.content)}`
: scenario.name,
).not.toBe(true);
const evidence = contract.outputSchema.parse(response.structuredContent);
verifyApplication(
javascriptApplicationAnalysisResultSchema.parse(
evidence.normalized_result,
),
scenario.candidate,
"guarded" in scenario && scenario.guarded
? "guardCondition" in scenario
? scenario.guardCondition
: "!(flag)"
: false,
"inlineEvents" in scenario && scenario.inlineEvents
? "complete"
: "partial",
);
if (["control", "false", "unknown"].includes(scenario.name)) {
const analyzed = await cli.run({
arguments: [
"analyze-javascript-application",
directory,
"--artifact-format",
"directory",
"--json",
],
});
expect(analyzed.exitCode, analyzed.stderr).toBe(0);
expect(contract.outputSchema.parse(analyzed.json).evidence_id).toBe(
evidence.evidence_id,
);
}
}
},
120_000,
);
cliTest(
"does not execute unadvanced generator calls through public CLI/MCP",
async ({ cli }) => {
const root = await createTestTempDirectory("rea-generator-call-");
const { call } = await connectLocalToolsMcp();
const contract = toolContract("analyze_javascript_application");
for (const [name, kind, expected, advance] of [
["regular", "", 1, false],
["generator", "*", 0, false],
["async-generator", "async *", 0, false],
["advanced-generator", "*", 1, true],
] as const) {
const declaration =
kind === "async *" ? "async function*" : `function${kind}`;
const source = `const e=require("electron");
${declaration} send(channel){e.ipcRenderer.send(channel);}
e.contextBridge.exposeInMainWorld("api",{start:()=>send("generator:test")${advance ? ".next()" : ""}});`;
const channels: string[] = [];
runInNewContext(
source,
{
require: () => ({
ipcRenderer: { send: (channel: string) => channels.push(channel) },
contextBridge: {
exposeInMainWorld: (
_key: string,
api: { start: () => unknown },
) => api.start(),
},
}),
},
{ timeout: 1_000 },
);
expect(channels.length, name).toBe(expected);
const directory = join(root, name);
await mkdir(directory);
await writeFile(join(directory, "main.js"), source);
const response = await call("analyze_javascript_application", {
input_path: directory,
format: "directory",
});
expect(response.isError, name).not.toBe(true);
const evidence = contract.outputSchema.parse(response.structuredContent);
const result = javascriptApplicationAnalysisResultSchema.parse(
evidence.normalized_result,
);
expect(result.summary.ipc.literal_channels, name).toBe(
advance ? 0 : expected,
);
if (advance) expect(result.graph.coverage.status).toBe("partial");
const paths = result.graph.nodes
.filter(
(node) =>
node.identity.strategy === "artifact-local-key" &&
node.identity.namespace === "electron-call-path-step",
)
.flatMap((node) => node.observations)
.filter(
(observation) =>
observation.properties.terminal === "electron.ipcRenderer.send" &&
observation.properties.entry === "api.start" &&
observation.properties.step_kind === "terminal",
);
expect(paths.length, name).toBe(advance ? 0 : expected);
const analyzed = await cli.run({
arguments: [
"analyze-javascript-application",
directory,
"--artifact-format",
"directory",
"--json",
],
});
expect(analyzed.exitCode, analyzed.stderr).toBe(0);
expect(contract.outputSchema.parse(analyzed.json).evidence_id).toBe(
evidence.evidence_id,
);
}
},
30_000,
);