fix(electron): retain nested bridge facts through wrapper resolution
Merge direct same-call nested member facts into symbolic wrapper results using canonical deduplication and ordering. Preserve unknown spread coverage rather than losing known member paths. Validation: bounded source cases and public CLI/stdio MCP spread/no-spread controls. The existing 125,000-member stress case exceeded the 384 MiB local cap and was stopped without increasing the limit; that capacity case remains unverified locally. Fixes #3212
This commit is contained in:
1 parent
81f7638203
commit
0351328e54
3 files changed
+82
-1
No files matched your search
@@ -140,6 +140,40 @@ it("preserves an observed empty contextBridge API object", () => {
|
||||
expect(result.parse_status).toBe("complete");
|
||||
});
|
||||
|
||||
it.each([
|
||||
[
|
||||
"explicit nested paths after an unknown spread",
|
||||
`{ ...extra, a: { b: 1 }, c: 2 }`,
|
||||
["a", "a.b", "c"],
|
||||
1,
|
||||
"dynamic",
|
||||
],
|
||||
[
|
||||
"a direct object literal",
|
||||
`{ a: { b: 1 }, c: 2 }`,
|
||||
["a", "a.b", "c"],
|
||||
0,
|
||||
"object-literal",
|
||||
],
|
||||
["a dynamic API argument", "getApi()", [], 1, "dynamic"],
|
||||
] as const)(
|
||||
"preserves directly observed nested bridge members for %s",
|
||||
(_name, api, members, unknownMembers, apiStatus) => {
|
||||
const result = analyzeJavaScriptStaticSource(`
|
||||
const { contextBridge } = require("electron");
|
||||
contextBridge.exposeInMainWorld("api", ${api});
|
||||
`);
|
||||
expect(result.electron.context_bridge_apis).toContainEqual(
|
||||
expect.objectContaining({
|
||||
api_key: "api",
|
||||
api_status: apiStatus,
|
||||
members,
|
||||
unknown_members: unknownMembers,
|
||||
}),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
describe("wrapped contextBridge exposure methods", () => {
|
||||
it("retains the original expression for a dynamic isolated world id", () => {
|
||||
const result = analyzeJavaScriptStaticSource(
|
||||
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
symbolicDefiniteExit,
|
||||
symbolicLogicalRight,
|
||||
} from "./javascriptSymbolicControlFlow.js";
|
||||
import { compareUnicodeCodePoints } from "../unicodeCodePointOrder.js";
|
||||
|
||||
const callLabel = (node: t.Node): string =>
|
||||
t.isSequenceExpression(node)
|
||||
@@ -410,7 +411,12 @@ export const collectElectronWrappedCallPaths = (
|
||||
api_key_expression: null,
|
||||
api_status: "dynamic" as const,
|
||||
resolution_basis: "wrapper-substitution" as const,
|
||||
members,
|
||||
// The direct syntax pass recursively retains literal dotted paths. The
|
||||
// symbolic resolver enumerates only immediate object keys, so its
|
||||
// replacement must not erase paths already established at this call.
|
||||
members: [
|
||||
...new Set([...(existing?.value.members ?? []), ...members]),
|
||||
].sort(compareUnicodeCodePoints),
|
||||
unknown_members: resolver.unknownMembers(object),
|
||||
module_key: null,
|
||||
location,
|
||||
|
||||
@@ -308,6 +308,47 @@ cliTest(
|
||||
);
|
||||
|
||||
for (const surface of ["CLI", "stdio MCP"] as const) {
|
||||
cliTest(
|
||||
`retains explicit nested contextBridge members after an unknown spread through ${surface}`,
|
||||
async ({ cli }) => {
|
||||
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});`,
|
||||
});
|
||||
return analyzeJavaScriptDirectory(surface, cli, root);
|
||||
};
|
||||
const withSpread = await analyzeBridge("{ ...extra, a: { b: 1 }, c: 2 }");
|
||||
const control = await analyzeBridge("{ a: { b: 1 }, c: 2 }");
|
||||
|
||||
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(
|
||||
`preserves handler ambiguity and short-circuit factory paths through ${surface}`,
|
||||
async ({ cli }) => {
|
||||
|
||||
Reference in new issue
Block a user