Files
rea/src/domain/javascript/javascriptFeatureSeed.array_members.test.ts
T
morluto 2612e474b6 fix(javascript): recover Electron API identity through aliases
Reuse semantic module provenance for aliases and keep property path segments intact through Electron matching. Reject shadowed names and nested or dotted lookalikes. Reassigned and conditionally initialized module origins remain ambiguous, with their source origins retained.

Add a compiled CLI/stdio MCP regression for windows, preloads, context bridges, IPC operations, pairing, and source ranges. Validate 771 JavaScript domain tests, seven composition tests, the public acceptance workflow, build, and check:fast.

Fixes #1656
2026-10-11 08:27:46 +08:00

450 lines
14 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
createJavaScriptApplicationGraph,
createJavaScriptApplicationNode,
} from "./javascriptApplicationGraph.js";
import type { ApplicationNode } from "./javascriptApplicationGraphSchemas.js";
import {
artifactEvidence,
completeCoverage,
} from "./javascriptApplicationGraph.fixture.js";
import { findApplicationFeatureSeeds } from "./javascriptFeatureSeed.js";
import { traceApplicationFeature } from "./javascriptFeatureTrace.js";
import type { ApplicationFeatureSeed } from "./javascriptFeatureTraceSchemas.js";
import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js";
import type { JsonValue } from "../jsonValue.js";
const memberFields = [
{ kind: "api", nodeKind: "context-bridge-api", field: "members" },
{ kind: "api", nodeKind: "context-bridge-api", field: "methods" },
{ kind: "module", nodeKind: "javascript-asset", field: "exports" },
{ kind: "module", nodeKind: "javascript-module", field: "exports" },
{ kind: "module", nodeKind: "source-module", field: "exports" },
{
kind: "native-export",
nodeKind: "native-export",
field: "requested_members",
},
{ kind: "native-export", nodeKind: "native-export", field: "members" },
] as const;
const matchingModes = [
{ value: "openProject", match: null, case_sensitive: true },
{ value: "openProject", match: "exact", case_sensitive: true },
{ value: "openProj", match: "contains", case_sensitive: true },
{ value: "OPENPROJECT", match: "exact", case_sensitive: false },
{ value: "OPENPROJ", match: "contains", case_sensitive: false },
] as const;
describe.each(memberFields)(
"$kind seeds for $nodeKind / $field",
({ kind, nodeKind, field }) => {
it("retains scalar and array member locations", () => {
const values: readonly [JsonValue, string][] = [
["openProject", ""],
[["closeProject", "openProject"], "[1]"],
];
for (const [value, suffix] of values) {
const node = memberNode(nodeKind, { [field]: value });
expect(findApplicationFeatureSeeds([node], seed(kind))).toEqual([
propertyMatch(node, `${field}${suffix}`),
]);
}
});
},
);
describe("shared array-member matching semantics", () => {
const kind = "api";
const nodeKind = "context-bridge-api";
const field = "members";
it.each(matchingModes)(
"matches $match / $value / case-sensitive $case_sensitive",
(mode) => {
const node = memberNode(nodeKind, {
members: ["closeProject", "openProject"],
});
expect(findApplicationFeatureSeeds([node], { kind, ...mode })).toEqual([
propertyMatch(node, "members[1]"),
]);
},
);
it("keeps exact, case-sensitive, and non-string exclusions", () => {
const node = memberNode(nodeKind, {
[field]: [null, 42, true, "openProject"],
});
for (const value of ["Project", "OPENPROJECT", "42", "true", "null"])
expect(findApplicationFeatureSeeds([node], seed(kind, value))).toEqual(
[],
);
expect(
findApplicationFeatureSeeds([node], {
...seed(kind, "Project"),
match: null,
}),
).toEqual([]);
});
it("retains actual indexes through nested arrays and object properties", () => {
const properties = {
nested: [{ [field]: [null, 42, false, [["openProject"]]] }],
};
const node = memberNode(nodeKind, properties);
const before = structuredClone(node);
expect(findApplicationFeatureSeeds([node], seed(kind))).toEqual([
propertyMatch(node, `nested[0].${field}[3][0][0]`),
]);
expect(node).toEqual(before);
expect(properties.nested[0]?.[field]).toEqual([
null,
42,
false,
[["openProject"]],
]);
});
it("preserves literal dotted-field suffixes for scalars and arrays", () => {
const values: readonly [JsonValue, string][] = [
["openProject", ""],
[["openProject"], "[0]"],
];
for (const [value, suffix] of values) {
const node = memberNode(nodeKind, { [`nested.${field}`]: value });
expect(findApplicationFeatureSeeds([node], seed(kind))).toEqual([
propertyMatch(node, `nested.${field}${suffix}`),
]);
}
});
it("does not select near-fields, literal bracket keys, or object descendants", () => {
const cases = [
{
properties: { [`other_${field}`]: "openProject" },
path: `other_${field}`,
},
{ properties: { [`${field}[0]`]: "openProject" }, path: `${field}[0]` },
{
properties: { [`nested.${field}[0]`]: ["openProject"] },
path: `nested.${field}[0][0]`,
},
{
properties: { [field]: { name: "openProject" } },
path: `${field}.name`,
},
{
properties: { [field]: [{ name: "openProject" }] },
path: `${field}[0].name`,
},
];
for (const { properties, path } of cases) {
const node = memberNode(nodeKind, properties);
expect(findApplicationFeatureSeeds([node], seed(kind))).toEqual([]);
expect(findApplicationFeatureSeeds([node], seed("string"))).toEqual([
propertyMatch(node, path),
]);
}
});
});
describe("feature seed selection controls", () => {
it.each([
{ kind: "api", nodeKind: "context-bridge-api", field: "label" },
{ kind: "api", nodeKind: "context-bridge-api", field: "api_name" },
{ kind: "api", nodeKind: "context-bridge-api", field: "key" },
{ kind: "module", nodeKind: "javascript-module", field: "label" },
{ kind: "module", nodeKind: "javascript-module", field: "module_key" },
{ kind: "module", nodeKind: "javascript-module", field: "path" },
{ kind: "module", nodeKind: "javascript-module", field: "source" },
{ kind: "module", nodeKind: "javascript-module", field: "original_source" },
{ kind: "native-export", nodeKind: "native-export", field: "label" },
{ kind: "native-export", nodeKind: "native-export", field: "key" },
{ kind: "route", nodeKind: "endpoint", field: "label" },
{ kind: "route", nodeKind: "endpoint", field: "value" },
{ kind: "route", nodeKind: "endpoint", field: "path" },
{ kind: "channel", nodeKind: "ipc-channel", field: "label" },
{ kind: "channel", nodeKind: "ipc-channel", field: "channel" },
{ kind: "channel", nodeKind: "ipc-channel", field: "key" },
] as const)(
"keeps $kind / $field scalar-only selection unchanged",
({ kind, nodeKind, field }) => {
const scalar = memberNode(nodeKind, {
endpoint_kind: "route",
[field]: "openProject",
});
const array = memberNode(nodeKind, {
endpoint_kind: "route",
[field]: ["openProject"],
});
expect(findApplicationFeatureSeeds([scalar], seed(kind))).toEqual([
propertyMatch(scalar, field),
]);
expect(findApplicationFeatureSeeds([array], seed(kind))).toEqual([]);
expect(findApplicationFeatureSeeds([array], seed("string"))).toEqual([
propertyMatch(array, `${field}[0]`),
]);
},
);
it("keeps generic matching and scalar label and identity selection", () => {
const node = memberNode("context-bridge-api", { members: ["openProject"] });
expect(findApplicationFeatureSeeds([node], seed("string"))).toEqual([
propertyMatch(node, "members[0]"),
]);
expect(
findApplicationFeatureSeeds([node], seed("api", "desktopApi")),
).toEqual([
{
node_id: node.node_id,
kind: node.kind,
basis: "label",
field: "observations[0].label",
},
]);
expect(
findApplicationFeatureSeeds([node], seed("node-id", node.node_id)),
).toEqual([
{
node_id: node.node_id,
kind: node.kind,
basis: "node-id",
field: "node_id",
},
]);
const module = memberNode("javascript-module", {});
expect(
findApplicationFeatureSeeds([module], seed("module", "preload.js")),
).toEqual([
{
node_id: module.node_id,
kind: module.kind,
basis: "identity",
field: "identity.path",
},
]);
});
it.each([
{
kind: "route",
nodeKind: "endpoint",
properties: { endpoint_kind: "route", path: "/projects" },
value: "/projects",
field: "path",
},
{
kind: "channel",
nodeKind: "ipc-channel",
properties: { channel: "project:open" },
value: "project:open",
field: "channel",
},
] as const)(
"preserves $kind scalar matching and node-kind restrictions",
({ kind, nodeKind, properties, value, field }) => {
const node = memberNode(nodeKind, properties);
expect(findApplicationFeatureSeeds([node], seed(kind, value))).toEqual([
propertyMatch(node, field),
]);
expect(
findApplicationFeatureSeeds(
[memberNode("context-bridge-api", properties)],
seed(kind, value),
),
).toEqual([]);
},
);
it.each(memberFields)(
"rejects $kind members on an unrelated node kind",
({ kind, field }) => {
const node = memberNode("unknown", { [field]: ["openProject"] });
expect(findApplicationFeatureSeeds([node], seed(kind))).toEqual([]);
},
);
});
describe("public feature trace from contextBridge source members", () => {
it("traces one exact member seed through the public feature workflow", () => {
const mode = {
value: "openProject",
match: "exact" as const,
case_sensitive: true,
};
const graph = contextBridgeGraph();
const before = structuredClone(graph);
const input = {
sourceEvidenceId,
graph,
nativeEvidence: [],
direction: "both" as const,
};
const generic = traceApplicationFeature({
...input,
seed: { kind: "string", ...mode },
});
const typed = traceApplicationFeature({
...input,
seed: { kind: "api", ...mode },
});
expect(typed.coverage).toEqual({
status: "complete-within-source",
source_graph_status: "complete",
total_seed_matches: 1,
});
expect(typed.seed_matches).toEqual(generic.seed_matches);
expect(typed.seed_matches).toEqual([
{
node_id: graph.nodes[0]?.node_id,
kind: "context-bridge-api",
basis: "property",
field: "observations[0].properties.members[1]",
},
]);
expect(typed.graph?.nodes).toEqual(graph.nodes);
expect(typed.graph?.edges).toEqual([]);
expect(typed.evidence_links).toEqual([sourceEvidenceId]);
expect(typed.summary).toMatchObject({
matched_seeds: 1,
traced_nodes: 1,
traced_edges: 0,
observed_facts: 1,
inferred_facts: 0,
unknown_facts: 0,
unavailable_facts: 0,
});
expect(typed.native_handoffs).toEqual([]);
expect(graph).toEqual(before);
});
it("keeps a missing member explicit without changing source coverage", () => {
const graph = contextBridgeGraph();
const result = traceApplicationFeature({
sourceEvidenceId,
graph,
nativeEvidence: [],
direction: "both",
seed: seed("api", "missingProject"),
});
expect(result.graph).toBeNull();
expect(result.seed_matches).toEqual([]);
expect(result.coverage).toEqual({
status: "no-match",
source_graph_status: "complete",
total_seed_matches: 0,
});
expect(result.evidence_links).toEqual([sourceEvidenceId]);
expect(result.limitations).toContain(
"No graph entity matched the literal seed; this is not evidence that the feature is absent.",
);
});
it("retains incomplete source coverage when an array member matches", () => {
const graph = contextBridgeGraph();
const { graph_id: _graphId, ...content } = graph;
const partialGraph = createJavaScriptApplicationGraph({
...content,
limitations: ["Only one source file was analyzed."],
coverage: {
status: "partial",
truncated: false,
omitted_count: null,
limits: [],
},
});
const result = traceApplicationFeature({
sourceEvidenceId,
graph: partialGraph,
nativeEvidence: [],
direction: "both",
seed: seed("api"),
});
expect(result.coverage).toEqual({
status: "partial",
source_graph_status: "partial",
total_seed_matches: 1,
});
expect(result.graph?.coverage).toEqual(partialGraph.coverage);
expect(result.graph?.nodes).toEqual(partialGraph.nodes);
expect(result.limitations).toContain(
"The source application graph is incomplete; unmatched seeds and frontiers remain unknown.",
);
});
});
const sourceEvidenceId = `ev_${"b".repeat(64)}`;
const seed = (
kind: ApplicationFeatureSeed["kind"],
value = "openProject",
): ApplicationFeatureSeed => ({
kind,
value,
match: "exact",
case_sensitive: true,
});
const memberNode = (
kind: ApplicationNode["kind"],
properties: Readonly<Record<string, JsonValue>>,
): ApplicationNode =>
createJavaScriptApplicationNode({
kind,
identity: {
strategy: "canonical-path",
stability: "artifact-version",
artifact_sha256: "a".repeat(64),
path: "preload.js",
},
observations: [
{
label: "desktopApi",
properties,
evidence: {
...artifactEvidence(
"a".repeat(64),
"preload.js",
"ast-static-analysis",
),
evidence_ids: [sourceEvidenceId],
},
},
],
});
const propertyMatch = (node: ApplicationNode, field: string) => ({
node_id: node.node_id,
kind: node.kind,
basis: "property",
field: `observations[0].properties.${field}`,
});
const contextBridgeGraph = () => {
const analysis = analyzeJavaScriptStaticSource(
"const { contextBridge } = require('electron');\ncontextBridge.exposeInMainWorld('desktopApi', { openProject() {}, closeProject() {} });",
);
expect(analysis.parse_status).toBe("complete");
const finding = analysis.electron.context_bridge_apis[0];
if (finding === undefined) throw new Error("Expected contextBridge finding");
expect(finding.api_key).toBe("desktopApi");
expect(finding.members).toEqual(["closeProject", "openProject"]);
expect(finding.unknown_members).toBe(0);
const node = memberNode("context-bridge-api", {
api_name: finding.api_key,
members: [...finding.members],
unknown_members: finding.unknown_members,
});
return createJavaScriptApplicationGraph({
schema: "JavaScriptApplicationGraph",
root_node_ids: [node.node_id],
nodes: [node],
edges: [],
coverage: completeCoverage,
limitations: [],
});
};