Files
reverse-engineering-anything/tests/acceptance/artifactExtractionSelection.test.ts
T
APZNandgensokyo-dev ad518cff65 feat(artifacts): select extraction entries and output destination (#4031)
* feat(artifacts): select extraction entries and output destination

Refs morluto/rea#3490

* fix(artifacts): omit absent selection from execution parameters

* fix(artifacts): protect source directories and verify public selection

* fix(artifacts): keep optional selection inputs JSON-safe

* fix(artifacts): separate reader ownership from selected destinations

Validate complete reader inventory independently from collisions among selected output entries. Use occurrence-parent ownership instead of filename prefixes so independently owned PyInstaller entries remain selectable while nested-reader members remain refused. Validate advertised MCP schemas through the shared acceptance helper and add real PyInstaller/ASAR and excluded-entry regressions.

* fix(artifacts): advertise absolute extraction destinations

Reuse the shared host-absolute local path schema instead of an opaque refinement so SDK-converted JSON Schema preserves the runtime constraint. Extend the advertised-path and MCP-handler regression matrix for extract_artifact.output_root.

* fix(contracts): advertise unique extraction occurrence IDs

---------

Co-authored-by: gensokyo-dev <gensokyo-dev@users.noreply.github.com>
2026-11-06 03:28:05 +08:00

205 lines
7.0 KiB
TypeScript

import { execFile } from "node:child_process";
import { createHash } from "node:crypto";
import { lstat, readFile, readdir, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
import { promisify } from "node:util";
import { TextReader, Uint8ArrayWriter, ZipWriter } from "@zip.js/zip.js";
import { Client } from "@modelcontextprotocol/client";
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
import { expect, it } from "vitest";
import { artifactMcpCall } from "../../scripts/lib/artifact-e2e.mjs";
import {
artifactExtractionResultSchema,
artifactInventoryResultSchema,
} from "../../src/domain/artifactGraph.js";
import { artifactInspectionResultSchema } from "../../src/domain/artifactInspection.js";
import { parseEvidence } from "../../src/domain/evidence.js";
import { advertisedMcpToolSchemas } from "../fixtures/advertisedMcpTool.js";
import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js";
const execute = promisify(execFile);
const cli = fileURLToPath(new URL("../../scripts/rea.mjs", import.meta.url));
const setup = async () => {
const root = await createTestTempDirectory(
"rea-public-extraction-selection-",
);
const path = join(root, "fixture.zip");
const writer = new ZipWriter(new Uint8ArrayWriter());
await writer.add("lib/selected.so", new TextReader("native fixture"));
await writer.add("lib/other.so", new TextReader("other architecture"));
await writer.add("res/2f.xml", new TextReader("lower"));
await writer.add("res/2F.xml", new TextReader("upper"));
const bytes = await writer.close();
await writeFile(path, bytes);
const environment = {
PATH: process.env.PATH ?? "",
HOME: root,
USERPROFILE: root,
REA_LOG_LEVEL: "silent",
REA_ANALYSIS_PROVIDER: "auto",
};
const run = async (command: string, flags: readonly string[] = []) => {
const { stdout } = await execute(
process.execPath,
[cli, command, path, ...flags, "--json"],
{
env: environment,
timeout: 30000,
maxBuffer: 16 * 1024 * 1024,
},
);
return parseEvidence(JSON.parse(stdout));
};
const inspected = artifactInspectionResultSchema.parse(
(await run("inspect-artifact")).normalized_result,
);
const inventoryEvidence = inspected.substeps.find(
({ evidence }) => evidence.operation === "inventory_artifact",
)?.evidence;
if (inventoryEvidence === undefined)
throw new Error("Missing inventory Evidence");
const inventory = artifactInventoryResultSchema.parse(
inventoryEvidence.normalized_result,
);
const occurrence = (path: string) => {
const entry = inventory.occurrences.find(
({ logical_path }) => logical_path === path,
);
if (entry === undefined) throw new Error(`Missing occurrence: ${path}`);
return entry.occurrence_id;
};
return { root, path, bytes, environment, run, inventory, occurrence };
};
it("extracts repeatable CLI selections into the exact requested destination", async () => {
const data = await setup();
const ids = [
data.occurrence("lib/selected.so"),
data.occurrence("lib/other.so"),
];
const output = join(data.root, "chosen-output");
const evidence = await data.run("extract-artifact", [
"--output-root",
output,
...ids.flatMap((id) => ["--occurrence-id", id]),
]);
const result = artifactExtractionResultSchema.parse(
evidence.normalized_result,
);
expect(result.output_root).toBe(output);
expect(result.artifacts.map(({ relative_path }) => relative_path)).toEqual([
"lib/other.so",
"lib/selected.so",
]);
expect(result.extraction_manifest.selected_occurrence_ids).toEqual(
[...ids].sort(),
);
expect(result.manifest).toEqual(data.inventory.manifest);
expect(await readdir(output)).toEqual(["lib"]);
expect(await readFile(join(output, "lib/selected.so"), "utf8")).toBe(
"native fixture",
);
});
it("keeps CLI/MCP selection identity, invalid-input refusals and committed output after close", async () => {
const data = await setup();
const id = data.occurrence("lib/selected.so");
const cliResult = artifactExtractionResultSchema.parse(
(
await data.run("extract-artifact", [
"--output-root",
join(data.root, "cli-output"),
"--occurrence-id",
id,
])
).normalized_result,
);
const transport = new StdioClientTransport({
command: process.execPath,
args: [cli, "mcp"],
env: {
...data.environment,
REA_MCP_INPUT_SCHEMA_PROFILE: "full",
REA_MCP_TOOL_FAMILIES: "artifact,session",
},
stderr: "pipe",
});
const client = new Client({
name: "extraction-selection-test",
version: "1",
});
const output = join(data.root, "mcp-output");
try {
await client.connect(transport);
const opened = await artifactMcpCall(client, {
name: "open_binary",
arguments: { path: data.path },
});
expect(opened.isError).not.toBe(true);
const schemas = await advertisedMcpToolSchemas(client, "extract_artifact");
for (const occurrence_ids of [[id], [id, data.occurrence("lib/other.so")]])
expect(
schemas.ajv.validate(schemas.inputSchema, {
output_root: output,
occurrence_ids,
}),
JSON.stringify(schemas.ajv.errors),
).toBe(true);
expect(
schemas.ajv.validate(schemas.inputSchema, {
output_root: output,
occurrence_ids: [id, id],
}),
).toBe(false);
expect(schemas.ajv.errors).toEqual(
expect.arrayContaining([
expect.objectContaining({ keyword: "uniqueItems" }),
]),
);
const reply = await artifactMcpCall(client, {
name: "extract_artifact",
arguments: { output_root: output, occurrence_ids: [id] },
});
expect(reply.isError).not.toBe(true);
const evidence = parseEvidence(reply.structuredContent);
const result = artifactExtractionResultSchema.parse(
evidence.normalized_result,
);
expect(result.output_root).toBe(output);
expect(result.extraction_manifest).toEqual(cliResult.extraction_manifest);
expect(result.artifacts).toEqual(cliResult.artifacts);
for (const [index, occurrence_ids] of [
[`occ_${"0".repeat(64)}`],
[id, id],
].entries()) {
const invalidOutput = join(data.root, `invalid-output-${index}`);
const invalid = await artifactMcpCall(client, {
name: "extract_artifact",
arguments: { output_root: invalidOutput, occurrence_ids },
});
expect(invalid.isError).toBe(true);
await expect(lstat(invalidOutput)).rejects.toMatchObject({
code: "ENOENT",
});
}
const closed = await artifactMcpCall(client, {
name: "close_binary",
arguments: {},
});
expect(closed.isError).not.toBe(true);
} finally {
await client.close();
await transport.close();
}
expect(await readFile(join(output, "lib/selected.so"), "utf8")).toBe(
"native fixture",
);
const digest = (bytes: Uint8Array) =>
createHash("sha256").update(bytes).digest("hex");
expect(digest(await readFile(data.path))).toBe(digest(data.bytes));
});