Separate independent exclusion, partial-source, credential-preflight and descriptor-failure workflows so their costs do not accumulate under one test deadline. Reuse one worker-owned MCP connection with explicit awaited teardown, preserving sensitivity isolation and retained-Evidence checks. Assisted-by: Reimu (gensokyo-skills:reimu-incident-triage)
107 lines
3.7 KiB
TypeScript
107 lines
3.7 KiB
TypeScript
import {
|
|
Client,
|
|
type CallToolRequestOptions,
|
|
} from "@modelcontextprotocol/client";
|
|
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
|
|
import { Ajv2020 } from "ajv/dist/2020.js";
|
|
import type { ValidateFunction } from "ajv";
|
|
import { fileURLToPath } from "node:url";
|
|
import { expect, onTestFinished } from "vitest";
|
|
import { z } from "zod";
|
|
|
|
// validateSchema checks against the meta-schema without registering user $ids.
|
|
const schemaValidator = new Ajv2020({
|
|
strict: false,
|
|
validateFormats: false,
|
|
});
|
|
const validatedSchemas = new Set<string>();
|
|
const compiledValidators = new Map<string, ValidateFunction<unknown>>();
|
|
|
|
const compileValidator = (schema: unknown): ValidateFunction<unknown> => {
|
|
const schemaRecord = z.record(z.string(), z.unknown()).parse(schema);
|
|
const serialized = JSON.stringify(schemaRecord);
|
|
const cached = compiledValidators.get(serialized);
|
|
if (cached !== undefined) return cached;
|
|
|
|
// Keep a private Ajv registry per schema: distinct tool schemas can carry
|
|
// identical $id values, while this cache only reuses exact schema content.
|
|
const validate = new Ajv2020({
|
|
strict: false,
|
|
validateFormats: false,
|
|
}).compile<unknown>(schemaRecord);
|
|
compiledValidators.set(serialized, validate);
|
|
return validate;
|
|
};
|
|
|
|
/** Start the built production server without configuring deep binary providers. */
|
|
export async function connectLocalToolsMcp(
|
|
environment: Readonly<Record<string, string>> = {},
|
|
registerCleanup: (cleanup: () => Promise<void>) => void = onTestFinished,
|
|
) {
|
|
const transport = new StdioClientTransport({
|
|
command: process.execPath,
|
|
args: [
|
|
fileURLToPath(new URL("../../scripts/rea.mjs", import.meta.url)),
|
|
"mcp",
|
|
],
|
|
env: {
|
|
PATH: process.env.PATH ?? "/usr/bin:/bin",
|
|
REA_LOG_LEVEL: "silent",
|
|
HOPPER_LAUNCHER_PATH: "/rea-unconfigured-deep-provider/hopper",
|
|
...environment,
|
|
},
|
|
stderr: "pipe",
|
|
});
|
|
const client = new Client({ name: "local-tools-e2e", version: "1" });
|
|
registerCleanup(async () => {
|
|
await client.close();
|
|
await transport.close();
|
|
});
|
|
await client.connect(transport);
|
|
const tools = (await client.listTools()).tools;
|
|
for (const tool of tools)
|
|
for (const [kind, schema] of [
|
|
["inputSchema", tool.inputSchema],
|
|
["outputSchema", tool.outputSchema],
|
|
] as const) {
|
|
if (schema === undefined) continue;
|
|
const serialized = JSON.stringify(schema);
|
|
if (validatedSchemas.has(serialized)) continue;
|
|
const schemaRecord = z.record(z.string(), z.unknown()).parse(schema);
|
|
const valid = schemaValidator.validateSchema(schemaRecord);
|
|
expect(
|
|
valid,
|
|
`${tool.name}.${kind}: ${schemaValidator.errorsText(schemaValidator.errors)}`,
|
|
).toBe(true);
|
|
validatedSchemas.add(serialized);
|
|
}
|
|
const definitions = new Map(tools.map((tool) => [tool.name, tool]));
|
|
const call = async (
|
|
name: string,
|
|
arguments_: Record<string, unknown>,
|
|
options?: Pick<CallToolRequestOptions, "signal" | "onprogress">,
|
|
) => {
|
|
const definition = definitions.get(name);
|
|
if (definition !== undefined) compileValidator(definition.inputSchema);
|
|
const response = await client.callTool(
|
|
{ name, arguments: arguments_ },
|
|
{
|
|
...options,
|
|
...(definition === undefined ? {} : { toolDefinition: definition }),
|
|
},
|
|
);
|
|
if (
|
|
response.structuredContent !== undefined &&
|
|
definition?.outputSchema !== undefined
|
|
) {
|
|
const validate = compileValidator(definition.outputSchema);
|
|
expect(
|
|
validate(response.structuredContent),
|
|
JSON.stringify(validate.errors),
|
|
).toBe(true);
|
|
}
|
|
return response;
|
|
};
|
|
return { client, call };
|
|
}
|