169 lines
5.1 KiB
TypeScript
169 lines
5.1 KiB
TypeScript
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
|
|
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
|
|
import { afterEach, describe, expect, it } from "vitest";
|
|
|
|
import { BinarySession } from "../src/application/BinarySession.js";
|
|
import { createEvidence } from "../src/domain/evidence.js";
|
|
import {
|
|
createEvidenceBundle,
|
|
serializeEvidenceBundle,
|
|
} from "../src/domain/evidenceBundle.js";
|
|
import { createServer } from "../src/server/createServer.js";
|
|
import { observed } from "./fixtures/analysisExecution.js";
|
|
|
|
const roots: string[] = [];
|
|
afterEach(async () => {
|
|
await Promise.all(
|
|
roots.splice(0).map((root) => rm(root, { recursive: true, force: true })),
|
|
);
|
|
});
|
|
|
|
const sourceEvidence = (label: string) =>
|
|
createEvidence(
|
|
undefined,
|
|
{ id: "fixture", name: "Fixture", version: "1" },
|
|
{
|
|
operation: "observe",
|
|
parameters: { label },
|
|
result: { label },
|
|
confidence: "observed",
|
|
authority: "shipped-artifact",
|
|
},
|
|
);
|
|
|
|
describe("bundle comparison MCP integration", () => {
|
|
it("reads two approved bounded bundle files and records a compact result", async () => {
|
|
const root = await mkdtemp(join(tmpdir(), "rea-bundle-mcp-"));
|
|
roots.push(root);
|
|
const leftPath = join(root, "left.json");
|
|
const rightPath = join(root, "right.json");
|
|
const leftRecord = sourceEvidence("left");
|
|
const rightRecord = sourceEvidence("right");
|
|
await Promise.all([
|
|
writeFile(
|
|
leftPath,
|
|
serializeEvidenceBundle(createEvidenceBundle([leftRecord])),
|
|
),
|
|
writeFile(
|
|
rightPath,
|
|
serializeEvidenceBundle(createEvidenceBundle([rightRecord])),
|
|
),
|
|
]);
|
|
const connected = await connect(root);
|
|
try {
|
|
const result = await connected.client.callTool({
|
|
name: "compare_bundles",
|
|
arguments: {
|
|
left_bundle_path: leftPath,
|
|
right_bundle_path: rightPath,
|
|
record_pairs: [
|
|
{
|
|
left_evidence_id: leftRecord.evidence_id,
|
|
right_evidence_id: rightRecord.evidence_id,
|
|
},
|
|
],
|
|
},
|
|
});
|
|
expect(result.isError).not.toBe(true);
|
|
expect(result.structuredContent).toMatchObject({
|
|
result: { status: "changed" },
|
|
evidence_id: expect.stringMatching(/^ev_[a-f0-9]{64}$/u),
|
|
evidence_uri: expect.stringMatching(/^rea:\/\/evidence\/ev_/u),
|
|
});
|
|
} finally {
|
|
await connected.close();
|
|
}
|
|
});
|
|
|
|
it("rejects a bundle path outside the approved root", async () => {
|
|
const root = await mkdtemp(join(tmpdir(), "rea-bundle-root-"));
|
|
const outside = await mkdtemp(join(tmpdir(), "rea-bundle-outside-"));
|
|
roots.push(root, outside);
|
|
const leftPath = join(root, "left.json");
|
|
const rightPath = join(outside, "right.json");
|
|
const encoded = serializeEvidenceBundle(createEvidenceBundle([]));
|
|
await Promise.all([
|
|
writeFile(leftPath, encoded),
|
|
writeFile(rightPath, encoded),
|
|
]);
|
|
const connected = await connect(root);
|
|
try {
|
|
const result = await connected.client.callTool({
|
|
name: "compare_bundles",
|
|
arguments: {
|
|
left_bundle_path: leftPath,
|
|
right_bundle_path: rightPath,
|
|
},
|
|
});
|
|
expect(result).toMatchObject({
|
|
isError: true,
|
|
structuredContent: { error: { code: "outside_approved_root" } },
|
|
});
|
|
} finally {
|
|
await connected.close();
|
|
}
|
|
});
|
|
|
|
it("rejects an oversized approved bundle", async () => {
|
|
const root = await mkdtemp(join(tmpdir(), "rea-bundle-size-"));
|
|
roots.push(root);
|
|
const leftPath = join(root, "left.json");
|
|
const rightPath = join(root, "right.json");
|
|
await Promise.all([
|
|
writeFile(leftPath, " ".repeat(2_000)),
|
|
writeFile(rightPath, serializeEvidenceBundle(createEvidenceBundle([]))),
|
|
]);
|
|
const connected = await connect(root, 1_024);
|
|
try {
|
|
const result = await connected.client.callTool({
|
|
name: "compare_bundles",
|
|
arguments: {
|
|
left_bundle_path: leftPath,
|
|
right_bundle_path: rightPath,
|
|
},
|
|
});
|
|
expect(result).toMatchObject({
|
|
isError: true,
|
|
structuredContent: { error: { code: "truncated" } },
|
|
});
|
|
} finally {
|
|
await connected.close();
|
|
}
|
|
});
|
|
});
|
|
|
|
const connect = async (root: string, maxBytes = 1024 * 1024) => {
|
|
const session = new BinarySession(() => ({
|
|
health: () => Promise.resolve(),
|
|
execute: () => Promise.resolve(observed(null)),
|
|
close: () => Promise.resolve(),
|
|
}));
|
|
const server = createServer(session, session, {
|
|
evidenceFilePolicy: {
|
|
roots: [root],
|
|
maxBytes,
|
|
maxDepth: 68,
|
|
maxStringLength: 1024 * 1024,
|
|
maxNodes: 100_000,
|
|
},
|
|
});
|
|
const client = new Client({ name: "bundle-comparison-test", version: "1" });
|
|
const [clientTransport, serverTransport] =
|
|
InMemoryTransport.createLinkedPair();
|
|
await server.connect(serverTransport);
|
|
await client.connect(clientTransport);
|
|
return {
|
|
client,
|
|
close: async () => {
|
|
await Promise.allSettled([
|
|
client.close(),
|
|
server.close(),
|
|
session.close(),
|
|
]);
|
|
},
|
|
};
|
|
};
|