Files
rea/tests/boundary/mcp/javascriptSourceSelection.test.ts
T
85c1d2587a feat(javascript): analyze selected application sources
Analyze exact selected sources in first-request order before unrelated
static scopes, preserving full artifact identity and byte verification.
Retain selection in CLI/MCP Evidence and summaries, with explicit unknowns
and partial coverage for unselected sources. Keep default full analysis.

Fixes #1848

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Co-authored-by: Kaoru0822 <[email protected]>
2026-10-12 04:07:14 +08:00

226 lines
8.1 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 { createTestTempDirectory } from "../../fixtures/temporaryDirectory.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("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);