Files
rea/tests/sessionMcp.test.ts
T

116 lines
4.3 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 type { CallToolResult } from "@modelcontextprotocol/server";
import { afterEach, describe, expect, it } from "vitest";
import { z } from "zod";
import { BinarySession } from "../src/application/BinarySession.js";
import type { AnalysisClient } from "../src/application/AnalysisProvider.js";
import { ok } from "../src/domain/result.js";
import { createServer } from "../src/server/createServer.js";
import { evidenceBundleSchema } from "../src/domain/evidenceBundle.js";
const resources: Array<{ close(): Promise<unknown> }> = [];
let directory: string | undefined;
afterEach(async () => {
await Promise.all(resources.splice(0).map((resource) => resource.close()));
if (directory !== undefined)
await rm(directory, { recursive: true, force: true });
directory = undefined;
});
describe("target-free MCP lifecycle", () => {
it("reports no-target, opens, analyzes, switches, reports status, and closes", async () => {
directory = await mkdtemp(join(tmpdir(), "rea-mcp-session-"));
const first = join(directory, "first.hop");
const second = join(directory, "second.hop");
await writeFile(first, "one");
await writeFile(second, "two");
const closed: string[] = [];
const session = new BinarySession((target) => client(target.path, closed));
const server = createServer(session, session);
const mcp = new Client({ name: "session-test", version: "1.0.0" });
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
resources.push(mcp, server);
await server.connect(serverTransport);
await mcp.connect(clientTransport);
const before = await mcp.callTool({
name: "current_document",
arguments: {},
});
expect(before.isError).toBe(true);
expect(text(before)).toContain("NoBinaryOpenError");
expect((await mcp.listTools()).tools).toHaveLength(50);
const deniedCapture = await mcp.callTool({
name: "capture_process_scenario",
arguments: {
approved: true,
executable: "/bin/sh",
working_directory: "/tmp",
},
});
expect(deniedCapture.isError).toBe(true);
expect(text(deniedCapture)).toContain("process capture is disabled");
expect(
(await mcp.callTool({ name: "open_binary", arguments: { path: first } }))
.isError,
).not.toBe(true);
expect(
text(await mcp.callTool({ name: "current_document", arguments: {} })),
).toContain("first.hop");
const exported = await mcp.callTool({
name: "export_evidence_bundle",
arguments: {},
});
const bundle = evidenceBundleSchema.parse(structured(exported).result);
expect(bundle.records).toHaveLength(1);
expect(
structured(
await mcp.callTool({
name: "import_evidence_bundle",
arguments: { bundle },
}),
).result,
).toEqual({ imported: 0, total: 1 });
await mcp.callTool({ name: "open_binary", arguments: { path: second } });
expect(closed.some((path) => path.endsWith("first.hop"))).toBe(true);
expect(
text(await mcp.callTool({ name: "binary_session", arguments: {} })),
).toContain("second.hop");
await mcp.callTool({ name: "close_binary", arguments: {} });
expect(
text(await mcp.callTool({ name: "binary_session", arguments: {} })),
).toContain('"open": false');
});
});
const client = (path: string, closed: string[]): AnalysisClient => ({
execute: (name) =>
Promise.resolve(
ok(name === "health" ? null : name === "current_document" ? path : null),
),
close: () => {
closed.push(path);
return Promise.resolve();
},
});
const text = (result: CallToolResult): string => {
const content = result.content.find((item) => item.type === "text");
if (content?.type !== "text") throw new Error("missing text result");
return content.text;
};
const structured = (result: CallToolResult): Record<string, unknown> => {
if (
typeof result.structuredContent !== "object" ||
result.structuredContent === null
)
throw new Error("missing structured result");
return z.record(z.string(), z.unknown()).parse(result.structuredContent);
};