mirror of
https://github.com/morluto/rea
synced 2026-10-27 06:33:32 -04:00
71b696582b
* perf(test): reuse one compiled CLI for export-binding parity The slow export-binding cases each spawned the compiled CLI three times. One warm process now serves those JSON commands from a single command instance, so startup and retained registrations are paid once per file. Co-authored-by: morluto <morluto@users.noreply.github.com> * style(test): format the warm CLI session oxfmt --check rejected the session and its worker. Co-authored-by: morluto <morluto@users.noreply.github.com> * fix(test): report warm CLI shutdown, deadlines, and exit status A compiled CLI session that needed a signal after stdin closed reported success. A command that never answered left later checks waiting, and a failure recorded only on process.exitCode was returned as status 0. Co-authored-by: morluto <morluto@users.noreply.github.com> * fix(test): close the warm CLI worker when a status check fails The failure-then-success case only closed the compiled worker after both assertions passed. Register the close with the test so a failed check still stops the child. Co-authored-by: morluto <morluto@users.noreply.github.com> * fix(test): retain a timed-out compiled CLI worker until it exits A request deadline cleared the child before SIGKILL was observed. The next command could start a replacement, and close could miss the original process. Retired workers stay owned until exit, and a failed stop remains for a later close to retry. Co-authored-by: morluto <morluto@users.noreply.github.com> * fix(test): reject a compiled CLI launch that close interrupts Closing during the ready handshake detached the child before the launch promise settled. The queued command then waited on the startup timer. Close now rejects that startup before the child is detached. Co-authored-by: morluto <morluto@users.noreply.github.com> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: morluto <morluto@users.noreply.github.com>
1335 lines
48 KiB
TypeScript
1335 lines
48 KiB
TypeScript
import { execFile } from "node:child_process";
|
|
import { createHash } from "node:crypto";
|
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
import { createRequire } from "node:module";
|
|
import { join, resolve } from "node:path";
|
|
import { promisify } from "node:util";
|
|
|
|
import { Client } from "@modelcontextprotocol/client";
|
|
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
|
|
import { afterAll, describe, expect, it } from "vitest";
|
|
import { z } from "zod";
|
|
|
|
import { closeVerifierResources } from "../../../scripts/lib/verifier-cleanup.mjs";
|
|
import { javascriptApplicationAnalysisResultSchema } from "../../../src/domain/javascript/javascriptApplicationAnalysis.js";
|
|
import { javaScriptExportShapeComparisonResultSchema } from "../../../src/domain/javascript/javascriptExportShapeComparisonSchemas.js";
|
|
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
|
import {
|
|
startCompiledCliSession,
|
|
type CompiledCliSession,
|
|
} from "../../support/cli/compiledCliSession.js";
|
|
|
|
const execute = promisify(execFile);
|
|
const requireFixture = createRequire(import.meta.url);
|
|
const comparisonEnvelope = z.object({
|
|
normalized_result: javaScriptExportShapeComparisonResultSchema,
|
|
});
|
|
const applicationEnvelope = z.object({
|
|
normalized_result: javascriptApplicationAnalysisResultSchema,
|
|
});
|
|
let cliSession: CompiledCliSession | undefined;
|
|
|
|
// Source semantics use one real stdio MCP session. Cases that also check the
|
|
// compiled CLI adapter share one warm process: it loads the production command
|
|
// runtime once and serves the same JSON commands as scripts/rea.mjs.
|
|
let mcpConnection:
|
|
| {
|
|
readonly client: Client;
|
|
readonly transport: StdioClientTransport;
|
|
readonly ready: Promise<void>;
|
|
}
|
|
| undefined;
|
|
|
|
afterAll(async () => {
|
|
if (mcpConnection !== undefined) {
|
|
try {
|
|
await mcpConnection.ready;
|
|
} catch {
|
|
// Keep the failed connection owned until both resources are closed below.
|
|
}
|
|
}
|
|
await closeVerifierResources([
|
|
async () => {
|
|
await mcpConnection?.client.close();
|
|
},
|
|
async () => {
|
|
await mcpConnection?.transport.close();
|
|
},
|
|
async () => {
|
|
await cliSession?.close();
|
|
},
|
|
]);
|
|
});
|
|
|
|
describe("exported function binding resolution through stdio MCP and CLI", () => {
|
|
it.each([
|
|
{
|
|
name: "a function declaration exported by a specifier",
|
|
exportName: "default",
|
|
source: (count: number) =>
|
|
`function current() { return { count: 0 }; }
|
|
current = () => ({ count: ${String(count)} });
|
|
export { current as default };`,
|
|
},
|
|
{
|
|
name: "a directly exported function declaration",
|
|
exportName: "current",
|
|
source: (count: number) =>
|
|
`export function current() { return { count: 0 }; }
|
|
current = () => ({ count: ${String(count)} });`,
|
|
},
|
|
{
|
|
name: "a named default function declaration",
|
|
exportName: "default",
|
|
source: (count: number) =>
|
|
`export default function current() { return { count: 0 }; }
|
|
current = () => ({ count: ${String(count)} });`,
|
|
},
|
|
{
|
|
name: "a directly exported arrow binding",
|
|
exportName: "current",
|
|
source: (count: number) =>
|
|
`export let current = () => ({ count: 0 });
|
|
current = () => ({ count: ${String(count)} });`,
|
|
},
|
|
{
|
|
name: "an arrow binding exported by a specifier",
|
|
exportName: "default",
|
|
source: (count: number) =>
|
|
`let current = () => ({ count: 0 });
|
|
current = () => ({ count: ${String(count)} });
|
|
export { current as default };`,
|
|
},
|
|
{
|
|
name: "a reassigned alias",
|
|
exportName: "default",
|
|
source: (count: number) =>
|
|
`const original = () => ({ count: 0 });
|
|
let current = original;
|
|
current = () => ({ count: ${String(count)} });
|
|
export { current as default };`,
|
|
},
|
|
{
|
|
name: "a conditional function reassignment",
|
|
exportName: "current",
|
|
source: (count: number) =>
|
|
`export function current() { return { count: 0 }; }
|
|
if (true) current = () => ({ count: ${String(count)} });`,
|
|
},
|
|
])(
|
|
"keeps $name uncertain after reassignment",
|
|
async ({ source, exportName }) => {
|
|
const fixture = await analyzeVersions(source, exportName);
|
|
await expectRuntimeExports(fixture, (count) => ({ count }));
|
|
const result = await compareThroughMcp(fixture.input);
|
|
expectUncertainComparison(result);
|
|
for (const evidence of [fixture.input.left, fixture.input.right]) {
|
|
const { normalized_result: analysis } = z
|
|
.object({
|
|
normalized_result: javascriptApplicationAnalysisResultSchema,
|
|
})
|
|
.parse(evidence);
|
|
expect(
|
|
analysis.graph.nodes
|
|
.flatMap(({ observations }) => observations)
|
|
.filter(
|
|
({ properties }) =>
|
|
properties.semantic_role === "export-return-shapes" &&
|
|
properties.exported_name === exportName,
|
|
),
|
|
).toEqual([]);
|
|
}
|
|
},
|
|
);
|
|
|
|
it("does not associate a private function name with a numeric export through stdio MCP", async () => {
|
|
const fixture = await analyzeVersions(
|
|
(count) =>
|
|
`const hidden = function current() { return { count: 0 }; };
|
|
const current = ${String(count)};
|
|
export { current as default };`,
|
|
"default",
|
|
);
|
|
await expectRuntimeExports(fixture, (count) => count);
|
|
const comparison = await compareThroughMcp(fixture.input);
|
|
expectUncertainComparison(comparison);
|
|
});
|
|
|
|
it("does not treat rebinding the CommonJS exports alias as a default export", async () => {
|
|
const fixture = await analyzeVersions(
|
|
(count) =>
|
|
`exports = function parse() { return { kind: "result", count: ${String(count)} }; };`,
|
|
"default",
|
|
"app.cjs",
|
|
);
|
|
|
|
// This is a test-owned fixture, so Node's actual CommonJS loader is the
|
|
// oracle for what the application exports.
|
|
expect(requireFixture(fixture.leftPath)).toEqual({});
|
|
expect(requireFixture(fixture.rightPath)).toEqual({});
|
|
|
|
const mcp = await compareThroughMcp(fixture.input);
|
|
await expectCliAnalysisParity(fixture);
|
|
expect(mcp).toMatchObject({
|
|
left: { status: "missing" },
|
|
right: { status: "missing" },
|
|
summary: { added: 0, removed: 0, changed: 0 },
|
|
});
|
|
expect(await compareThroughCli(fixture)).toEqual(mcp);
|
|
});
|
|
|
|
it.each([
|
|
["the CommonJS exports alias", "exports"],
|
|
["an ordinary object property", "holder.value"],
|
|
])(
|
|
"resolves exported callable values through = chains ending at %s",
|
|
async (_, target) => {
|
|
const fixture = await analyzeVersions(
|
|
(count) =>
|
|
`const holder = {};\nfunction parse() { return { kind: "result", count: ${String(count)} }; }\nmodule.exports = ${target} = parse;`,
|
|
"default",
|
|
"app.cjs",
|
|
);
|
|
expect(requireFixture(fixture.leftPath)()).toEqual({
|
|
kind: "result",
|
|
count: 1,
|
|
});
|
|
expect(requireFixture(fixture.rightPath)()).toEqual({
|
|
kind: "result",
|
|
count: 2,
|
|
});
|
|
for (const evidence of [fixture.input.left, fixture.input.right])
|
|
expectExportedCallableBinding(evidence, "parse");
|
|
const comparison = await compareThroughMcp(fixture.input);
|
|
expectCountChange(comparison);
|
|
if (target === "exports") {
|
|
await expectCliAnalysisParity(fixture);
|
|
expect(await compareThroughCli(fixture)).toEqual(comparison);
|
|
}
|
|
},
|
|
);
|
|
});
|
|
|
|
describe("CommonJS receiver lifetimes through stdio MCP and CLI", () => {
|
|
it.each([
|
|
"module.exports = {}; exports.parse = VALUE;",
|
|
"exports = {}; exports.parse = VALUE;",
|
|
"exports.parse = VALUE; module.exports = {};",
|
|
"module.exports.parse = VALUE; module.exports = {};",
|
|
"module.exports = {}; exports = module.exports; module.exports = {}; exports.parse = VALUE;",
|
|
'module.exports = {}; exports.parse = "enabled";',
|
|
'module.exports = {}; Reflect.set(module, "other", exports); exports.parse = VALUE;',
|
|
"module.exports = {}; let wrapper = module; wrapper = {}; Object.assign(wrapper, { exports }); exports.parse = VALUE;",
|
|
])("excludes detached public export receivers: %s", async (source) => {
|
|
const fixture = await analyzeVersions(
|
|
(count) =>
|
|
source.replace(
|
|
"VALUE",
|
|
`() => ({ kind: "result", count: ${String(count)} })`,
|
|
),
|
|
"parse",
|
|
"app.cjs",
|
|
);
|
|
for (const path of [fixture.leftPath, fixture.rightPath]) {
|
|
const value = requireFixture(path);
|
|
expect(Object.hasOwn(value, "parse")).toBe(false);
|
|
expect(value.parse).toBeUndefined();
|
|
}
|
|
for (const evidence of [fixture.input.left, fixture.input.right]) {
|
|
const { graph } = applicationEnvelope.parse(evidence).normalized_result;
|
|
expect(
|
|
graph.nodes
|
|
.flatMap(({ observations }) => observations)
|
|
.filter(
|
|
({ properties }) =>
|
|
properties.exported_name === "parse" &&
|
|
["export-binding", "export-return-shapes"].includes(
|
|
String(properties.semantic_role),
|
|
),
|
|
),
|
|
).toEqual([]);
|
|
}
|
|
await expectCliAnalysisParity(fixture);
|
|
const comparison = await compareThroughMcp(fixture.input);
|
|
expect(comparison).toMatchObject({
|
|
left: { status: "missing" },
|
|
right: { status: "missing" },
|
|
summary: { added: 0, removed: 0, changed: 0 },
|
|
});
|
|
expect(await compareThroughCli(fixture)).toEqual(comparison);
|
|
});
|
|
|
|
it.each([
|
|
"exports.parse = VALUE;",
|
|
"module.exports = {}; module.exports.parse = VALUE;",
|
|
"module.exports = {}; exports = module.exports; exports.parse = VALUE;",
|
|
"exports.parse = VALUE; Object.assign(module.exports, { other: exports });",
|
|
])("retains connected public export receivers: %s", async (source) => {
|
|
const fixture = await analyzeVersions(
|
|
(count) =>
|
|
source.replace(
|
|
"VALUE",
|
|
`() => ({ kind: "result", count: ${String(count)} })`,
|
|
),
|
|
"parse",
|
|
"app.cjs",
|
|
);
|
|
for (const [path, count] of [
|
|
[fixture.leftPath, 1],
|
|
[fixture.rightPath, 2],
|
|
] as const)
|
|
expect(requireFixture(path).parse()).toEqual({ kind: "result", count });
|
|
await expectCliAnalysisParity(fixture);
|
|
const comparison = await compareThroughMcp(fixture.input);
|
|
expectCountChange(comparison);
|
|
expect(await compareThroughCli(fixture)).toEqual(comparison);
|
|
});
|
|
|
|
it.each([
|
|
"if (false) module.exports = {}; exports.parse = VALUE;",
|
|
"exports.parse = VALUE; if (false) module.exports = {};",
|
|
"function replace() { module.exports = {}; } exports.parse = VALUE;",
|
|
"function replace() { exports = {}; } exports.parse = VALUE;",
|
|
'const holder = {}; ({ [(module.exports = exports, "x")]: holder.x } = (module.exports = {})); exports.parse = VALUE;',
|
|
'const { [(module.exports = exports, "x")]: value } = (module.exports = {}); exports.parse = VALUE;',
|
|
"const [value = (module.exports = exports)] = (module.exports = []); exports.parse = VALUE;",
|
|
"module.exports = {}; Object.assign(module, { exports }); exports.parse = VALUE;",
|
|
'module.exports = {}; Object.defineProperty(module, "exports", { value: exports }); exports.parse = VALUE;',
|
|
'module.exports = {}; Reflect.set(module, "exports", exports); exports.parse = VALUE;',
|
|
"module.exports = {}; const assign = Object.assign; assign.call(null, module, { exports }); exports.parse = VALUE;",
|
|
"module.exports = {}; const wrapper = module; Object.assign(wrapper, { exports }); exports.parse = VALUE;",
|
|
])(
|
|
"keeps execution-dependent receiver identity uncertain: %s",
|
|
async (source) => {
|
|
const fixture = await analyzeVersions(
|
|
(count) =>
|
|
source.replace(
|
|
"VALUE",
|
|
`() => ({ kind: "result", count: ${String(count)} })`,
|
|
),
|
|
"parse",
|
|
"app.cjs",
|
|
);
|
|
for (const [path, count] of [
|
|
[fixture.leftPath, 1],
|
|
[fixture.rightPath, 2],
|
|
] as const)
|
|
expect(requireFixture(path).parse()).toEqual({ kind: "result", count });
|
|
for (const evidence of [fixture.input.left, fixture.input.right])
|
|
expectUnresolvedExportBinding(evidence, "parse");
|
|
await expectCliAnalysisParity(fixture);
|
|
const comparison = await compareThroughMcp(fixture.input);
|
|
expectUncertainComparison(comparison);
|
|
expect(await compareThroughCli(fixture)).toEqual(comparison);
|
|
},
|
|
);
|
|
});
|
|
|
|
describe("CommonJS assignment-chain receiver identity through stdio MCP", () => {
|
|
it("does not export a named write whose captured module.exports receiver is replaced", async () => {
|
|
const fixture = await analyzeVersions(
|
|
(count) =>
|
|
`const VERSION = ${String(count)};\nmodule.exports.parse = (module.exports = () => ({ count: VERSION }));`,
|
|
"parse",
|
|
"app.cjs",
|
|
);
|
|
for (const path of [fixture.leftPath, fixture.rightPath]) {
|
|
const value = requireFixture(path);
|
|
expect(typeof value).toBe("function");
|
|
expect(Reflect.ownKeys(value)).toEqual(["length", "name"]);
|
|
}
|
|
for (const evidence of [fixture.input.left, fixture.input.right]) {
|
|
const analysis = z
|
|
.object({
|
|
normalized_result: javascriptApplicationAnalysisResultSchema,
|
|
})
|
|
.parse(evidence).normalized_result;
|
|
expect(
|
|
analysis.graph.nodes
|
|
.flatMap(({ observations }) => observations)
|
|
.filter(
|
|
({ properties }) =>
|
|
properties.semantic_role === "export-binding" &&
|
|
properties.exported_name === "parse",
|
|
),
|
|
).toEqual([]);
|
|
}
|
|
const comparison = await compareThroughMcp(fixture.input);
|
|
expect(comparison).toMatchObject({
|
|
left: { status: "missing" },
|
|
right: { status: "missing" },
|
|
summary: { added: 0, removed: 0, changed: 0 },
|
|
});
|
|
});
|
|
|
|
it("keeps a named property export when its receiver remains stable through the RHS chain", async () => {
|
|
const fixture = await analyzeVersions(
|
|
(count) =>
|
|
`const holder = {};\nfunction parse() { return { kind: "result", count: ${String(count)} }; }\nmodule.exports.parse = holder.value = parse;`,
|
|
"parse",
|
|
"app.cjs",
|
|
);
|
|
for (const path of [fixture.leftPath, fixture.rightPath])
|
|
expect(requireFixture(path).parse()).toEqual({
|
|
kind: "result",
|
|
count: path === fixture.leftPath ? 1 : 2,
|
|
});
|
|
const comparison = await compareThroughMcp(fixture.input);
|
|
expectCountChange(comparison);
|
|
});
|
|
|
|
it("keeps receiver identity unresolved when module.exports is reassigned to the same function", async () => {
|
|
const fixture = await analyzeVersions(
|
|
(count) =>
|
|
`const VERSION = ${String(count)};\nfunction parse() { return { count: VERSION }; }\nmodule.exports = parse;\nmodule.exports.parse = (module.exports = parse);`,
|
|
"parse",
|
|
"app.cjs",
|
|
);
|
|
for (const path of [fixture.leftPath, fixture.rightPath]) {
|
|
const value = requireFixture(path);
|
|
expect(value.parse).toBe(value);
|
|
expect(value.parse()).toEqual({
|
|
count: path === fixture.leftPath ? 1 : 2,
|
|
});
|
|
}
|
|
for (const evidence of [fixture.input.left, fixture.input.right])
|
|
expectUnresolvedExportBinding(evidence, "parse");
|
|
const comparison = await compareThroughMcp(fixture.input);
|
|
expect(comparison).toMatchObject({
|
|
left: { status: "unavailable" },
|
|
right: { status: "unavailable" },
|
|
});
|
|
});
|
|
|
|
it("keeps receiver replacement by an existing function unresolved", async () => {
|
|
const fixture = await analyzeVersions(
|
|
(count) =>
|
|
`const VERSION = ${String(count)};\nfunction parse() { return { count: VERSION }; }\nmodule.exports.parse = (module.exports = parse);`,
|
|
"parse",
|
|
"app.cjs",
|
|
);
|
|
for (const path of [fixture.leftPath, fixture.rightPath]) {
|
|
const value = requireFixture(path);
|
|
expect(typeof value).toBe("function");
|
|
expect(value.parse).toBeUndefined();
|
|
expect(value()).toEqual({
|
|
count: path === fixture.leftPath ? 1 : 2,
|
|
});
|
|
}
|
|
for (const evidence of [fixture.input.left, fixture.input.right])
|
|
expectUnresolvedExportBinding(evidence, "parse");
|
|
const comparison = await compareThroughMcp(fixture.input);
|
|
expect(comparison).toMatchObject({
|
|
left: { status: "unavailable" },
|
|
right: { status: "unavailable" },
|
|
});
|
|
});
|
|
|
|
it("resolves a nested lexical parse binding in an ordinary module.exports chain", async () => {
|
|
const fixture = await analyzeVersions(
|
|
(count) =>
|
|
`function parse() { return { count: VERSION }; }\nfunction configure() { const parse = () => ({ count: 7 }); let local; module.exports = local = parse; }\nconst VERSION = ${String(count)};\nconfigure();`,
|
|
"default",
|
|
"app.cjs",
|
|
);
|
|
for (const path of [fixture.leftPath, fixture.rightPath])
|
|
expect(requireFixture(path)()).toEqual({ count: 7 });
|
|
for (const evidence of [fixture.input.left, fixture.input.right])
|
|
expectExportedCallableBinding(evidence, "parse");
|
|
const comparison = await compareThroughMcp(fixture.input);
|
|
expect(comparison).toMatchObject({
|
|
summary: { added: 0, removed: 0, changed: 0, unknown: 0 },
|
|
changes: [],
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("CommonJS export identity and scope regressions", () => {
|
|
it("does not export a named write through the original exports alias after replacement", async () => {
|
|
const fixture = await analyzeVersions(
|
|
(count) =>
|
|
`const VERSION = ${String(count)};\nexports.parse = (module.exports = () => ({ count: VERSION }));`,
|
|
"parse",
|
|
"app.cjs",
|
|
);
|
|
for (const path of [fixture.leftPath, fixture.rightPath]) {
|
|
const value = requireFixture(path);
|
|
expect(typeof value).toBe("function");
|
|
expect(Reflect.ownKeys(value)).toEqual(["length", "name"]);
|
|
}
|
|
const comparison = await compareThroughMcp(fixture.input);
|
|
expect(comparison).toMatchObject({
|
|
left: { status: "missing" },
|
|
right: { status: "missing" },
|
|
summary: { added: 0, removed: 0, changed: 0 },
|
|
});
|
|
});
|
|
|
|
it("keeps a dead var shadow of the alias-chain RHS unavailable", async () => {
|
|
const fixture = await analyzeVersions(
|
|
(count) =>
|
|
`function parse() { return { count: ${String(count)} }; }\nfunction configure() { if (false) { var parse = () => ({ count: 7 }); } module.exports = exports = parse; }\nconfigure();`,
|
|
"default",
|
|
"app.cjs",
|
|
);
|
|
expect(requireFixture(fixture.leftPath)).toBeUndefined();
|
|
expect(requireFixture(fixture.rightPath)).toBeUndefined();
|
|
const comparison = await compareThroughMcp(fixture.input);
|
|
expectUncertainComparison(comparison);
|
|
});
|
|
|
|
it.each(["+=", "||=", "??="] as const)(
|
|
"does not treat module.exports %s as a definite function export",
|
|
async (operator) => {
|
|
const fixture = await analyzeVersions(
|
|
(count) =>
|
|
`module.exports ${operator} (() => ({ kind: "result", count: ${String(count)} }));`,
|
|
"default",
|
|
"app.cjs",
|
|
);
|
|
const left = requireFixture(fixture.leftPath);
|
|
const right = requireFixture(fixture.rightPath);
|
|
if (operator === "+=") {
|
|
expect(typeof left).toBe("string");
|
|
expect(typeof right).toBe("string");
|
|
} else {
|
|
expect(left).toEqual({});
|
|
expect(right).toEqual({});
|
|
}
|
|
const comparison = await compareThroughMcp(fixture.input);
|
|
expectUncertainComparison(comparison);
|
|
},
|
|
);
|
|
|
|
it("keeps a parameter-default mutation unknown in a CommonJS return shape", async () => {
|
|
const fixture = await analyzeVersions(
|
|
(count) =>
|
|
`const source = { value: 0 }; module.exports = exports = function current(arg = null, set = (arg = source)) { var arg; arg.value = ${String(count)}; return { kind: "result", count: source.value }; };`,
|
|
"default",
|
|
"app.cjs",
|
|
);
|
|
expect(requireFixture(fixture.leftPath)()).toEqual({
|
|
kind: "result",
|
|
count: 1,
|
|
});
|
|
expect(requireFixture(fixture.rightPath)()).toEqual({
|
|
kind: "result",
|
|
count: 2,
|
|
});
|
|
for (const evidence of [fixture.input.left, fixture.input.right]) {
|
|
const analysis = z
|
|
.object({
|
|
normalized_result: javascriptApplicationAnalysisResultSchema,
|
|
})
|
|
.parse(evidence).normalized_result;
|
|
const shapes = analysis.graph.nodes
|
|
.flatMap(({ observations }) => observations)
|
|
.find(
|
|
({ properties }) =>
|
|
properties.semantic_role === "export-return-shapes" &&
|
|
properties.exported_name === "default",
|
|
)?.properties.static_return_shapes;
|
|
expect(shapes).toEqual([
|
|
expect.objectContaining({
|
|
fields: expect.arrayContaining([
|
|
expect.objectContaining({ path: "/count", state: "unknown" }),
|
|
]),
|
|
}),
|
|
]);
|
|
}
|
|
const comparison = await compareThroughMcp(fixture.input);
|
|
expect(comparison).toMatchObject({
|
|
left: { status: "selected" },
|
|
right: { status: "selected" },
|
|
summary: { added: 0, removed: 0, changed: 0 },
|
|
changes: [],
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("stable exported function bindings through stdio MCP", () => {
|
|
it.each([
|
|
{
|
|
name: "an anonymous default",
|
|
source: (count: number) =>
|
|
`export default () => ({ kind: "result", count: ${String(count)} });`,
|
|
},
|
|
{
|
|
name: "a named default declaration",
|
|
source: (count: number) =>
|
|
`export default function current() { return { kind: "result", count: ${String(count)} }; }`,
|
|
},
|
|
{
|
|
name: "a private named default expression",
|
|
source: (count: number) =>
|
|
`export default (function current() { return { kind: "result", count: ${String(count)} }; });`,
|
|
},
|
|
{
|
|
name: "a declaration alias",
|
|
source: (count: number) =>
|
|
`function original() { return { kind: "result", count: ${String(count)} }; }
|
|
const current = original; export { current as default };`,
|
|
},
|
|
{
|
|
name: "an unchanged mutable binding",
|
|
source: (count: number) =>
|
|
`let current = () => ({ kind: "result", count: ${String(count)} }); export { current as default };`,
|
|
},
|
|
{
|
|
name: "a private function reached through its real binding",
|
|
source: (count: number) =>
|
|
`const current = function privateName() { return { kind: "result", count: ${String(count)} }; };
|
|
const privateName = 99; const alias = current; export { alias as default };`,
|
|
},
|
|
{
|
|
name: "a declaration sharing a private display name",
|
|
source: (count: number) =>
|
|
`const hidden = function current() { return { count: 99 }; };
|
|
function current() { return { kind: "result", count: ${String(count)} }; }
|
|
export { current as default };`,
|
|
},
|
|
])("retains exact comparison for $name", async ({ source }) => {
|
|
const fixture = await analyzeVersions(source, "default");
|
|
expectCountChange(await compareThroughMcp(fixture.input));
|
|
});
|
|
|
|
it.each([
|
|
{
|
|
name: "a local callable captured by an identifier default export",
|
|
source: (count: number) =>
|
|
`const run = () => ({ kind: "result", count: ${String(count)} });\nexport default run;`,
|
|
runtimeValue: (count: number) => ({ kind: "result", count }),
|
|
expected: "known",
|
|
},
|
|
{
|
|
name: "the latest callable assigned before the default export",
|
|
source: (count: number) =>
|
|
`let run = () => ({ count: 0 });\nrun = () => ({ kind: "result", count: ${String(count)} });\nexport default run;`,
|
|
runtimeValue: (count: number) => ({ kind: "result", count }),
|
|
expected: "known",
|
|
},
|
|
{
|
|
name: "a callable assigned only after the default export capture",
|
|
source: (count: number) =>
|
|
`var run;\nexport default run;\nrun = () => ({ kind: "result", count: ${String(count)} });`,
|
|
runtimeValue: () => undefined,
|
|
expected: "unknown",
|
|
},
|
|
{
|
|
name: "a reassignment after the default export capture",
|
|
source: (count: number) =>
|
|
`let run = () => ({ kind: "result", count: ${String(count)} });\nexport default run;\nrun = () => ({ count: 99 });`,
|
|
runtimeValue: (count: number) => ({ kind: "result", count }),
|
|
expected: "known",
|
|
},
|
|
] as const)(
|
|
"uses the value at $name",
|
|
async ({ source, runtimeValue, expected }) => {
|
|
const fixture = await analyzeVersions(source, "default");
|
|
await expectRuntimeExports(fixture, runtimeValue);
|
|
const mcp = await compareThroughMcp(fixture.input);
|
|
const cli = await compareThroughCli(fixture);
|
|
expect(cli).toEqual(mcp);
|
|
if (expected === "known") expectCountChange(mcp);
|
|
else expectUncertainComparison(mcp);
|
|
},
|
|
);
|
|
});
|
|
|
|
describe("export bindings with loop and assignment-only writes", () => {
|
|
it.each([
|
|
["a skipped branch", "if (false)"],
|
|
["a zero-iteration loop", "for (; false;)"],
|
|
["an unselected switch case", "switch (0) { case 1:"],
|
|
["an interrupted try block", "try { throw 1;"],
|
|
])("does not infer a callable initialized in %s", async (_, prefix) => {
|
|
const fixture = await analyzeVersions((count) => {
|
|
const initializer = `var current = () => ({ count: ${String(count)} });`;
|
|
const statement = prefix.startsWith("try")
|
|
? `${prefix} ${initializer} } catch {}`
|
|
: prefix.startsWith("switch")
|
|
? `${prefix} ${initializer} }`
|
|
: `${prefix} { ${initializer} }`;
|
|
return `${statement} export { current as default };`;
|
|
}, "default");
|
|
await expectRuntimeExports(fixture, () => undefined);
|
|
expectUncertainComparison(await compareThroughMcp(fixture.input));
|
|
});
|
|
|
|
it("retains a var initializer in an unconditional nested block", async () => {
|
|
const fixture = await analyzeVersions(
|
|
(count) =>
|
|
`{ var current = () => ({ kind: "result", count: ${String(count)} }); }
|
|
export { current as default };`,
|
|
"default",
|
|
);
|
|
await expectRuntimeExports(fixture, (count) => ({ kind: "result", count }));
|
|
expectCountChange(await compareThroughMcp(fixture.input));
|
|
});
|
|
|
|
it.each([
|
|
{
|
|
name: "a for-of var declaration",
|
|
loop: (count: number) =>
|
|
`for (var current of [() => ({ count: ${String(count)} })]) {}`,
|
|
},
|
|
{
|
|
name: "a destructured for-of var declaration",
|
|
loop: (count: number) =>
|
|
`for (var { value: current } of [{ value: () => ({ count: ${String(count)} }) }]) {}`,
|
|
},
|
|
])("keeps $name uncertain", async ({ loop }) => {
|
|
const fixture = await analyzeVersions(
|
|
(count) =>
|
|
`function current() { return { count: 0 }; }
|
|
${loop(count)}
|
|
module.exports = current;`,
|
|
"default",
|
|
"app.cjs",
|
|
);
|
|
await expectRuntimeExports(fixture, (count) => ({ count }));
|
|
expectUncertainComparison(await compareThroughMcp(fixture.input));
|
|
});
|
|
|
|
it("does not associate a for-in key with the overwritten function", async () => {
|
|
const fixture = await analyzeVersions(
|
|
(count) =>
|
|
`function current() { return { count: 0 }; }
|
|
for (var current in { ${String(count)}: true }) {}
|
|
module.exports = current;`,
|
|
"default",
|
|
"app.cjs",
|
|
);
|
|
await expectRuntimeExports(fixture, String);
|
|
expectUncertainComparison(await compareThroughMcp(fixture.input));
|
|
});
|
|
|
|
it("preserves an outer export shadowed by a lexical loop declaration", async () => {
|
|
const fixture = await analyzeVersions(
|
|
(count) =>
|
|
`export function current() { return { kind: "result", count: ${String(count)} }; }
|
|
for (let current of [() => ({ count: 0 })]) { current = null; }`,
|
|
"current",
|
|
);
|
|
await expectRuntimeExports(fixture, (count) => ({ kind: "result", count }));
|
|
expectCountChange(await compareThroughMcp(fixture.input));
|
|
});
|
|
|
|
it.each([
|
|
{
|
|
name: "a parameter",
|
|
source: (count: number) =>
|
|
`function install(current) {
|
|
if (false) current = () => ({ count: 0 });
|
|
module.exports = current;
|
|
}
|
|
install(${String(count)});`,
|
|
},
|
|
{
|
|
name: "a catch binding",
|
|
source: (count: number) =>
|
|
`try { throw ${String(count)}; } catch (current) {
|
|
if (false) current = () => ({ count: 0 });
|
|
module.exports = current;
|
|
}`,
|
|
},
|
|
{
|
|
name: "a parameter redeclared with var",
|
|
source: (count: number) =>
|
|
`function install(current) {
|
|
if (false) { var current = () => ({ count: 0 }); }
|
|
module.exports = current;
|
|
}
|
|
install(${String(count)});`,
|
|
},
|
|
])("does not infer $name from its only assignment", async ({ source }) => {
|
|
const fixture = await analyzeVersions(source, "default", "app.cjs");
|
|
await expectRuntimeExports(fixture, (count) => count);
|
|
const comparison = await compareThroughMcp(fixture.input);
|
|
expectUncertainComparison(comparison);
|
|
});
|
|
});
|
|
|
|
describe("export binding lexical scope and declaration order", () => {
|
|
it.each([
|
|
{
|
|
name: "a write before a hoisted function declaration",
|
|
source: (count: number) =>
|
|
`current = () => ({ count: ${String(count)} });
|
|
export function current() { return { count: 0 }; }`,
|
|
},
|
|
{
|
|
name: "a nested write collected before its outer declaration",
|
|
source: (count: number) =>
|
|
`function replace() { current = () => ({ count: ${String(count)} }); }
|
|
export function current() { return { count: 0 }; }
|
|
replace();`,
|
|
},
|
|
])("keeps $name uncertain", async ({ source }) => {
|
|
const fixture = await analyzeVersions(source, "current");
|
|
await expectRuntimeExports(fixture, (count) => ({ count }));
|
|
expectUncertainComparison(await compareThroughMcp(fixture.input));
|
|
});
|
|
|
|
it("does not let a later local declaration contaminate the exported outer binding", async () => {
|
|
const fixture = await analyzeVersions(
|
|
(count) =>
|
|
`export function current() { return { kind: "result", count: ${String(count)} }; }
|
|
function isolated() {
|
|
current = () => ({ count: 9 });
|
|
function current() { return { count: 0 }; }
|
|
}
|
|
isolated();`,
|
|
"current",
|
|
);
|
|
await expectRuntimeExports(fixture, (count) => ({ kind: "result", count }));
|
|
expectCountChange(await compareThroughMcp(fixture.input));
|
|
});
|
|
|
|
it("does not associate a nested CommonJS numeric export with an outer function", async () => {
|
|
const fixture = await analyzeVersions(
|
|
(count) =>
|
|
`function current() { return { count: 0 }; }
|
|
function install() { const current = ${String(count)}; module.exports = current; }
|
|
install();`,
|
|
"default",
|
|
"app.cjs",
|
|
);
|
|
await expectRuntimeExports(fixture, (count) => count);
|
|
const comparison = await compareThroughMcp(fixture.input);
|
|
expectUncertainComparison(comparison);
|
|
});
|
|
|
|
it.each([
|
|
{
|
|
name: "a nested function-local alias",
|
|
source: (count: number) =>
|
|
`function current() { return { kind: "result", count: 0 }; }
|
|
function install() {
|
|
const actual = () => ({ kind: "result", count: ${String(count)} });
|
|
const current = actual; module.exports = current;
|
|
}
|
|
install();`,
|
|
},
|
|
{
|
|
name: "a block-local function",
|
|
source: (count: number) =>
|
|
`const current = 0;
|
|
{ const current = () => ({ kind: "result", count: ${String(count)} });
|
|
module.exports = current; }`,
|
|
},
|
|
])("compares $name through its real CommonJS binding", async ({ source }) => {
|
|
const fixture = await analyzeVersions(source, "default", "app.cjs");
|
|
await expectRuntimeExports(fixture, (count) => ({ kind: "result", count }));
|
|
expectCountChange(await compareThroughMcp(fixture.input));
|
|
});
|
|
});
|
|
|
|
describe("export writes in function evaluation contexts", () => {
|
|
it.each([
|
|
{ body: "var arg; arg.value = COUNT;", uncertain: true },
|
|
{ body: "var arg = arg; arg.value = COUNT;", uncertain: true },
|
|
{ body: "arg.value = COUNT; var arg;", uncertain: true },
|
|
{ body: "arg.value = COUNT; var arg = {};", uncertain: true },
|
|
{ body: "var arg = {}; arg.value = COUNT;", uncertain: false },
|
|
{ body: "var arg; arg = {}; arg.value = COUNT;", uncertain: false },
|
|
{ body: "function arg() {} arg.value = COUNT;", uncertain: false },
|
|
])("retains parameter-copy timing for $body", async ({ body, uncertain }) => {
|
|
const fixture = await analyzeVersions(
|
|
(count) =>
|
|
`const source = { value: 0 };
|
|
export function current(arg = null, set = (arg = source)) {
|
|
${body.replace("COUNT", String(count))}
|
|
return { kind: "result", count: source.value };
|
|
}`,
|
|
"current",
|
|
);
|
|
await expectRuntimeExports(fixture, (count) => ({
|
|
kind: "result",
|
|
count: uncertain ? count : 0,
|
|
}));
|
|
for (const evidence of [fixture.input.left, fixture.input.right]) {
|
|
const { normalized_result: analysis } = z
|
|
.object({
|
|
normalized_result: javascriptApplicationAnalysisResultSchema,
|
|
})
|
|
.parse(evidence);
|
|
const observations = analysis.graph.nodes.flatMap(
|
|
({ observations }) => observations,
|
|
);
|
|
expect(observations).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
properties: expect.objectContaining({
|
|
semantic_role: "export-return-shapes",
|
|
exported_name: "current",
|
|
static_return_shapes: [
|
|
expect.objectContaining({
|
|
fields: expect.arrayContaining([
|
|
expect.objectContaining({
|
|
path: "/count",
|
|
state: uncertain ? "unknown" : "literal",
|
|
value: uncertain ? null : 0,
|
|
}),
|
|
]),
|
|
}),
|
|
],
|
|
}),
|
|
}),
|
|
]),
|
|
);
|
|
}
|
|
});
|
|
|
|
it.each([
|
|
...["let current;", "var current;", "function current() {}"].map(
|
|
(body) => ({
|
|
name: `a parameter default before ${body}`,
|
|
source: (count: number) =>
|
|
`export function current() { return { count: 0 }; }
|
|
function replace(value = (current = () => ({ count: ${String(count)} }))) {
|
|
${body}
|
|
}
|
|
replace();`,
|
|
}),
|
|
),
|
|
{
|
|
name: "a closure invoked from a parameter default",
|
|
source: (count: number) =>
|
|
`export function current() { return { count: 0 }; }
|
|
function replace(value = (() => { current = () => ({ count: ${String(count)} }); })()) {
|
|
let current;
|
|
}
|
|
replace();`,
|
|
},
|
|
{
|
|
name: "a computed destructuring parameter key",
|
|
source: (count: number) =>
|
|
`export function current() { return { count: 0 }; }
|
|
function replace({ [current = () => ({ count: ${String(count)} })]: value }) {
|
|
var current;
|
|
}
|
|
replace({});`,
|
|
},
|
|
{
|
|
name: "an object method's computed key",
|
|
source: (count: number) =>
|
|
`export function current() { return { count: 0 }; }
|
|
const object = { [current = () => ({ count: ${String(count)} })]() { let current; } };`,
|
|
},
|
|
{
|
|
name: "a class method's computed key",
|
|
source: (count: number) =>
|
|
`export function current() { return { count: 0 }; }
|
|
class Example { [current = () => ({ count: ${String(count)} })]() { var current; } }`,
|
|
},
|
|
])("keeps an outer export write in $name uncertain", async ({ source }) => {
|
|
const fixture = await analyzeVersions(source, "current");
|
|
await expectRuntimeExports(fixture, (count) => ({ count }));
|
|
expectUncertainComparison(await compareThroughMcp(fixture.input));
|
|
});
|
|
|
|
it("resolves a CommonJS export in a parameter default outside the function body", async () => {
|
|
const fixture = await analyzeVersions(
|
|
(count) =>
|
|
`function current() { return { kind: "result", count: ${String(count)} }; }
|
|
function install(value = (module.exports = current)) { var current = 9; }
|
|
install();`,
|
|
"default",
|
|
"app.cjs",
|
|
);
|
|
await expectRuntimeExports(fixture, (count) => ({ kind: "result", count }));
|
|
expectCountChange(await compareThroughMcp(fixture.input));
|
|
});
|
|
});
|
|
|
|
it("retains namespace re-export bindings and provenance through stdio MCP and CLI", async () => {
|
|
const root = await createTestTempDirectory("rea-namespace-reexports-");
|
|
const declarations = [
|
|
{ clause: "* as ns", name: "ns", imported: "*" },
|
|
{
|
|
clause: '* as "namespace value"',
|
|
name: "namespace value",
|
|
imported: "*",
|
|
},
|
|
{ clause: "{ value as forwarded }", name: "forwarded", imported: "value" },
|
|
{ clause: "*", name: "*", imported: "*" },
|
|
];
|
|
const source = declarations
|
|
.map(({ clause }) => `export ${clause} from "./dependency.mjs";`)
|
|
.join("\n");
|
|
const path = join(root, "barrel.mjs");
|
|
await Promise.all([
|
|
writeFile(path, source),
|
|
writeFile(join(root, "dependency.mjs"), "export const value = 42;"),
|
|
]);
|
|
// Node executes only this test-owned fixture to establish its actual exports.
|
|
for (const name of ["ns", "namespace value"])
|
|
expect(await runtimeExport(path, name)).toEqual({ value: 42 });
|
|
for (const name of ["forwarded", "value"])
|
|
expect(await runtimeExport(path, name)).toBe(42);
|
|
|
|
const documents = [
|
|
await analyzeThroughMcp(root),
|
|
await runCli(["analyze-javascript-application", root, "--json"]),
|
|
];
|
|
const digest = createHash("sha256").update(source).digest("hex");
|
|
for (const document of documents) {
|
|
const { graph } = applicationEnvelope.parse(document).normalized_result;
|
|
const sourceModule = graph.nodes.find(({ observations }) =>
|
|
observations.some(
|
|
({ properties }) =>
|
|
properties.semantic_role === "source-module" &&
|
|
properties.path === "barrel.mjs",
|
|
),
|
|
);
|
|
expect(sourceModule?.observations[0]?.properties).toMatchObject({
|
|
export_relationships: 4,
|
|
export_names: ["*", "forwarded", "namespace value", "ns"],
|
|
omitted_export_names: 0,
|
|
});
|
|
for (const [index, { clause, name, imported }] of declarations.entries()) {
|
|
const exported = graph.nodes.find(({ observations }) =>
|
|
observations.some(
|
|
({ properties }) =>
|
|
properties.semantic_role === "export-binding" &&
|
|
properties.module_path === "barrel.mjs" &&
|
|
properties.exported_name === name,
|
|
),
|
|
);
|
|
const observation = exported?.observations[0];
|
|
expect(observation?.properties).toMatchObject({
|
|
relationship_kind: "re-export",
|
|
declared_specifier: "./dependency.mjs",
|
|
imported_name: imported,
|
|
local_name: null,
|
|
});
|
|
if (imported === "*" && name !== "*")
|
|
expect(observation?.evidence).toMatchObject({
|
|
artifact: { available: true, sha256: digest },
|
|
location: {
|
|
available: true,
|
|
value: {
|
|
kind: "source-range",
|
|
source: "barrel.mjs",
|
|
start: { line: index + 1, column: 7 },
|
|
end: { line: index + 1, column: 7 + clause.length },
|
|
},
|
|
},
|
|
});
|
|
expect(graph.edges).toContainEqual(
|
|
expect.objectContaining({
|
|
source_node_id: sourceModule?.node_id,
|
|
target_node_id: exported?.node_id,
|
|
relation: "exposes",
|
|
properties: expect.objectContaining({ exported_name: name }),
|
|
evidence: expect.objectContaining({
|
|
artifact: observation?.evidence.artifact,
|
|
location: observation?.evidence.location,
|
|
}),
|
|
}),
|
|
);
|
|
expect(graph.edges).toContainEqual(
|
|
expect.objectContaining({
|
|
source_node_id: exported?.node_id,
|
|
relation: "imports",
|
|
properties: expect.objectContaining({
|
|
module_link_kind: "re-export",
|
|
imported_name: imported,
|
|
exported_name: name,
|
|
resolved_path: "dependency.mjs",
|
|
resolution_status: "resolved",
|
|
}),
|
|
}),
|
|
);
|
|
}
|
|
expect(graph.coverage).toMatchObject({
|
|
status: "complete",
|
|
omitted_count: 0,
|
|
truncated: false,
|
|
});
|
|
}
|
|
});
|
|
|
|
it("excludes erased namespace type exports through stdio MCP and CLI", async () => {
|
|
const root = await createTestTempDirectory("rea-namespace-type-exports-");
|
|
const path = join(root, "barrel.mts");
|
|
await Promise.all([
|
|
writeFile(
|
|
path,
|
|
'export type * as Types from "./types.mjs";\nexport * as Values from "./values.mjs";',
|
|
),
|
|
writeFile(
|
|
join(root, "types.mjs"),
|
|
'throw new Error("Type-only dependency must not execute");',
|
|
),
|
|
writeFile(join(root, "values.mjs"), "export const value = 42;"),
|
|
]);
|
|
// Node strips the type-only declaration without loading its throwing target.
|
|
expect(await runtimeExport(path, "Types")).toBeUndefined();
|
|
expect(await runtimeExport(path, "Values")).toEqual({ value: 42 });
|
|
|
|
const documents = [
|
|
await analyzeThroughMcp(root),
|
|
await runCli(["analyze-javascript-application", root, "--json"]),
|
|
];
|
|
for (const document of documents) {
|
|
const { graph } = applicationEnvelope.parse(document).normalized_result;
|
|
const observations = graph.nodes.flatMap(
|
|
({ observations }) => observations,
|
|
);
|
|
expect(
|
|
observations.find(
|
|
({ properties }) =>
|
|
properties.semantic_role === "source-module" &&
|
|
properties.path === "barrel.mts",
|
|
)?.properties,
|
|
).toMatchObject({
|
|
export_relationships: 1,
|
|
export_names: ["Values"],
|
|
omitted_export_names: 0,
|
|
});
|
|
expect(
|
|
observations.filter(
|
|
({ properties }) =>
|
|
properties.semantic_role === "export-binding" &&
|
|
properties.module_path === "barrel.mts" &&
|
|
properties.exported_name === "Types",
|
|
),
|
|
).toEqual([]);
|
|
expect(
|
|
graph.edges.filter(
|
|
({ properties }) => properties.exported_name === "Types",
|
|
),
|
|
).toEqual([]);
|
|
expect(graph.coverage).toMatchObject({
|
|
status: "complete",
|
|
omitted_count: 0,
|
|
truncated: false,
|
|
});
|
|
}
|
|
});
|
|
|
|
const expectRuntimeExports = async (
|
|
fixture: Awaited<ReturnType<typeof analyzeVersions>>,
|
|
expected: (count: number) => unknown,
|
|
): Promise<void> => {
|
|
// Execute only source-owned fixtures as an independent runtime oracle.
|
|
for (const [path, exportName, count] of [
|
|
[fixture.leftPath, fixture.input.left_export_name, 1],
|
|
[fixture.rightPath, fixture.input.right_export_name, 2],
|
|
] as const)
|
|
expect(await runtimeExport(path, exportName)).toEqual(expected(count));
|
|
};
|
|
|
|
const expectExportedCallableBinding = (
|
|
evidence: unknown,
|
|
localName: string,
|
|
): void => {
|
|
const analysis = z
|
|
.object({
|
|
normalized_result: javascriptApplicationAnalysisResultSchema,
|
|
})
|
|
.parse(evidence).normalized_result;
|
|
const exportBinding = analysis.graph.nodes
|
|
.flatMap(({ observations }) => observations)
|
|
.find(
|
|
({ properties }) =>
|
|
properties.semantic_role === "export-binding" &&
|
|
properties.exported_name === "default",
|
|
);
|
|
expect(exportBinding?.properties.local_name).toBe(localName);
|
|
expect(analysis.semantic_graph.nodes).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({ kind: "function", label: localName }),
|
|
]),
|
|
);
|
|
};
|
|
|
|
const expectUnresolvedExportBinding = (
|
|
evidence: unknown,
|
|
exportName: string,
|
|
): void => {
|
|
const analysis = z
|
|
.object({
|
|
normalized_result: javascriptApplicationAnalysisResultSchema,
|
|
})
|
|
.parse(evidence).normalized_result;
|
|
const observations = analysis.graph.nodes.flatMap(
|
|
({ observations }) => observations,
|
|
);
|
|
expect(
|
|
observations.filter(
|
|
({ properties }) =>
|
|
properties.semantic_role === "export-binding" &&
|
|
properties.exported_name === exportName,
|
|
),
|
|
).not.toHaveLength(0);
|
|
expect(
|
|
observations.filter(
|
|
({ properties }) =>
|
|
properties.semantic_role === "export-return-shapes" &&
|
|
properties.exported_name === exportName,
|
|
),
|
|
).toEqual([]);
|
|
};
|
|
|
|
const expectUncertainComparison = (
|
|
result: Awaited<ReturnType<typeof compareThroughCli>>,
|
|
): void => {
|
|
expect(result).toMatchObject({
|
|
left: { status: "unavailable" },
|
|
right: { status: "unavailable" },
|
|
summary: { added: 0, removed: 0, changed: 0, unknown: 1 },
|
|
changes: [{ status: "unknown", path: "" }],
|
|
coverage: { status: "partial" },
|
|
});
|
|
expect(result.property_inventories).toEqual([]);
|
|
};
|
|
|
|
const expectCountChange = (
|
|
result: Awaited<ReturnType<typeof compareThroughCli>>,
|
|
): void => {
|
|
expect(result).toMatchObject({
|
|
left: { status: "selected" },
|
|
right: { status: "selected" },
|
|
summary: { added: 0, removed: 0, changed: 1, unknown: 0 },
|
|
changes: [
|
|
{
|
|
path: "/count",
|
|
status: "changed",
|
|
left: { availability: "literal", value: 1 },
|
|
right: { availability: "literal", value: 2 },
|
|
},
|
|
],
|
|
coverage: { status: "complete-within-inputs" },
|
|
});
|
|
};
|
|
|
|
const runCli = async (args: readonly string[]): Promise<unknown> => {
|
|
cliSession ??= startCompiledCliSession();
|
|
return cliSession.run(args);
|
|
};
|
|
|
|
const analyzeVersions = async (
|
|
source: (count: number) => string,
|
|
exportName: string,
|
|
fileName = "app.mjs",
|
|
) => {
|
|
const root = await createTestTempDirectory("rea-exported-function-bindings-");
|
|
const leftRoot = join(root, "left");
|
|
const rightRoot = join(root, "right");
|
|
await Promise.all([mkdir(leftRoot), mkdir(rightRoot)]);
|
|
const leftPath = join(leftRoot, fileName);
|
|
const rightPath = join(rightRoot, fileName);
|
|
await Promise.all([
|
|
writeFile(leftPath, source(1)),
|
|
writeFile(rightPath, source(2)),
|
|
]);
|
|
const [left, right] = await Promise.all([
|
|
analyzeThroughMcp(leftRoot),
|
|
analyzeThroughMcp(rightRoot),
|
|
]);
|
|
return {
|
|
root,
|
|
leftPath,
|
|
rightPath,
|
|
input: {
|
|
left,
|
|
right,
|
|
left_module_path: fileName,
|
|
left_export_name: exportName,
|
|
right_module_path: fileName,
|
|
right_export_name: exportName,
|
|
},
|
|
};
|
|
};
|
|
|
|
const analyzeThroughMcp = async (inputPath: string) => {
|
|
const client = await getMcpClient();
|
|
const response = await client.callTool({
|
|
name: "analyze_javascript_application",
|
|
arguments: { input_path: inputPath, format: "auto" },
|
|
});
|
|
expect(response.isError).not.toBe(true);
|
|
return response.structuredContent;
|
|
};
|
|
|
|
const expectCliAnalysisParity = async (
|
|
fixture: Awaited<ReturnType<typeof analyzeVersions>>,
|
|
): Promise<void> => {
|
|
for (const [root, evidence] of [
|
|
[join(fixture.root, "left"), fixture.input.left],
|
|
[join(fixture.root, "right"), fixture.input.right],
|
|
] as const) {
|
|
const cli = applicationEnvelope.parse(
|
|
await runCli(["analyze-javascript-application", root, "--json"]),
|
|
);
|
|
const mcp = applicationEnvelope.parse(evidence);
|
|
expect(cli.normalized_result).toEqual(mcp.normalized_result);
|
|
}
|
|
};
|
|
|
|
const compareThroughCli = async (
|
|
fixture: Awaited<ReturnType<typeof analyzeVersions>>,
|
|
) => {
|
|
const inputPath = join(fixture.root, "comparison.json");
|
|
await writeFile(inputPath, JSON.stringify(fixture.input));
|
|
return comparisonEnvelope.parse(
|
|
await runCli(["compare-javascript-export-shapes", inputPath, "--json"]),
|
|
).normalized_result;
|
|
};
|
|
|
|
const compareThroughMcp = async (input: Record<string, unknown>) => {
|
|
const client = await getMcpClient();
|
|
const response = await client.callTool({
|
|
name: "compare_javascript_export_shapes",
|
|
arguments: input,
|
|
});
|
|
expect(response.isError).not.toBe(true);
|
|
await client.ping();
|
|
return comparisonEnvelope.parse(response.structuredContent).normalized_result;
|
|
};
|
|
|
|
const getMcpClient = async (): Promise<Client> => {
|
|
if (mcpConnection === undefined) {
|
|
const transport = new StdioClientTransport({
|
|
command: process.execPath,
|
|
args: [resolve("scripts/rea.mjs"), "mcp"],
|
|
cwd: process.cwd(),
|
|
env: { PATH: process.env.PATH ?? "", REA_LOG_LEVEL: "silent" },
|
|
stderr: "pipe",
|
|
});
|
|
transport.stderr?.on("data", () => undefined);
|
|
const client = new Client({
|
|
name: "exported-function-bindings",
|
|
version: "1",
|
|
});
|
|
mcpConnection = {
|
|
client,
|
|
transport,
|
|
ready: client.connect(transport),
|
|
};
|
|
}
|
|
await mcpConnection.ready;
|
|
return mcpConnection.client;
|
|
};
|
|
|
|
const runtimeExport = async (
|
|
path: string,
|
|
exportName: string,
|
|
): Promise<unknown> => {
|
|
const { stdout } = await execute(process.execPath, [
|
|
"--input-type=module",
|
|
"-e",
|
|
`import { pathToFileURL } from 'node:url';
|
|
const module = await import(pathToFileURL(process.argv[1]).href);
|
|
const value = module[process.argv[2]];
|
|
console.log(JSON.stringify({ value: typeof value === 'function' ? value() : value }));`,
|
|
path,
|
|
exportName,
|
|
]);
|
|
return z.object({ value: z.unknown().optional() }).parse(JSON.parse(stdout))
|
|
.value;
|
|
};
|