Files
rea/tests/acceptance/tools/nativeFunctionMatching.test.ts
gensokyo-dev 1417bf943b feat(native): match retained function inventories across builds (#5390)
Match retained native inventories through shared CLI/MCP workflows with conservative symbol/byte correspondence, unresolved structural candidates, complete Evidence provenance and bounded paged reads.

Closes #5343.
2026-10-20 09:48:52 +00:00

277 lines
8.5 KiB
TypeScript

import { readFile, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { expect } from "vitest";
import { cliTest, type TestCli } from "../../support/cli/cliFixture.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { connectLocalToolsMcp } from "../../fixtures/localToolsMcp.js";
import {
nativeMatchingInventory,
nativeMatchingDetails,
} from "../../fixtures/nativeFunctionMatching.js";
import { createEvidence, parseEvidence } from "../../../src/domain/evidence.js";
import { nativeMatchingProvider } from "../../fixtures/nativeFunctionMatching.js";
import {
createEvidenceBundle,
parseEvidenceBundle,
} from "../../../src/domain/evidenceBundle.js";
import { nativeFunctionMatchingSchema } from "../../../src/domain/native/functionMatching.js";
const createMatchingFixture = async () => {
const root = await createTestTempDirectory("rea-native-matching-");
const ancestor = createEvidence(undefined, nativeMatchingProvider, {
operation: "fixture_provenance",
parameters: {},
result: { source: "Synthetic ancestor" },
});
const unrelated = createEvidence(undefined, nativeMatchingProvider, {
operation: "fixture_unrelated",
parameters: {},
result: { source: "Unselected synthetic record" },
});
const dependency = createEvidence(undefined, nativeMatchingProvider, {
operation: "fixture_dependency",
parameters: {},
result: { source: "Synthetic dependency" },
evidenceLinks: [ancestor.evidence_id],
});
const left = nativeMatchingInventory(
"1",
[
{ address: "0x10", value: "main" },
{ address: "0x20", value: "old_name" },
],
[dependency.evidence_id],
);
const right = nativeMatchingInventory("2", [
{ address: "0x30", value: "main" },
{ address: "0x40", value: "new_name" },
]);
const leftDetails = [
...nativeMatchingDetails("1", "0x10", "9000", true),
...nativeMatchingDetails("1", "0x20", "c3", true),
];
const rightDetails = [
...nativeMatchingDetails("2", "0x30", "9001", true),
...nativeMatchingDetails("2", "0x40", "c3", true),
];
const ancestorsPath = join(root, "ancestors.json");
await writeFile(
ancestorsPath,
JSON.stringify(createEvidenceBundle([ancestor, unrelated])),
);
const importedPath = join(root, "dependencies.json");
await writeFile(
importedPath,
JSON.stringify(createEvidenceBundle([ancestor, dependency])),
);
return {
root,
ancestor,
dependency,
unrelated,
left,
right,
leftDetails,
rightDetails,
ancestorsPath,
importedPath,
};
};
const verifyPortableCli = async (
cli: TestCli,
fixture: Awaited<ReturnType<typeof createMatchingFixture>>,
input: Record<string, unknown>,
evidenceId: string,
) => {
const {
root,
ancestor,
dependency,
unrelated,
left,
right,
leftDetails,
rightDetails,
ancestorsPath,
} = fixture;
const bundlePath = join(root, "sources.json"),
inputPath = join(root, "request.json"),
outputPath = join(root, "result.json");
await writeFile(
bundlePath,
JSON.stringify(
createEvidenceBundle([
left,
right,
...leftDetails,
...rightDetails,
dependency,
]),
),
);
const original = await readFile(bundlePath);
await writeFile(inputPath, JSON.stringify({ ...input, detail: "complete" }));
const cliFull = await cli.run({
arguments: [
"match-native-functions",
inputPath,
"--source_bundle",
ancestorsPath,
"--source_bundle",
bundlePath,
"--json",
],
});
expect(cliFull.exitCode, cliFull.stderr).toBe(0);
expect(parseEvidence(cliFull.json).evidence_id).toBe(evidenceId);
await writeFile(inputPath, JSON.stringify(input));
const cliSummary = await cli.run({
arguments: [
"match-native-functions",
inputPath,
"--source_bundle",
ancestorsPath,
"--source_bundle",
bundlePath,
"--evidence_output",
outputPath,
"--json",
],
});
expect(cliSummary.exitCode, cliSummary.stderr).toBe(0);
expect(cliSummary.json).toMatchObject({
summary: {
normalized_result: { parent_evidence_id: evidenceId },
},
evidence_bundle: { path: outputPath },
});
const bundle = parseEvidenceBundle(
JSON.parse(await readFile(outputPath, "utf8")),
);
const ids = new Set(bundle.records.map((x) => x.evidence_id));
for (const record of bundle.records)
for (const id of record.evidence_links)
expect(
ids.has(id),
`Missing dependency ${id} of ${record.evidence_id}`,
).toBe(true);
expect(ids.has(ancestor.evidence_id)).toBe(true);
expect(ids.has(dependency.evidence_id)).toBe(true);
expect(ids.has(unrelated.evidence_id)).toBe(false);
expect(ids.has(evidenceId)).toBe(true);
expect(await readFile(bundlePath)).toEqual(original);
const incomplete = await cli.run({
arguments: [
"match-native-functions",
inputPath,
"--source_bundle",
bundlePath,
"--evidence_output",
join(root, "incomplete.json"),
"--json",
],
});
expect(incomplete.json).toMatchObject({
code: "evidence_integrity_mismatch",
});
};
cliTest(
"shared public CLI/MCP matching retains sources, pages exact facts, and feeds selected dossier pairs directly to compare_functions",
async ({ cli }) => {
const fixture = await createMatchingFixture();
const { left, right, leftDetails, rightDetails, importedPath } = fixture;
const { call } = await connectLocalToolsMcp();
const imported = await call("import_evidence_bundle", {
path: importedPath,
});
expect(imported.isError, JSON.stringify(imported)).not.toBe(true);
const full = await call("match_native_functions", {
left,
right,
left_observations: leftDetails,
right_observations: rightDetails,
detail: "complete",
});
expect(full.isError, JSON.stringify(full)).not.toBe(true);
const evidence = parseEvidence(full.structuredContent),
report = nativeFunctionMatchingSchema.parse(evidence.normalized_result);
expect(report.summary).toEqual({
matched: 2,
only_left: 0,
only_right: 0,
unresolved: 0,
});
const reference = (evidence_id: string) => ({
kind: "retained-evidence",
evidence_id,
});
const input = {
left: reference(left.evidence_id),
right: reference(right.evidence_id),
left_observations: leftDetails.map((x) => reference(x.evidence_id)),
right_observations: rightDetails.map((x) => reference(x.evidence_id)),
};
const summary = await call("match_native_functions", input);
expect(summary.isError, JSON.stringify(summary)).not.toBe(true);
expect(
parseEvidence(summary.structuredContent).normalized_result,
).toMatchObject({
kind: "summary",
artifact: null,
parent_evidence_id: evidence.evidence_id,
summary: { summary: report.summary },
});
expect(parseEvidence(summary.structuredContent).locations).not.toEqual(
expect.arrayContaining([
expect.objectContaining({ kind: "artifact-path" }),
]),
);
const page = await call("inspect_analysis_view", {
source: reference(evidence.evidence_id),
view: {
kind: "function-matches",
collection: "functions",
sides: ["left"],
fields: ["name", "body_sha256"],
offset: 1,
limit: 1,
},
});
expect(page.isError, JSON.stringify(page)).not.toBe(true);
expect(
parseEvidence(page.structuredContent).normalized_result,
).toMatchObject({
artifact: null,
coverage: { total: 2, next_offset: null },
items: [
{
name: "old_name",
function_id: `${left.evidence_id}/1`,
address: "0x20",
},
],
});
expect(parseEvidence(page.structuredContent).locations).not.toEqual(
expect.arrayContaining([
expect.objectContaining({ kind: "artifact-path" }),
]),
);
const compared = await call(
"compare_functions",
report.matches[0]!.compare_functions_input!,
);
expect(compared.isError, JSON.stringify(compared)).not.toBe(true);
expect(parseEvidence(compared.structuredContent).operation).toBe(
"compare_functions",
);
await verifyPortableCli(cli, fixture, input, evidence.evidence_id);
const missing = await call("match_native_functions", {
left: reference(`ev_${"a".repeat(64)}`),
right: reference(right.evidence_id),
});
expect(missing.isError).toBe(true);
},
60000,
);