* 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
503 lines
16 KiB
TypeScript
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,
|
|
);
|