Files
reverse-engineering-anything/tests/acceptance/applications/applicationWorkflowCli.test.ts
T
morluto ffe0574e17 fix(artifacts)!: retain cleanup owners through client shutdown
Keep failed reader, root descriptor, and output-tree cleanup with the caller. Drain admitted work before close, retry the same retained owners, and block new resource work while cleanup is incomplete. Preserve primary failures and complete inventory observations.

Thread ownership through JavaScript reconstruction, the CLI's one-shot scope, MCP server shutdown, verifier scripts, and test fixtures. Remove redundant ownership flags and the unused validated test wrapper.

Validate with 850 passing focused tests across 90 suites (two skipped), targeted classification and cleanup retries, static and unused-code checks, and real macOS CLI/MCP directory, ZIP, and DMG workflows. Native Windows and deep-analysis providers were not verified.

BREAKING CHANGE: Direct artifact inventory/extraction and JavaScript analysis APIs require an explicit caller-owned ArtifactResourceScope. CLI and MCP input contracts are unchanged.
2026-10-11 04:43:02 +08:00

936 lines
30 KiB
TypeScript

import { execFile } from "node:child_process";
import { copyFile, mkdir, rm, writeFile } from "node:fs/promises";
import { join, relative, resolve } from "node:path";
import { promisify } from "node:util";
import { Client } from "@modelcontextprotocol/client";
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
import { afterEach, describe, expect, it } from "vitest";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import {
JAVASCRIPT_FEATURE_TRACE_EXAMPLE,
JAVASCRIPT_APPLICATION_VERSION_COMPARISON_EXAMPLE,
SOURCE_TO_BUNDLE_COMPARISON_EXAMPLE,
} from "../../../src/contracts/javascript/javascriptApplicationWorkflowExamples.js";
import { JAVASCRIPT_EXPORT_SHAPE_COMPARISON_EXAMPLE } from "../../../src/contracts/javascript/javascriptExportShapeComparisonExample.js";
import { analyzeJavaScriptApplication } from "../../support/javascriptApplicationScope.js";
import {
javascriptApplicationAnalysisResultSchema,
type JavaScriptApplicationAnalysisResult,
} from "../../../src/domain/javascript/javascriptApplicationAnalysis.js";
import { z } from "zod";
const execute = promisify(execFile);
const temporary: string[] = [];
afterEach(async () => {
await Promise.all(
temporary
.splice(0)
.map(async (path) => rm(path, { recursive: true, force: true })),
);
});
const writeSelfImportFixture = async (root: string): Promise<void> => {
await Promise.all([
writeFile(
join(root, "esm.js"),
'import "./esm.js"; export const value = 1;\n',
),
writeFile(
join(root, "common.cjs"),
'const self = require("./common.cjs"); module.exports = self;\n',
),
writeFile(
join(root, "util.ts"),
'import "./util.ts"; export const value = 1;\n',
),
]);
};
const expectSelfImportEvidence = (
graph: JavaScriptApplicationAnalysisResult["graph"],
): void => {
const rawSelfImports = graph.edges.filter(
({ relation, properties, evidence }) =>
relation === "imports" &&
(properties.kind === "static-import" || properties.kind === "require") &&
evidence.location.available &&
evidence.location.value.kind === "source-range" &&
((properties.specifier === "./esm.js" &&
properties.kind === "static-import" &&
evidence.location.value.source === "esm.js") ||
(properties.specifier === "./common.cjs" &&
properties.kind === "require" &&
evidence.location.value.source === "common.cjs") ||
(properties.specifier === "./util.ts" &&
properties.kind === "static-import" &&
evidence.location.value.source === "util.ts")),
);
expect(rawSelfImports).toEqual(
expect.arrayContaining([
expect.objectContaining({
properties: expect.objectContaining({
kind: "static-import",
specifier: "./esm.js",
}),
evidence: expect.objectContaining({
authority: "static-relationship-inference",
location: expect.objectContaining({
available: true,
value: expect.objectContaining({
kind: "source-range",
source: "esm.js",
start: expect.objectContaining({ line: 1, column: 0 }),
}),
}),
}),
}),
expect.objectContaining({
properties: expect.objectContaining({
kind: "require",
specifier: "./common.cjs",
}),
evidence: expect.objectContaining({
authority: "static-relationship-inference",
location: expect.objectContaining({
available: true,
value: expect.objectContaining({
kind: "source-range",
source: "common.cjs",
start: expect.objectContaining({ line: 1, column: 13 }),
}),
}),
}),
}),
expect.objectContaining({
properties: expect.objectContaining({
kind: "static-import",
specifier: "./util.ts",
}),
evidence: expect.objectContaining({
authority: "static-relationship-inference",
location: expect.objectContaining({
available: true,
value: expect.objectContaining({
kind: "source-range",
source: "util.ts",
start: expect.objectContaining({ line: 1, column: 0 }),
}),
}),
}),
}),
]),
);
expect(rawSelfImports).toHaveLength(3);
};
const analyzeThroughStdioMcp = async (
inputPath: string,
): Promise<JavaScriptApplicationAnalysisResult> => {
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",
});
const client = new Client({
name: "javascript-self-import-parity",
version: "1",
});
try {
await client.connect(transport);
const response = await client.callTool({
name: "analyze_javascript_application",
arguments: { input_path: inputPath },
});
expect(response.isError).not.toBe(true);
expect(response.content).toEqual(
expect.arrayContaining([expect.objectContaining({ type: "text" })]),
);
const result = z
.object({ normalized_result: javascriptApplicationAnalysisResultSchema })
.parse(response.structuredContent).normalized_result;
expect(result).toMatchObject({
input_path: inputPath,
format: "directory",
});
return result;
} finally {
try {
await client.close();
} finally {
await transport.close();
}
}
};
describe("JavaScript application path CLI", () => {
it("accepts a relative local application path and preserves canonical Evidence identity", async () => {
const root = await createTestTempDirectory("rea-relative-application-cli-");
temporary.push(root);
await writeFile(join(root, "app.js"), "export const value = 1;\n");
const absolute = await analyzeJavaScriptApplication({ input_path: root });
if (!absolute.ok) throw absolute.error;
const absoluteAnalysis = javascriptApplicationAnalysisResultSchema.parse(
absolute.value.normalized_result,
);
const relativePath = relative(process.cwd(), root);
const fromCli = await runCli([
"analyze-javascript-application",
relativePath,
"--json",
]);
expect(fromCli).toMatchObject({
evidence_id: absolute.value.evidence_id,
normalized_result: {
input_path: absoluteAnalysis.input_path,
},
subject: { local_path: absolute.value.subject?.local_path },
});
}, 20_000);
it("returns the self-import graph consistently through CLI and stdio MCP", async () => {
const root = await createTestTempDirectory(
"rea-self-import-application-cli-",
);
temporary.push(root);
await writeSelfImportFixture(root);
const analyzed = await runCli([
"analyze-javascript-application",
root,
"--json",
]);
const result = z
.object({ normalized_result: javascriptApplicationAnalysisResultSchema })
.parse(analyzed);
const graph = result.normalized_result.graph;
expectSelfImportEvidence(graph);
const mcpResult = await analyzeThroughStdioMcp(root);
expect(mcpResult.graph).toEqual(graph);
expect(mcpResult.semantic_graph).toEqual(
result.normalized_result.semantic_graph,
);
}, 20_000);
});
describe("application workflow CLI parity", () => {
it("accepts inline trace JSON and file-backed comparison JSON", async () => {
const traced = await runCli([
"trace-application-feature",
JSON.stringify(JAVASCRIPT_FEATURE_TRACE_EXAMPLE),
"--json",
]);
expect(traced).toMatchObject({
operation: "trace_application_feature",
predicate_type: "rea.application-feature-trace",
normalized_result: {
seed: { kind: "module", value: "renderer.js", match: "exact" },
summary: {
matched_seeds: 1,
traced_nodes: 1,
traced_edges: 0,
terminal_paths: 0,
native_handoffs: 0,
observed_facts: 1,
inferred_facts: 0,
unknown_facts: 0,
unavailable_facts: 0,
},
coverage: {
status: "complete-within-source",
source_graph_status: "complete",
total_seed_matches: 1,
},
},
});
const root = await createTestTempDirectory("rea-application-cli-");
temporary.push(root);
const comparisonPath = join(root, "comparison.json");
await writeFile(
comparisonPath,
JSON.stringify(JAVASCRIPT_APPLICATION_VERSION_COMPARISON_EXAMPLE),
);
const compared = await runCli([
"compare-application-versions",
comparisonPath,
"--json",
]);
expect(compared).toMatchObject({
operation: "compare_application_versions",
predicate_type: "rea.application-version-comparison",
normalized_result: {
summary: {
unchanged: 0,
added: 2,
removed: 0,
changed: 1,
unknown: 0,
},
coverage: {
left_graph_status: "complete",
right_graph_status: "complete",
left_graph_omitted_count: 0,
right_graph_omitted_count: 0,
status: "complete-within-inputs",
},
},
});
const sourceCompared = await runCli([
"compare-source-to-bundle",
JSON.stringify({
reference: SOURCE_TO_BUNDLE_COMPARISON_EXAMPLE.reference,
application: JAVASCRIPT_FEATURE_TRACE_EXAMPLE.application,
}),
"--json",
]);
expect(sourceCompared).toMatchObject({
operation: "compare_source_to_bundle",
predicate_type: "rea.source-to-bundle-comparison",
normalized_result: {
reference: { inventory_state: "complete" },
summary: {
unchanged: 0,
modified: 0,
removed: 1,
split: 0,
merged: 0,
duplicated: 0,
unknown: 0,
},
coverage: {
status: "complete-within-inputs",
reference_inventory_state: "complete",
application_graph_status: "complete",
retained_source_files: 1,
retained_application_nodes: 1,
},
},
});
}, 20_000);
it("traces the same authenticated semantic graph through the CLI", async () => {
const root = await createTestTempDirectory("rea-semantic-cli-");
temporary.push(root);
await writeFile(
join(root, "app.js"),
"function add(value) { return value + 1; } add(2);",
);
const analyzed = await analyzeJavaScriptApplication({
input_path: root,
});
if (!analyzed.ok) throw analyzed.error;
const result = javascriptApplicationAnalysisResultSchema.parse(
analyzed.value.normalized_result,
);
const seed = result.semantic_graph.relations[0]?.source_node_id;
if (seed === undefined)
throw new TypeError("Expected at least one semantic relation");
const traced = await runCli([
"trace-javascript-semantics",
JSON.stringify({
application: analyzed.value,
query: {
seed: { kind: "semantic-node", node_id: seed },
direction: "forward-influence",
include_ambiguous_dynamic_edges: true,
},
}),
"--json",
]);
expect(traced).toMatchObject({
operation: "trace_javascript_semantics",
predicate_type: "rea.javascript-semantic-trace",
normalized_result: {
source_evidence_id: analyzed.value.evidence_id,
source_graph_id: result.semantic_graph.graph_id,
},
});
}, 20_000);
});
describe("rest parameter semantic trace CLI", () => {
it("traces each ordinary rest argument to its parameter through public application Evidence", async () => {
const root = await createTestTempDirectory("rea-rest-arguments-cli-");
await writeFile(
join(root, "app.js"),
"function collect(first, ...rest) { return rest; } collect('first', 'second', 'third');",
);
const application = await runCli([
"analyze-javascript-application",
root,
"--json",
]);
const analyzed = await analyzeJavaScriptApplication({ input_path: root });
if (!analyzed.ok) throw analyzed.error;
const graph = javascriptApplicationAnalysisResultSchema.parse(
analyzed.value.normalized_result,
).semantic_graph;
for (const index of [1, 2]) {
const argument = graph.nodes.find(
({ kind, label }) =>
kind === "expression" && label === `argument ${String(index)}`,
);
if (argument === undefined) throw new Error("Missing argument node");
const traced = await runCli([
"trace-javascript-semantics",
JSON.stringify({
application,
query: {
seed: { kind: "semantic-node", node_id: argument.node_id },
direction: "forward-influence",
allowed_relations: ["argument-to-parameter"],
expected: { role: "sink", classes: ["parameter"] },
},
}),
"--json",
]);
expect(traced).toMatchObject({
normalized_result: {
status: "found",
nodes: expect.arrayContaining([
expect.objectContaining({ kind: "parameter", label: "rest" }),
]),
},
});
}
}, 20_000);
});
describe("empty property key application CLI", () => {
it.each(["const root = routes[''];", "const {'': root} = routes;"])(
"analyzes a root-route dictionary with %s",
async (read) => {
const root = await createTestTempDirectory("rea-empty-key-cli-");
await writeFile(
join(root, "app.js"),
`const routes = {'': 'HOME'}; ${read}`,
);
const evidence = await runCli([
"analyze-javascript-application",
root,
"--json",
]);
expect(evidence).toMatchObject({
operation: "analyze_javascript_application",
normalized_result: {
semantic_graph: {
nodes: expect.arrayContaining([
expect.objectContaining({
kind: "property-slot",
label: '""',
properties: expect.objectContaining({ name: "" }),
}),
]),
},
},
});
},
20_000,
);
});
describe("application workflow CLI input", () => {
it("rejects Evidence ID-only workflow inputs", async () => {
const result = await runCli([
"compare-application-versions",
JSON.stringify({
left: JAVASCRIPT_APPLICATION_VERSION_COMPARISON_EXAMPLE.left
.evidence_id,
right:
JAVASCRIPT_APPLICATION_VERSION_COMPARISON_EXAMPLE.right.evidence_id,
}),
"--json",
]);
expect(result).toMatchObject({ code: "invalid_request" });
});
});
describe("application workflow CLI copy boundaries", () => {
it("compares source values excluded from escaped rest and spread copies", async () => {
const root = await createTestTempDirectory("rea-copy-boundary-cli-");
temporary.push(root);
const sources = [
`export default function make() {
function mutate(value) { value.changed = true; }
const objectRest = { only: { value: "TOKEN" } };
const { only, ...restObject } = objectRest;
mutate(restObject);
const arrayRest = [{ value: "TOKEN" }];
const [head, ...restArray] = arrayRest;
mutate(restArray);
const objectSpread = { child: { value: "TOKEN" } };
const spreadObject = { ...objectSpread, child: {} };
mutate(spreadObject.child);
const arraySpread = [{ value: "TOKEN" }];
const spreadArray = [{}, ...arraySpread];
mutate(spreadArray[0]);
return {
kind: "copy-boundary",
objectRest: objectRest.only.value,
arrayRest: arrayRest[0].value,
objectSpread: objectSpread.child.value,
arraySpread: arraySpread[0].value,
};
}`,
`export default function make() {
return {
kind: "copy-boundary",
objectRest: "UPDATED", arrayRest: "UPDATED",
objectSpread: "UPDATED", arraySpread: "UPDATED",
};
}`,
];
const [left, right] = await analyzeCliSources(root, sources);
const inputPath = join(root, "comparison.json");
await writeFile(
inputPath,
JSON.stringify({
left,
right,
left_module_path: "parser.mjs",
left_export_name: "default",
right_module_path: "parser.mjs",
right_export_name: "default",
}),
);
const compared = await runCli([
"compare-javascript-export-shapes",
inputPath,
"--json",
]);
expect(compared).toMatchObject({
normalized_result: {
summary: { added: 0, removed: 0, changed: 4, unknown: 0 },
coverage: { status: "complete-within-inputs" },
changes: expect.arrayContaining(
["/arrayRest", "/arraySpread", "/objectRest", "/objectSpread"].map(
(path) =>
expect.objectContaining({
status: "changed",
path,
left: { availability: "literal", value: "TOKEN" },
right: { availability: "literal", value: "UPDATED" },
}),
),
),
},
});
}, 20_000);
});
describe("application workflow CLI and MCP capture lifetimes", () => {
it("compares captured primitives and source values after aliases are replaced", async () => {
const root = await createTestTempDirectory("rea-capture-lifetime-cli-");
temporary.push(root);
const fields = [
"rebound",
"rest",
"snapshot",
"destructured",
"objectSnapshot",
"arraySnapshot",
"unusedFallback",
"nestedSnapshot",
"declaratorSnapshot",
];
const sources = [
`export default function make() {
function mutate(value) { value.value = "MUTATED"; }
const original = {value: "TOKEN"};
let alias = original;
alias = {};
mutate(alias);
const child = {value: "TOKEN"};
let {...rest} = {child};
rest = {child: {}};
rest.child.value = "MUTATED";
const source = {value: "TOKEN"};
const snapshot = source.value;
const {value: destructured} = source;
const objectSnapshot = {value: source.value};
const arraySnapshot = [source.value];
mutate(source);
const fallback = {value: "TOKEN"};
const container = {child: {}};
const {child: selected = fallback} = container;
mutate(selected);
const nestedSource = {value: "TOKEN"};
const nestedSnapshot = nestedSource.value;
const ignored = mutate(nestedSource);
const declaratorSource = {value: "TOKEN"},
declaratorSnapshot = declaratorSource.value,
declaratorIgnored = mutate(declaratorSource);
return {
kind: "capture-boundary",
rebound: original.value, rest: child.value,
snapshot, destructured,
objectSnapshot: objectSnapshot.value, arraySnapshot: arraySnapshot[0],
unusedFallback: fallback.value, nestedSnapshot, declaratorSnapshot,
};
}`,
`export default function make() { return ${JSON.stringify({
kind: "capture-boundary",
...Object.fromEntries(fields.map((field) => [field, "UPDATED"])),
})}; }`,
];
const [left, right] = await analyzeCliSources(root, sources);
// Execute the authored fixtures independently of static analysis.
for (const [index, value] of ["TOKEN", "UPDATED"].entries()) {
const { stdout } = await execute(process.execPath, [
"--input-type=module",
"--eval",
"const module = await import(process.argv[1]); console.log(JSON.stringify(module.default()));",
join(root, String(index), "parser.mjs"),
]);
expect(JSON.parse(stdout)).toEqual({
kind: "capture-boundary",
...Object.fromEntries(fields.map((field) => [field, value])),
});
}
const input = {
left,
right,
left_module_path: "parser.mjs",
left_export_name: "default",
right_module_path: "parser.mjs",
right_export_name: "default",
};
const inputPath = join(root, "comparison.json");
await writeFile(inputPath, JSON.stringify(input));
const expected = {
normalized_result: {
summary: { added: 0, removed: 0, changed: fields.length, unknown: 0 },
coverage: { status: "complete-within-inputs" },
changes: expect.arrayContaining(
fields.map((field) =>
expect.objectContaining({
path: `/${field}`,
status: "changed",
left: { availability: "literal", value: "TOKEN" },
right: { availability: "literal", value: "UPDATED" },
}),
),
),
},
};
expect(
await runCli(["compare-javascript-export-shapes", inputPath, "--json"]),
).toMatchObject(expected);
expect(await compareThroughStdioMcp(input)).toMatchObject(expected);
}, 20_000);
});
describe("application workflow CLI export Evidence", () => {
it("preserves uncertainty after a helper can mutate an awaited return object", async () => {
const root = await createTestTempDirectory("rea-awaited-export-shape-cli-");
temporary.push(root);
const sources = [
`export default async function make() {
const result = { kind: "record" };
function mutate(value) { value.extra = 1; }
mutate(await result);
return result;
}`,
'export default async function make() { return { kind: "record", extra: 1 }; }',
];
const [left, right] = await analyzeCliSources(root, sources);
const compared = await runCli([
"compare-javascript-export-shapes",
JSON.stringify({
left,
right,
left_module_path: "parser.mjs",
left_export_name: "default",
right_module_path: "parser.mjs",
right_export_name: "default",
}),
"--json",
]);
expect(compared).toMatchObject({
operation: "compare_javascript_export_shapes",
normalized_result: {
summary: { added: 0, removed: 0, changed: 0 },
coverage: { status: "partial" },
changes: expect.arrayContaining([
expect.objectContaining({ status: "unknown" }),
]),
},
});
}, 20_000);
it("compares exact export shapes from file-backed Evidence", async () => {
const root = await createTestTempDirectory("rea-export-shape-cli-");
temporary.push(root);
const leftRoot = join(root, "left");
const rightRoot = join(root, "right");
await Promise.all([mkdir(leftRoot), mkdir(rightRoot)]);
await Promise.all([
copyFile(
join(process.cwd(), "tests/fixtures/replay/parser.mjs"),
join(leftRoot, "parser.mjs"),
),
copyFile(
join(process.cwd(), "tests/fixtures/replay/parser-v2.mjs"),
join(rightRoot, "parser.mjs"),
),
]);
const [left, right] = await Promise.all([
analyzeJavaScriptApplication({
input_path: leftRoot,
}),
analyzeJavaScriptApplication({
input_path: rightRoot,
}),
]);
if (!left.ok) throw left.error;
if (!right.ok) throw right.error;
const inputPath = join(root, "comparison.json");
await writeFile(
inputPath,
JSON.stringify({
left: left.value,
right: right.value,
left_module_path: "parser.mjs",
left_export_name: "default",
right_module_path: "parser.mjs",
right_export_name: "default",
}),
);
const compared = await runCli([
"compare-javascript-export-shapes",
inputPath,
"--json",
]);
expect(compared).toMatchObject({
operation: "compare_javascript_export_shapes",
predicate_type: "rea.javascript-export-shape-comparison",
normalized_result: {
summary: { added: 1, removed: 0, changed: 0, unknown: 0 },
changes: [
{
status: "added",
path: "/depth",
presence: { left: "absent", right: "present" },
right: { availability: "literal", value: 1 },
},
],
},
});
}, 20_000);
});
describe("application workflow CLI export property presence", () => {
it("reports tagged search property presence through compare-javascript-export-shapes", async () => {
const root = await createTestTempDirectory(
"rea-export-shape-presence-cli-",
);
temporary.push(root);
const leftRoot = join(root, "left");
const rightRoot = join(root, "right");
await Promise.all([mkdir(leftRoot), mkdir(rightRoot)]);
await Promise.all([
writeFile(
join(leftRoot, "search.js"),
[
"export function search(items, q) {",
" const matches = items.filter((item) => item.includes(q));",
' return { kind: "results", matches, count: matches.length };',
"}",
"",
].join("\n"),
),
writeFile(
join(rightRoot, "search.js"),
[
"export function search(items, q) {",
" const matches = items.filter((item) => item.includes(q));",
' return { kind: "results", matches, total: matches.length, query: String(q) };',
"}",
"",
].join("\n"),
),
]);
const [left, right] = await Promise.all([
analyzeJavaScriptApplication({ input_path: leftRoot }),
analyzeJavaScriptApplication({ input_path: rightRoot }),
]);
if (!left.ok) throw left.error;
if (!right.ok) throw right.error;
const inputPath = join(root, "comparison.json");
await writeFile(
inputPath,
JSON.stringify({
left: left.value,
right: right.value,
left_module_path: "search.js",
left_export_name: "search",
right_module_path: "search.js",
right_export_name: "search",
}),
);
const compared = await runCli([
"compare-javascript-export-shapes",
inputPath,
"--json",
]);
expect(compared).toMatchObject({
operation: "compare_javascript_export_shapes",
normalized_result: {
summary: { added: 2, removed: 1 },
property_inventories: expect.arrayContaining([
expect.objectContaining({
side: "left",
paired: true,
properties: expect.arrayContaining(["/count", "/kind", "/matches"]),
}),
expect.objectContaining({
side: "right",
paired: true,
properties: expect.arrayContaining([
"/kind",
"/matches",
"/query",
"/total",
]),
}),
]),
changes: expect.arrayContaining([
expect.objectContaining({
status: "removed",
path: "/count",
presence: { left: "present", right: "absent" },
}),
expect.objectContaining({
status: "added",
path: "/total",
presence: { left: "absent", right: "present" },
}),
expect.objectContaining({
status: "added",
path: "/query",
presence: { left: "absent", right: "present" },
}),
]),
},
});
}, 20_000);
});
describe("application workflow CLI validation", () => {
it("returns safe actionable JSON validation details", async () => {
const malformed = await runCli([
"trace-application-feature",
"{not-json",
"--json",
]);
expect(malformed).toMatchObject({
code: "invalid_request",
retryable: true,
details: {
issues: [{ path: [], reason: "invalid_format", expected: "JSON" }],
},
});
expect(JSON.stringify(malformed)).not.toContain("not-json");
const missing = await runCli(["trace-application-feature", "{}", "--json"]);
expect(missing).toMatchObject({
code: "invalid_request",
details: {
issues: expect.arrayContaining([
expect.objectContaining({
path: ["seed"],
reason: "missing_argument",
}),
]),
},
});
}, 20_000);
});
const compareThroughStdioMcp = async (
input: Record<string, unknown>,
): Promise<unknown> => {
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",
});
const client = new Client({
name: "javascript-capture-parity",
version: "1",
});
try {
await client.connect(transport);
const response = await client.callTool({
name: "compare_javascript_export_shapes",
arguments: input,
});
expect(response.isError).not.toBe(true);
return response.structuredContent;
} finally {
try {
await client.close();
} finally {
await transport.close();
}
}
};
const analyzeCliSources = (
root: string,
sources: readonly string[],
): Promise<unknown[]> =>
Promise.all(
sources.map(async (source, index) => {
const applicationRoot = join(root, String(index));
await mkdir(applicationRoot);
await writeFile(join(applicationRoot, "parser.mjs"), source);
return runCli([
"analyze-javascript-application",
applicationRoot,
"--json",
]);
}),
);
const runCli = async (
arguments_: readonly string[],
environment: Readonly<Record<string, string>> = {},
): Promise<unknown> => {
try {
const { stdout } = await execute(
process.execPath,
["scripts/rea.mjs", ...arguments_],
{
cwd: process.cwd(),
env: { ...process.env, ...environment },
maxBuffer: 16 * 1_024 * 1_024,
},
);
return JSON.parse(stdout);
} catch (cause: unknown) {
if (
typeof cause === "object" &&
cause !== null &&
"stdout" in cause &&
typeof cause.stdout === "string"
)
return JSON.parse(cause.stdout);
throw cause;
}
};