rea/tests/boundary/mcp/functionCallDirectionsMcp.t...

139 lines
4.9 KiB
TypeScript

import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
import { Ajv2020 } from "ajv/dist/2020.js";
import { expect, it } from "vitest";
import { FUNCTION_COMPARISON_EXAMPLE } from "../../../src/contracts/functionComparisonExample.js";
import { createEvidence } from "../../../src/domain/evidence.js";
import { functionDossierSchema } from "../../../src/domain/hopperValues.js";
import type { RelationshipCoverage } from "../../../src/domain/relationshipCoverage.js";
import { createServer } from "../../../src/server/createServer.js";
import { createTestBinarySession } from "../../fixtures/binarySession.js";
const observe = (relationshipCoverage: RelationshipCoverage) => {
const template = FUNCTION_COMPARISON_EXAMPLE.left;
if (template.subject === null)
throw new Error("Expected artifact-bound fixture");
const dossier = functionDossierSchema.parse(template.normalized_result);
return createEvidence(
{
path: template.subject.local_path,
sha256: template.subject.digest.sha256,
format: template.subject.format,
},
template.provider,
{
operation: "analyze_function",
parameters: template.parameters,
result: { ...dossier, callees: [], unresolved_calls: [] },
confidence: template.confidence,
authority: template.authority,
relationshipCoverage,
},
);
};
it("preserves directional gaps through advertised schemas and public path reconstruction", async () => {
const session = createTestBinarySession(() => {
throw new Error("Offline call paths must not launch a provider");
});
const server = createServer({ kind: "session", session });
const client = new Client({ name: "function-call-directions", version: "1" });
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
try {
await server.connect(serverTransport);
await client.connect(clientTransport);
const advertised = (await client.listTools()).tools.find(
({ name }) => name === "build_call_path",
);
if (advertised === undefined)
throw new Error("Missing build_call_path contract");
const ajv = new Ajv2020({ strict: false, validateFormats: false });
expect(ajv.validateSchema(advertised.inputSchema)).toBe(true);
const validate = ajv.compile(advertised.inputSchema);
for (const { directions, status, exhaustive } of [
{
directions: ["callers"] as ["callers"],
status: "not_found",
exhaustive: true,
},
{
directions: ["callees"] as ["callees"],
status: "unknown",
exhaustive: false,
},
{
directions: ["callers", "callees"] as ["callers", "callees"],
status: "unknown",
exhaustive: false,
},
]) {
const limitation =
"An observed call reference is absent from the inventory.";
const coverage: RelationshipCoverage = {
status: "partial",
limitations: [limitation],
limited_directions: directions,
};
const observation = observe(coverage);
expect(session.recordEvidence(observation).ok).toBe(true);
const input = {
functions: [observation],
start: { address: "0x1000" },
goal: { address: "0x9999" },
};
expect(validate(input), JSON.stringify(validate.errors)).toBe(true);
for (const limited_directions of [
[],
["callers", "callers"],
["callees", "callees"],
["callers", "callees", "callers"],
["incoming"],
])
expect(
validate({
...input,
functions: [
{
...observation,
relationship_coverage: {
...coverage,
limited_directions,
},
},
],
}),
).toBe(false);
const result = await client.callTool({
name: "build_call_path",
arguments: input,
});
if (result.isError) throw new Error(JSON.stringify(result));
expect(result.structuredContent).toMatchObject({
normalized_result: {
status,
search_scope: { exhaustive },
limitations: expect.arrayContaining([limitation]),
},
});
const comparison = await client.callTool({
name: "compare_functions",
arguments: { left: observation, right: observation },
});
if (comparison.isError) throw new Error(JSON.stringify(comparison));
expect(comparison.structuredContent).toMatchObject({
normalized_result: {
dimensions: expect.arrayContaining([
expect.objectContaining({ dimension: "calls", status: "unknown" }),
]),
},
});
// Each scenario owns its Evidence and residual-unknown ledger.
const closed = await session.close();
if (!closed.ok) throw closed.error;
}
} finally {
await Promise.allSettled([session.close(), client.close(), server.close()]);
}
});