rea/tests/boundary/mcp/artifactMcp.inlineInventory...

181 lines
6.2 KiB
TypeScript

import { readFile, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
import { TextReader, Uint8ArrayWriter, ZipWriter } from "@zip.js/zip.js";
import { expect, it } from "vitest";
import { z } from "zod";
import { parseConfig } from "../../../src/config.js";
import { createBinarySession } from "../../../src/application/runtime.js";
import { keyedArchiveResultSchema } from "../../../src/domain/apple/keyedArchive.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { createTestBinarySession } from "../../fixtures/binarySession.js";
import { ArtifactProvider } from "../../../src/artifacts/ArtifactProvider.js";
import { createServer } from "../../../src/server/createServer.js";
it("inspects a standalone keyed archive through MCP with original object identities", async () => {
const directory = await createTestTempDirectory("rea-keyed-mcp-");
const path = join(directory, "archive.plist");
await writeFile(
path,
await readFile(
new URL(
"../../fixtures/golden/keyed-archive/foundation.xml",
import.meta.url,
),
),
);
const session = createTestBinarySession(new ArtifactProvider());
const server = createServer(session, session);
const client = new Client({ name: "keyed-mcp-test", version: "1" });
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
try {
await server.connect(serverTransport);
await client.connect(clientTransport);
const opened = await client.callTool({
name: "open_binary",
arguments: { path },
});
expect(opened.isError).not.toBe(true);
const called = await client.callTool({
name: "inspect_keyed_archive",
arguments: {},
});
expect(called.isError, JSON.stringify(called.structuredContent)).not.toBe(
true,
);
const graph = keyedArchiveResultSchema.parse(
z.object({ result: z.unknown() }).parse(called.structuredContent).result,
);
expect(graph.references).toEqual(
expect.arrayContaining([
expect.objectContaining({ source: 2, target: 2, status: "resolved" }),
expect.objectContaining({
source: 1,
target: 2,
status: "resolved",
}),
]),
);
const rejected = await client.callTool({
name: "inspect_keyed_archive",
arguments: { path: "other.plist" },
});
expect(rejected.isError).toBe(true);
} finally {
await client.close();
await server.close();
await session.close();
}
});
it("returns every artifact occurrence in one MCP call", async () => {
const directory = await createTestTempDirectory("rea-artifact-inline-");
const archive = join(directory, "many.zip");
const writer = new ZipWriter(new Uint8ArrayWriter());
const fileCount = 520;
for (let index = 0; index < fileCount; index += 1) {
await writer.add(
`files/${String(index)}.txt`,
new TextReader(`file-${String(index)}`),
);
}
await writeFile(archive, await writer.close());
const configured = parseConfig({
HOPPER_LAUNCHER_PATH: "/rea-unconfigured-deep-provider/hopper",
});
expect(configured.ok).toBe(true);
if (!configured.ok) throw configured.error;
const session = createBinarySession(configured.value);
const server = createServer(session, session);
const client = new Client({ name: "artifact-inline-test", version: "1" });
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
try {
await server.connect(serverTransport);
await client.connect(clientTransport);
const opened = await client.callTool({
name: "open_binary",
arguments: { path: archive },
});
expect(opened.isError).not.toBe(true);
const names = (await client.listTools()).tools.map(({ name }) => name);
expect(names).toContain("inspect_artifact");
expect(names).toContain("inspect_asset_catalog");
expect(names).not.toContain("inventory_artifact");
const status = await client.callTool({
name: "binary_session",
arguments: {},
});
expect(status.isError).not.toBe(true);
const capabilities = z
.object({
result: z.object({
capabilities: z.array(
z.object({
operation: z.string(),
available: z.boolean(),
reason: z.string().nullable(),
}),
),
}),
})
.parse(status.structuredContent).result.capabilities;
expect(capabilities.map(({ operation }) => operation)).not.toContain(
"inventory_artifact",
);
expect(
capabilities.find(
({ operation }) => operation === "inspect_asset_catalog",
),
).toMatchObject(
process.platform === "darwin"
? { available: true, reason: null }
: {
available: false,
reason: "Apple asset catalogs require macOS assetutil.",
},
);
const result = await client.callTool({
name: "inspect_artifact",
arguments: {},
});
expect(result.isError, JSON.stringify(result.structuredContent)).not.toBe(
true,
);
const inspection = z
.object({
substeps: z.array(
z.object({
evidence: z.object({
normalized_result: z.object({
occurrences: z.array(z.object({ logical_path: z.string() })),
}),
}),
}),
),
observations: z.array(z.unknown()),
derived_relationships: z.array(z.unknown()),
hypotheses: z.array(z.unknown()),
unexplored_branches: z.array(z.unknown()),
next_probes: z.array(z.unknown()),
})
.parse(
z.object({ result: z.unknown() }).parse(result.structuredContent)
.result,
);
const occurrences =
inspection.substeps[0]?.evidence.normalized_result.occurrences;
if (occurrences === undefined)
throw new Error("inspection omitted the artifact inventory");
expect(occurrences).toHaveLength(fileCount + 1);
expect(inspection.observations.length).toBeGreaterThan(0);
expect(inspection.derived_relationships.length).toBeGreaterThan(0);
} finally {
await Promise.allSettled([client.close(), server.close(), session.close()]);
}
});