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.
201 lines
7.1 KiB
TypeScript
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();
|
|
}
|
|
},
|
|
);
|