import { writeFile } from "node:fs/promises"; import { join } from "node:path"; import { Client, InMemoryTransport } from "@modelcontextprotocol/client"; import { Ajv2020 } from "ajv/dist/2020.js"; import { expect } from "vitest"; import { createEvidence, parseEvidence } from "../../src/domain/evidence.js"; import { wasmInterfaceComparisonSchema } from "../../src/contracts/wasm/wasmComparisonContracts.js"; import { composeBinarySession } from "../../src/application/binary/BinarySessionComposition.js"; import { AnalysisProviderRegistry } from "../../src/application/binary/AnalysisProviderRegistry.js"; import { createServer } from "../../src/server/createServer.js"; import { wasmArtifactFixture } from "../fixtures/wasm/artifact.js"; import { cliTest } from "../support/cli/cliFixture.js"; import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js"; cliTest( "compares portable Evidence identically through CLI and retained MCP with schema/provenance", async ({ cli }) => { const sources = (["i32", "i64"] as const).map((parameter) => { const report = wasmArtifactFixture(); report.interface = { state: "complete", issues: [], entries: [ { direction: "export", module: null, name: 'quoted"\ufeffname', kind: "func", index: 0, signature: { kind: "func", params: [parameter], results: [] }, wat_form: '(export "quoted\\22\\ef\\bb\\bfname" (func 0))', issue: null, }, ], }; return createEvidence( { path: report.artifact.path, sha256: report.artifact.sha256, format: "file", }, { id: "typed-fixture", name: "Typed fixture", version: "1" }, { operation: "inspect_wasm_artifact", parameters: { path: report.artifact.path }, result: report, confidence: "observed", }, ); }); const [left, right] = sources; if (left === undefined || right === undefined) throw new Error("Missing paired fixture"); const root = await createTestTempDirectory("rea-wasm-comparison-"); const path = join(root, "input.json"); const input = { left, right, scope: "both" }; await writeFile(path, JSON.stringify(input)); const cliResult = await cli.run({ arguments: ["compare-wasm-interfaces", path, "--json"], }); expect(cliResult.exitCode).toBe(0); const cliEvidence = parseEvidence(cliResult.json); const cliReport = wasmInterfaceComparisonSchema.parse( cliEvidence.normalized_result, ); expect(cliReport).toMatchObject({ coverage: "complete-within-inputs", changes: [{ name: 'quoted"\ufeffname', state: "changed" }], left: { evidence_id: left.evidence_id, artifact: { path: "/artifacts/module.wasm", sha256: "a".repeat(64), bytes: 8, }, }, }); const session = composeBinarySession(new AnalysisProviderRegistry([])); const server = createServer({ kind: "session", session }); const client = new Client({ name: "wasm-interface-parity", version: "1" }); const [ct, st] = InMemoryTransport.createLinkedPair(); try { await server.connect(st); await client.connect(ct); const tool = (await client.listTools()).tools.find( (t) => t.name === "compare_wasm_interfaces", ); if (tool?.outputSchema === undefined) throw new Error("Missing comparison schema"); const outputSchema: Record = tool.outputSchema; const ajv = new Ajv2020({ strict: false, validateFormats: false }); expect(ajv.validate(tool.inputSchema, input)).toBe(true); expect(ajv.validate(tool.inputSchema, { left, right })).toBe(false); const mcp = await client.callTool({ name: tool.name, arguments: input }); expect(mcp.isError, JSON.stringify(mcp)).not.toBe(true); expect( ajv.validate(outputSchema, mcp.structuredContent), JSON.stringify(ajv.errors), ).toBe(true); const mcpEvidence = parseEvidence(mcp.structuredContent); expect(mcpEvidence.normalized_result).toEqual(cliReport); expect(mcpEvidence.evidence_id).toBe(cliEvidence.evidence_id); expect(mcpEvidence.evidence_links).toEqual([ left.evidence_id, right.evidence_id, ]); const retained = await client.callTool({ name: tool.name, arguments: { scope: "both", left: { kind: "retained-evidence", evidence_id: left.evidence_id }, right: { kind: "retained-evidence", evidence_id: right.evidence_id }, }, }); expect(retained.isError).not.toBe(true); expect(parseEvidence(retained.structuredContent).evidence_id).toBe( mcpEvidence.evidence_id, ); const invalid = await client.callTool({ name: tool.name, arguments: { ...input, scope: "all" }, }); expect(invalid.isError).toBe(true); } finally { await client.close(); await server.close(); await session.close(); } }, );