fix(electron): retain wrapped protocol handler registrations

This commit is contained in:
ShuaixinHuang authored and morluto committed 2026-11-03 23:11:45 +08:00
1 parent 8c857ce56c
commit c28d47bf13
4 files changed
+180 -4

No files matched your search

@@ -289,8 +289,18 @@ describe("Electron protocol registrations without a scheme", () => {
describe("Electron protocol calls that register nothing", () => {
it.each([
["a handler without a callback", 'protocol.handle("app");'],
["a handler with a null callback", 'protocol.handle("app", null);'],
[
"a legacy handler without a callback",
'protocol.registerStringProtocol("app");',
],
[
"a legacy handler with a null callback",
'protocol.registerStringProtocol("app", null);',
],
[
"a legacy handler with a void callback",
'protocol.registerStringProtocol("app", void 0);',
],
[
"an interceptor without a callback",
'protocol.interceptHttpProtocol("https");',
@@ -331,3 +341,31 @@ describe("Electron protocol calls that register nothing", () => {
]);
});
});
describe("Electron wrapped protocol.handle registrations", () => {
it.each([
["protocol", "default-protocol"],
["session.defaultSession.protocol", "default-session"],
])(
"retains %s registrations before handler invocation",
(receiver, session) => {
const protocols = protocolsOf(`
const { protocol, session } = require("electron");
${receiver}.handle("omitted");
${receiver}.handle("null", null);
${receiver}.handle("undefined", void 0);
`);
expect(protocols).toMatchObject([
{ api: "handle", role: "handler", session, scheme: literal("omitted") },
{ api: "handle", role: "handler", session, scheme: literal("null") },
{
api: "handle",
role: "handler",
session,
scheme: literal("undefined"),
},
]);
expect(protocols).toHaveLength(3);
},
);
});
@@ -87,7 +87,12 @@ export const inspectElectronProtocolNode = (
: INTERCEPTORS.has(target.api)
? "interceptor"
: undefined;
if (role !== undefined && !handlerKnownMissing(node))
// handle installs its own native callback and invokes the supplied handler
// only when a request arrives; a missing handler does not prevent registration.
if (
role !== undefined &&
(target.api === "handle" || !handlerKnownMissing(node))
)
addProtocol(context, {
node,
api: target.api,
@@ -97,7 +102,7 @@ export const inspectElectronProtocolNode = (
});
};
/** Electron requires the callback, so a call without one installs nothing. */
/** Legacy native registration APIs require a callback before installing it. */
const handlerKnownMissing = (
node: t.CallExpression | t.OptionalCallExpression,
): boolean => {
@@ -0,0 +1,87 @@
import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import { expect } from "vitest";
import { z } from "zod";
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";
cliTest.for(["CLI", "stdio MCP"] as const)(
"retains wrapped protocol.handle source registrations through %s",
async (surface, { cli }) => {
const root = await createTestTempDirectory("rea-electron-handle-presence-");
await writeFile(
join(root, "package.json"),
JSON.stringify({ main: "main.cjs" }),
);
await writeFile(
join(root, "main.cjs"),
`
const { protocol, session } = require("electron");
protocol.handle("module-omitted");
protocol.handle("module-null", null);
protocol.handle("module-undefined", void 0);
session.defaultSession.protocol.handle("session-omitted");
session.defaultSession.protocol.handle("session-null", null);
session.defaultSession.protocol.handle("session-undefined", void 0);
protocol.registerStringProtocol("legacy-omitted");
protocol.registerStringProtocol("legacy-null", null);
protocol.registerStringProtocol("legacy-undefined", void 0);
throw new Error("analysis fixture must remain inert");
`,
);
let document: unknown;
if (surface === "CLI") {
const response = await cli.run({
arguments: ["analyze-javascript-application", root, "--json"],
});
expect(response.exitCode, response.stderr).toBe(0);
document = response.json;
} else {
const { call } = await connectLocalToolsMcp();
const response = await call("analyze_javascript_application", {
input_path: root,
});
expect(response.isError, JSON.stringify(response)).not.toBe(true);
document = response.structuredContent;
}
const result = z
.object({ normalized_result: javascriptApplicationAnalysisResultSchema })
.parse(document).normalized_result;
expect(result.summary.protocol_registrations).toBe(6);
const schemes = result.graph.nodes.filter(
({ kind }) => kind === "url-scheme",
);
expect(
schemes.map((node) => node.observations[0]?.label).toSorted(),
).toEqual([
"module-null",
"module-omitted",
"module-undefined",
"session-null",
"session-omitted",
"session-undefined",
]);
for (const scheme of schemes) {
const observation = scheme.observations[0];
expect(observation?.properties).toMatchObject({
api: "handle",
role: "handler",
resolution: "literal",
});
expect(observation?.evidence).toMatchObject({
authority: "ast-static-analysis",
state: "observed",
limitations: expect.arrayContaining([
expect.stringContaining("successful registration"),
]),
});
}
expect(
result.graph.edges.filter((edge) => edge.relation === "handles"),
).toHaveLength(6);
},
);
@@ -37,6 +37,52 @@ const protocolFixture = async (): Promise<string> => {
return root;
};
describe("Electron wrapped protocol.handle graph", () => {
it("retains wrapped handle registrations while rejecting missing legacy callbacks", async () => {
const root = await createTestTempDirectory(
"rea-electron-wrapped-protocol-",
);
await writeFile(
join(root, "main.cjs"),
`
const { protocol, session } = require("electron");
protocol.handle("module-omitted");
protocol.handle("module-null", null);
protocol.handle("module-undefined", void 0);
session.defaultSession.protocol.handle("session-omitted");
session.defaultSession.protocol.handle("session-null", null);
session.defaultSession.protocol.handle("session-undefined", void 0);
protocol.registerStringProtocol("legacy-omitted");
protocol.registerStringProtocol("legacy-null", null);
protocol.registerStringProtocol("legacy-undefined", void 0);
`,
);
const result = await reconstructJavaScriptArtifact({ input_path: root });
const graph = parseJavaScriptApplicationGraph(result.graph);
const schemes = graph.nodes.filter(({ kind }) => kind === "url-scheme");
expect(
schemes.map((node) => node.observations[0]?.label).toSorted(),
).toEqual([
"module-null",
"module-omitted",
"module-undefined",
"session-null",
"session-omitted",
"session-undefined",
]);
expect(
graph.edges.filter((edge) => edge.relation === "handles"),
).toHaveLength(6);
for (const scheme of schemes) {
expect(scheme.observations[0]?.properties).toMatchObject({
api: "handle",
role: "handler",
resolution: "literal",
});
}
});
});
describe("Electron URL scheme boundaries", () => {
it("joins case-insensitive scheme identities while preserving declared spellings", async () => {
const root = await createTestTempDirectory("rea-electron-protocol-case-");