mirror of
https://github.com/morluto/rea.git
synced 2026-10-12 02:19:24 +00:00
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]>
226 lines
8.1 KiB
TypeScript
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);
|