Files
rea/tests/boundary/mcp/javascriptSourceSelection.test.ts
T
7e5cffa2ae fix(javascript): preserve loader ownership and unavailable artifact facts
Preserve missing ASAR companion occurrence identity, declared size and unknown content in graph nodes and resolution limitations. Apply URL text semantics once for ESM lookup while preserving literal CommonJS paths and original specifiers. Keep CJS wrapper var bindings and assignment history, recognizing only untouched wrapper redeclarations.

Validation: real ASAR producer workflow and CLI/MCP path/coverage cases; semantic reassignment controls. Context7 Node documentation and direct Node wrapper execution confirm wrapper behavior.

Fixes #3213
Fixes #3214
Fixes #3223
Related: #3278, #3254

Co-authored-by: Geronimo <varungore1@gmail.com>
Co-authored-by: Paolo Valletta <paovalletta@hotmail.it>
2026-10-14 16:54:08 +08:00

301 lines
11 KiB
TypeScript

import { Client } from "@modelcontextprotocol/client";
import { createPackageWithOptions } from "@electron/asar";
import { Ajv2020 } from "ajv/dist/2020.js";
import { rm } from "node:fs/promises";
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
import { join, resolve } from "node:path";
import { expect, it } from "vitest";
import { z } from "zod";
import { parseEvidence } from "../../../src/domain/evidence.js";
import { javascriptApplicationAnalysisResultSchema } from "../../../src/domain/javascript/javascriptApplicationAnalysis.js";
import { writeJavaScriptSourceSelectionFixture } from "../../fixtures/javascriptSourceSelection.js";
import { createStrippedAsarAddon } from "../../fixtures/strippedAsarAddon.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { writeFixtureFiles } from "../../support/javascriptApplicationFixture.js";
const createSelectionClient = () => {
const client = new Client({
name: "javascript-source-selection",
version: "1",
});
const transport = new StdioClientTransport({
command: process.execPath,
args: [resolve("scripts/rea.mjs"), "mcp"],
cwd: process.cwd(),
env: {
PATH: process.env.PATH ?? "",
NODE_OPTIONS: "--max-old-space-size=512 --max-semi-space-size=8",
},
stderr: "pipe",
});
return { client, transport };
};
it("resolves ESM whitespace specifiers through the public application tool", async () => {
const root = await createTestTempDirectory("rea-esm-whitespace-mcp-");
const rawSpecifier = "./feature\tname.js";
const encodedSpecifier = "./feature%09name.js";
await writeFixtureFiles(root, {
"main.mjs": `import ${JSON.stringify(rawSpecifier)};\nimport ${JSON.stringify(encodedSpecifier)};\n`,
"featurename.js": "export const plain = 1;\n",
"feature\tname.js": "export const value = 1;\n",
});
const { client, transport } = createSelectionClient();
try {
await client.connect(transport);
const output = await client.callTool({
name: "analyze_javascript_application",
arguments: { input_path: root },
});
expect(output.isError, JSON.stringify(output.content)).not.toBe(true);
const result = javascriptApplicationAnalysisResultSchema.parse(
parseEvidence(output.structuredContent).normalized_result,
);
expect(result.graph.edges).toEqual(
expect.arrayContaining([
expect.objectContaining({
properties: expect.objectContaining({
specifier: rawSpecifier,
resolved_path: "featurename.js",
}),
}),
expect.objectContaining({
properties: expect.objectContaining({
specifier: encodedSpecifier,
resolved_path: "feature\tname.js",
}),
}),
]),
);
} finally {
await client.close();
await transport.close();
}
}, 60_000);
it("retains missing ASAR native-addon inventory through the public application tool", async () => {
const { archive, addon } = await createStrippedAsarAddon();
await rm(join(`${archive}.unpacked`, addon));
const { client, transport } = createSelectionClient();
try {
await client.connect(transport);
const output = await client.callTool({
name: "analyze_javascript_application",
arguments: { input_path: archive },
});
expect(output.isError, JSON.stringify(output.content)).not.toBe(true);
const result = javascriptApplicationAnalysisResultSchema.parse(
parseEvidence(output.structuredContent).normalized_result,
);
expect(result.graph.coverage.status).toBe("partial");
expect(
result.graph.nodes.some(
(node) =>
node.kind === "native-addon" &&
node.observations.some(
({ label, properties }) =>
label === addon && properties.availability === "unavailable",
),
),
).toBe(true);
} finally {
await client.close();
await transport.close();
}
}, 60_000);
it("advertises exact source selection and retains its scope through summary, Evidence retrieval and invalid-input recovery", async () => {
const root = await createTestTempDirectory("rea-js-selected-mcp-");
await writeJavaScriptSourceSelectionFixture(root);
const { client, transport } = createSelectionClient();
try {
await client.connect(transport);
const tools = await client.listTools();
const definition = tools.tools.find(
({ name }) => name === "analyze_javascript_application",
);
const schema = definition?.inputSchema;
if (schema === undefined)
throw new Error("Missing advertised application contract");
expect(schema.properties?.source_paths).toBeDefined();
const validate = new Ajv2020({
strict: false,
validateFormats: false,
}).compile(schema);
expect(validate({ input_path: root, source_paths: ["main.mjs"] })).toBe(
true,
);
for (const source_paths of [[], [""], [1]])
expect(validate({ input_path: root, source_paths })).toBe(false);
const selected = await client.callTool({
name: "analyze_javascript_application",
arguments: {
input_path: root,
source_paths: ["main.mjs", "package.json"],
detail: "summary",
},
});
expect(selected.isError, JSON.stringify(selected.content)).not.toBe(true);
if (definition?.outputSchema === undefined)
throw new Error("Missing advertised application output contract");
const validateOutput = new Ajv2020({
strict: false,
validateFormats: false,
}).compile(
z.record(z.string(), z.unknown()).parse(definition.outputSchema),
);
expect(
validateOutput(selected.structuredContent),
JSON.stringify(validateOutput.errors),
).toBe(true);
const summary = z
.object({
normalized_result: z.object({
parent_evidence_id: z.string(),
summary: z.object({
source_selection: z.object({
requested_paths: z.array(z.string()),
unselected_files: z.number(),
}),
coverage: z.object({
application: z.object({ status: z.string() }),
}),
}),
}),
})
.parse(selected.structuredContent).normalized_result;
expect(summary.summary.source_selection).toEqual({
requested_paths: ["main.mjs", "package.json"],
unselected_files: 4,
});
expect(summary.summary.coverage.application.status).toBe("partial");
const bundle = await client.callTool({
name: "get_evidence_bundle",
arguments: { detail: "complete" },
});
expect(bundle.isError).not.toBe(true);
const retained = z
.object({ result: z.object({ records: z.array(z.unknown()) }) })
.parse(bundle.structuredContent)
.result.records.map(parseEvidence)
.find(({ evidence_id }) => evidence_id === summary.parent_evidence_id);
expect(retained).toBeDefined();
const result = javascriptApplicationAnalysisResultSchema.parse(
retained!.normalized_result,
);
expect(result.statistics.parsed_javascript_files).toBe(1);
expect(result.source_selection).toEqual(summary.summary.source_selection);
expect(
result.semantic_graph.nodes.some(
({ kind, label }) => kind === "function" && label === "selected",
),
).toBe(true);
for (const source_paths of [
["missing.mjs"],
["./main.mjs"],
["../main.mjs"],
["*.mjs"],
]) {
const invalid = await client.callTool({
name: "analyze_javascript_application",
arguments: { input_path: root, source_paths },
});
expect(invalid.isError).toBe(true);
const text = invalid.content.find((item) => item.type === "text");
if (text?.type !== "text")
throw new Error("Missing source-selection correction");
expect(JSON.parse(text.text)).toMatchObject({
error: {
code: "invalid_request",
details: {
issues: [{ path: ["source_paths", 0], reason: "invalid_value" }],
},
},
});
await client.ping();
}
const next = await client.callTool({
name: "analyze_javascript_application",
arguments: { input_path: root, source_paths: ["main.mjs"] },
});
expect(next.isError).not.toBe(true);
expect(
javascriptApplicationAnalysisResultSchema.parse(
parseEvidence(next.structuredContent).normalized_result,
).statistics.parsed_javascript_files,
).toBe(1);
await client.ping();
} finally {
await client.close();
await transport.close();
}
}, 60_000);
it("selects nested ASAR paths and distinguishes missing inventoried bytes from invalid paths", async () => {
const root = await createTestTempDirectory("rea-js-selected-containers-");
await writeJavaScriptSourceSelectionFixture(root);
const { client, transport } = createSelectionClient();
try {
await client.connect(transport);
const nestedRoot = await createTestTempDirectory(
"rea-js-nested-selection-",
);
await createPackageWithOptions(root, join(nestedRoot, "nested.asar"), {});
const nested = await client.callTool({
name: "analyze_javascript_application",
arguments: {
input_path: nestedRoot,
source_paths: ["nested.asar/main.mjs"],
},
});
expect(nested.isError).not.toBe(true);
const nestedResult = javascriptApplicationAnalysisResultSchema.parse(
parseEvidence(nested.structuredContent).normalized_result,
);
expect(nestedResult.source_selection).toEqual({
requested_paths: ["nested.asar/main.mjs"],
unselected_files: 5,
});
expect(nestedResult.statistics).toMatchObject({
nested_asar_containers: 1,
parsed_javascript_files: 1,
parse_failures: 0,
});
expect(
nestedResult.semantic_graph.nodes.some(
({ identity, kind, label }) =>
identity.module_path === "nested.asar/main.mjs" &&
kind === "function" &&
label === "selected",
),
).toBe(true);
const unpacked = join(
await createTestTempDirectory("rea-js-unavailable-selection-"),
"app.asar",
);
await createPackageWithOptions(root, unpacked, { unpack: "main.mjs" });
await rm(join(unpacked + ".unpacked", "main.mjs"));
const unavailable = await client.callTool({
name: "analyze_javascript_application",
arguments: { input_path: unpacked, source_paths: ["main.mjs"] },
});
expect(unavailable.isError).toBe(true);
const unavailableText = unavailable.content.find(
(item) => item.type === "text",
);
if (unavailableText?.type !== "text")
throw new Error("Missing selected-source availability reason");
expect(JSON.parse(unavailableText.text)).toMatchObject({
error: {
code: "artifact_operation_failed",
details: { reason: "unavailable" },
},
});
await client.ping();
} finally {
await client.close();
await transport.close();
}
}, 60_000);