Files
rea/tests/boundary/mcp/nativeViewPredicateMcp.test.ts
morluto 53363166f0 fix(evidence)!: validate native view parent predicates (#8451)
Reject native view sources whose predicate disagrees with the selected operation. Use the original predicate for existing Evidence; do not alter a record without recomputing its identity.
2026-11-01 20:27:34 +08:00

201 lines
7.1 KiB
TypeScript

import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
import { expect, it } from "vitest";
import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import { FUNCTION_COMPARISON_EXAMPLE } from "../../../src/contracts/functionComparisonExample.js";
import {
createEvidence,
parseEvidence,
type Evidence,
} from "../../../src/domain/evidence.js";
import { createEvidenceBundle } from "../../../src/domain/evidenceBundle.js";
import { createServer } from "../../../src/server/createServer.js";
import { createTestBinarySession } from "../../fixtures/binarySession.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { createMcpSchemaValidation } from "../../fixtures/mcpSchemaValidation.js";
import { parseMcpToolError } from "../../fixtures/mcpToolError.js";
const fixture = FUNCTION_COMPARISON_EXAMPLE.left;
if (fixture.subject === null)
throw new Error("Expected artifact-bound fixture");
const subject = {
path: fixture.subject.local_path,
sha256: fixture.subject.digest.sha256,
format: fixture.subject.format,
};
const selected = (
operation: string,
result: Evidence["normalized_result"],
predicateType: string,
) =>
createEvidence(subject, fixture.provider, {
operation,
parameters: {},
result,
predicateType,
confidence: "observed",
authority: "shipped-artifact",
});
const wrongMatchingPredicate = (matching: Evidence) =>
createEvidence(undefined, matching.provider, {
operation: matching.operation,
parameters: matching.parameters,
result: matching.normalized_result,
predicateType: "rea.analysis",
confidence: matching.confidence,
authority: matching.authority,
evidenceLinks: matching.evidence_links,
});
it.each(["full", "compact"] as const)(
"validates native parent predicates across public sources (%s schema)",
async (profile) => {
const session = createTestBinarySession(() => {
throw new Error("No provider should start");
});
const server = createServer(
{ kind: "session", session },
{
environment: { ...process.env, REA_MCP_OUTPUT_SCHEMA_PROFILE: profile },
},
);
const client = new Client({ name: "native-view-identity", version: "1" });
const validation = createMcpSchemaValidation();
const [ct, st] = InMemoryTransport.createLinkedPair();
const root = await createTestTempDirectory("rea-native-view-identity-");
try {
await server.connect(st);
await client.connect(ct);
const definitions = new Map(
(await client.listTools()).tools.map((tool) => [tool.name, tool]),
);
const viewTool = definitions.get("inspect_analysis_view");
if (viewTool?.outputSchema === undefined)
throw new Error("Missing advertised view schema");
expect(validation.assertions.validateSchema(viewTool.outputSchema)).toBe(
true,
);
const call = (name: string, args: Record<string, unknown>) => {
const toolDefinition = definitions.get(name);
if (toolDefinition === undefined) throw new Error(`Missing ${name}`);
return client.callTool({ name, arguments: args }, { toolDefinition });
};
const inventory = selected(
"list_procedures",
[{ address: "0x1000", value: "main" }],
"rea.analysis",
);
const matchingResult = await call("match_native_functions", {
left: inventory,
right: inventory,
detail: "complete",
});
expect(
matchingResult.isError,
JSON.stringify(matchingResult.content),
).not.toBe(true);
const matching = parseEvidence(matchingResult.structuredContent);
expect(matching.predicate_type).toBe("rea.native-function-matching");
const wrongMatching = wrongMatchingPredicate(matching);
const graph = {
graph: {
"0": [
{
address: "0x1000",
status: "ok",
calls: [],
relationship_coverage: { status: "complete" },
},
],
},
limitations: [],
};
const cases = [
{
good: selected(
"analyze_function",
fixture.normalized_result,
"rea.analysis",
),
bad: selected(
"analyze_function",
fixture.normalized_result,
"rea.unrelated",
),
view: { kind: "summary" },
},
{
good: inventory,
bad: selected(
"list_procedures",
inventory.normalized_result,
"rea.unrelated",
),
view: { kind: "native-inventory", offset: 0, limit: 1 },
},
{
good: selected("procedure_address", "0x1000", "rea.analysis"),
bad: selected("procedure_address", "0x1000", "rea.unrelated"),
view: { kind: "native-observation", offset: 0, limit: 1 },
},
{
good: selected("get_call_graph", graph, "rea.analysis"),
bad: selected("get_call_graph", graph, "rea.unrelated"),
view: { kind: "native-observation", offset: 0, limit: 1 },
},
{ good: matching, bad: wrongMatching, view: { kind: "summary" } },
];
for (const [index, { good, bad, view }] of cases.entries()) {
expect(session.recordEvidence(good).ok).toBe(true);
expect(session.recordEvidence(bad).ok).toBe(true);
const goodReply = await call("inspect_analysis_view", {
source: { kind: "retained-evidence", evidence_id: good.evidence_id },
view,
});
expect(goodReply.isError, JSON.stringify(goodReply.content)).not.toBe(
true,
);
expect(
validation.assertions.validate(
viewTool.outputSchema,
goodReply.structuredContent,
),
JSON.stringify(validation.assertions.errors),
).toBe(true);
const goodView = parseEvidence(goodReply.structuredContent);
expect(goodView.evidence_links).toContain(good.evidence_id);
const raw = join(root, `${index}.json`),
bundle = join(root, `${index}-bundle.json`);
await writeFile(raw, JSON.stringify(bad));
await writeFile(bundle, JSON.stringify(createEvidenceBundle([bad])));
for (const source of [
{ kind: "inline", evidence: bad },
{ kind: "retained-evidence", evidence_id: bad.evidence_id },
{ kind: "evidence-file", path: raw },
{ kind: "evidence-file", path: bundle },
]) {
const reply = await call("inspect_analysis_view", { source, view });
expect(parseMcpToolError(reply).error).toMatchObject({
code: "invalid_request",
details: {
issues: [
expect.objectContaining({
path: ["source", "predicate_type"],
reason: "invalid_value",
}),
],
},
});
}
}
expect(session.activeTarget()).toBeUndefined();
} finally {
await client.close();
await server.close();
await session.close();
validation.clear();
}
},
);