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"; import { parseMcpToolError } from "../../fixtures/mcpToolError.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, }, ); }; const profiles = ["full", "compact"] as const; it.each(profiles)("preserves directional gaps (%s)", async (profile) => { const session = createTestBinarySession(() => { throw new Error("Offline call paths must not launch a provider"); }); const server = createServer( { kind: "session", session }, { environment: { ...process.env, REA_MCP_INPUT_SCHEMA_PROFILE: profile } }, ); 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"], ]) { const malformed = { ...input, functions: [ { ...observation, relationship_coverage: { ...coverage, limited_directions, }, }, ], }; if (profile === "full") expect(validate(malformed)).toBe(false); const rejected = await client.callTool({ name: "build_call_path", arguments: malformed, }); expect(parseMcpToolError(rejected)).toMatchObject({ error: { code: "invalid_request", category: "invalid_input" }, }); } 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()]); } });