Files
reverse-engineering-anything/tests/acceptance/applications/javascriptExportBindingModuleView.test.ts
T
908d9c0726 fix(javascript): keep export bindings out of module views (#9486)
* fix(javascript): keep export bindings out of module views

Exported symbol bindings reuse the javascript-module node kind, so the modules collection counted one row per export alongside the owning module. On a CommonJS or ESM file with N exports, inspect_analysis_view reported N+1 module units, module pages listed indistinguishable rows sharing the module path, and item selection by selector.path failed with "Multiple modules have path".

Treat a node whose observations carry semantic_role export-binding as an export of its owning module, not a module unit, and exclude it from module entries. Export bindings remain distinct graph entities for export-shape comparison and version matching.

Validation: a domain regression in analysisView.test.ts and a CLI acceptance regression over a real CommonJS analysis both fail on main and pass with this change. Also ran the analysis view, MCP boundary, CLI boundary and JavaScript acceptance suites (125 tests), npm run check:fast, and the MCP catalog schema test after regenerating .cache/mcp-tool-catalog.json.

* test(javascript): run the export-binding view case on the shared CLI

The case starts a compiled CLI process for each of its three commands,
paying startup and tool registration three times per run. The fixture
already provides a file-scoped process for calls with no stdin or explicit
timeout, so use it.

* style(javascript): format the constructed-evidence type in the module-view case

The acceptance case's inline graph type did not match what oxfmt emits for the
80-column limit, so `format:check` refused the tree. Reflow only; the assertions
are unchanged.

* style(cli): wrap the native data type path description

The `inspect-native-data-type` argument description line is 82 columns, over the repository's 80-column `.oxfmtrc.json` limit, so `npm run format:check` fails on main and on every branch merged from it. It came in with #9465 and the following commits to this file did not touch it. Reformat the one call the formatter objects to; no behaviour change.

---------

Co-authored-by: 2SinceJuly <202121119+2SinceJuly@users.noreply.github.com>
Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
2026-11-09 19:42:47 +08:00

145 lines
5.0 KiB
TypeScript

import { applicationFeatureTraceResultSchema } from "../../../src/domain/javascript/javascriptFeatureTraceSchemas.js";
import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import { expect } from "vitest";
import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisView.js";
import { parseEvidence } from "../../../src/domain/evidence.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { cliTest, runFileCli } from "../../support/cli/cliFixture.js";
cliTest(
"pages only module units for a CommonJS file that exports symbols",
async ({ cli }) => {
const root = await createTestTempDirectory("rea-js-export-binding-view-");
await writeFile(
join(root, "package.json"),
JSON.stringify({ name: "export-binding-view", version: "1.0.0" }),
);
await writeFile(
join(root, "index.js"),
[
"exports.alpha = function alpha() { return 1; };",
"exports.beta = function beta() { return 2; };",
].join("\n"),
);
const analyzed = await runFileCli(cli, {
arguments: ["analyze-javascript-application", root, "--format", "json"],
});
expect(analyzed.exitCode, analyzed.stdout + analyzed.stderr).toBe(0);
const evidence = parseEvidence(analyzed.json);
const nodeIds = (
evidence.normalized_result as {
graph: {
nodes: { node_id: string; observations: { properties: unknown }[] }[];
};
}
).graph.nodes;
const exportBindingIds = nodeIds
.filter(({ observations }) =>
observations.some(
({ properties }) =>
(properties as Record<string, unknown>).semantic_role ===
"export-binding",
),
)
.map(({ node_id }) => node_id);
expect(exportBindingIds).toHaveLength(2);
const traceInput = join(root, "trace.json");
for (const seed of [
{ kind: "module", value: "index.js", match: "contains" },
{ kind: "node-id", value: exportBindingIds[0], match: "exact" },
]) {
await writeFile(
traceInput,
JSON.stringify({ application: evidence, seed }),
);
const traced = await runFileCli(cli, {
arguments: ["trace-application-feature", traceInput, "--json"],
});
expect(traced.exitCode, traced.stdout + traced.stderr).toBe(0);
const trace = applicationFeatureTraceResultSchema.parse(
parseEvidence(traced.json).normalized_result,
);
expect(trace.seed_matches.length).toBeGreaterThan(0);
if (seed.kind === "module") {
expect(
trace.seed_matches.filter((match) =>
exportBindingIds.includes(match.node_id),
),
).toEqual([]);
} else {
expect(trace.seed_matches.map((match) => match.node_id)).toEqual([
exportBindingIds[0],
]);
}
}
const evidencePath = join(root, "evidence.json");
await writeFile(evidencePath, JSON.stringify(evidence));
const queryPath = join(root, "query.json");
const inspect = async (view: Record<string, unknown>) => {
await writeFile(
queryPath,
JSON.stringify({
source: { kind: "evidence-file", path: evidencePath },
view,
}),
);
const result = await runFileCli(cli, {
arguments: ["inspect-analysis-view", queryPath, "--json"],
});
expect(result.exitCode, result.stdout + result.stderr).toBe(0);
return analysisViewResultSchema.parse(
parseEvidence(result.json).normalized_result,
);
};
const page = await inspect({
kind: "page",
collection: "modules",
offset: 0,
limit: 100,
});
if (page.kind !== "page") throw new Error("expected modules page");
const pageNodeIds = page.items.map((item) =>
String((item as { node_id?: unknown }).node_id),
);
expect(
pageNodeIds.filter((nodeId) => exportBindingIds.includes(nodeId)),
).toEqual([]);
expect(page.coverage).toMatchObject({
total: page.items.length,
examined: page.items.length,
exhausted: true,
});
// The remaining path ambiguity names only module units, never export
// bindings, so an ambiguous selector stays actionable.
await writeFile(
queryPath,
JSON.stringify({
source: { kind: "evidence-file", path: evidencePath },
view: {
kind: "item",
collection: "modules",
selector: { path: "index.js" },
},
}),
);
const item = await cli.run({
arguments: ["inspect-analysis-view", queryPath, "--json"],
});
expect(item.exitCode).not.toBe(0);
const failure = JSON.parse(item.stdout) as {
details?: { issues?: { expected?: { node_id: string }[] }[] };
};
const candidates =
failure.details?.issues?.flatMap(({ expected }) => expected ?? []) ?? [];
expect(candidates.length).toBeGreaterThan(0);
expect(
candidates.filter(({ node_id }) => exportBindingIds.includes(node_id)),
).toEqual([]);
},
);