mirror of
https://github.com/morluto/rea
synced 2026-10-26 06:16:17 -04:00
test(mcp): reuse immutable schema validators within analysis view suite
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { createMcpSchemaValidation } from "../../fixtures/mcpSchemaValidation.js";
|
||||
import { advertisedMcpToolSchemas } from "../../fixtures/advertisedMcpTool.js";
|
||||
import { API_SURFACE_EXAMPLE } from "../../../src/contracts/analysisViewApiSurfaceExample.js";
|
||||
import { ENVIRONMENT_MANIFEST_EXAMPLE } from "../../../src/contracts/analysisViewEnvironmentManifestExample.js";
|
||||
@@ -14,8 +15,7 @@ import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
import { parseMcpToolError } from "../../fixtures/mcpToolError.js";
|
||||
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
|
||||
import { STDIO_DEFAULT_MAX_BUFFER_SIZE } from "@modelcontextprotocol/server";
|
||||
import { Ajv2020 } from "ajv/dist/2020.js";
|
||||
import { expect, it, onTestFinished } from "vitest";
|
||||
import { afterAll, expect, it, onTestFinished } from "vitest";
|
||||
import { z } from "zod";
|
||||
|
||||
import { BinaryLayoutService } from "../../../src/application/binaryDiagnostics/BinaryLayoutService.js";
|
||||
@@ -51,6 +51,9 @@ import { wasmArtifactFixture } from "../../fixtures/wasm/artifact.js";
|
||||
import { evmInterfaceFixture } from "../../fixtures/evm/interface.js";
|
||||
import type { RelationshipCoverage } from "../../../src/domain/relationshipCoverage.js";
|
||||
|
||||
const schemaValidation = createMcpSchemaValidation();
|
||||
afterAll(() => schemaValidation.clear());
|
||||
|
||||
const connect = async (
|
||||
binaryLayout?: BinaryLayoutService,
|
||||
webNetworkCapture?: WebNetworkCaptureService,
|
||||
@@ -67,7 +70,10 @@ const connect = async (
|
||||
...(webNetworkCapture === undefined ? {} : { webNetworkCapture }),
|
||||
},
|
||||
);
|
||||
const client = new Client({ name: "analysis-view-mcp", version: "1" });
|
||||
const client = new Client(
|
||||
{ name: "analysis-view-mcp", version: "1" },
|
||||
{ jsonSchemaValidator: schemaValidation },
|
||||
);
|
||||
onTestFinished(async () => {
|
||||
await client.close();
|
||||
await server.close();
|
||||
@@ -85,6 +91,7 @@ it("preserves native relationship coverage through inline and retained MCP views
|
||||
const { ajv, outputSchema } = await advertisedMcpToolSchemas(
|
||||
client,
|
||||
"inspect_analysis_view",
|
||||
schemaValidation.assertions,
|
||||
);
|
||||
const coverages: (RelationshipCoverage | undefined)[] = [
|
||||
{
|
||||
@@ -156,6 +163,7 @@ it("advertises typed module references and preserves portable/retained Evidence
|
||||
const { ajv, inputSchema, outputSchema } = await advertisedMcpToolSchemas(
|
||||
client,
|
||||
"inspect_analysis_view",
|
||||
schemaValidation.assertions,
|
||||
);
|
||||
const view = {
|
||||
kind: "javascript",
|
||||
@@ -407,6 +415,7 @@ it("advertises exact schemas and projects one layout section from retained Evide
|
||||
const { ajv, inputSchema, outputSchema } = await advertisedMcpToolSchemas(
|
||||
client,
|
||||
"inspect_analysis_view",
|
||||
schemaValidation.assertions,
|
||||
);
|
||||
expect(
|
||||
ajv.validate(inputSchema, {
|
||||
@@ -758,7 +767,7 @@ it("delivers a bounded native dossier view over MCP without a Ghidra provider or
|
||||
);
|
||||
if (advertised?.outputSchema === undefined)
|
||||
throw new Error("missing view schemas");
|
||||
const ajv = new Ajv2020({ strict: false, validateFormats: false });
|
||||
const ajv = schemaValidation.assertions;
|
||||
const inputSchema: Record<string, unknown> = advertised.inputSchema;
|
||||
const outputSchema: Record<string, unknown> = advertised.outputSchema;
|
||||
const arguments_ = {
|
||||
@@ -1232,6 +1241,7 @@ it("extracts an environment state identically from retained, portable and file M
|
||||
const { ajv, inputSchema, outputSchema } = await advertisedMcpToolSchemas(
|
||||
client,
|
||||
"inspect_analysis_view",
|
||||
schemaValidation.assertions,
|
||||
);
|
||||
const capture = environmentStateCaptureFixture();
|
||||
const parent = analysisViewCaptureEvidence(capture);
|
||||
@@ -1305,6 +1315,7 @@ it("projects an environment manifest identically from retained, portable and fil
|
||||
const { ajv, inputSchema, outputSchema } = await advertisedMcpToolSchemas(
|
||||
client,
|
||||
"inspect_analysis_view",
|
||||
schemaValidation.assertions,
|
||||
);
|
||||
const capture = environmentManifestCaptureFixture();
|
||||
const parent = analysisViewCaptureEvidence(capture);
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import { expect, it } from "vitest";
|
||||
import { createMcpSchemaValidation } from "../../fixtures/mcpSchemaValidation.js";
|
||||
|
||||
it("reuses equal schemas and recompiles changed schemas sharing an id", () => {
|
||||
const validation = createMcpSchemaValidation();
|
||||
try {
|
||||
const schema = {
|
||||
$schema: "https://json-schema.org/draft/2020-12/schema",
|
||||
$id: "https://fixture.test/result",
|
||||
type: "string" as "string" | "integer",
|
||||
};
|
||||
const first = validation.getValidator(schema);
|
||||
expect(validation.getValidator(structuredClone(schema))).toBe(first);
|
||||
expect(first("recorded").valid).toBe(true);
|
||||
expect(first(42).valid).toBe(false);
|
||||
expect(validation.assertions.validate(schema, "recorded")).toBe(true);
|
||||
expect(validation.assertions.validate(schema, 42)).toBe(false);
|
||||
|
||||
schema.type = "integer";
|
||||
const changed = validation.getValidator(schema);
|
||||
expect(changed).not.toBe(first);
|
||||
expect(changed(42).valid).toBe(true);
|
||||
expect(changed("recorded").valid).toBe(false);
|
||||
expect(validation.assertions.validate(schema, 42)).toBe(true);
|
||||
expect(validation.assertions.validate(schema, "recorded")).toBe(false);
|
||||
|
||||
validation.clear();
|
||||
expect(validation.getValidator(schema)).not.toBe(changed);
|
||||
} finally {
|
||||
validation.clear();
|
||||
}
|
||||
});
|
||||
|
||||
it("preserves SDK format validation and declared dialect dispatch", () => {
|
||||
const validation = createMcpSchemaValidation();
|
||||
try {
|
||||
const schema = {
|
||||
$schema: "http://json-schema.org/draft-07/schema#",
|
||||
type: "string",
|
||||
format: "email",
|
||||
} as const;
|
||||
const validator = validation.getValidator(schema);
|
||||
expect(validator("analyst@example.test").valid).toBe(true);
|
||||
expect(validator("invalid email").valid).toBe(false);
|
||||
// Explicit schema assertions retain their separate format-off policy.
|
||||
expect(
|
||||
validation.assertions.validate(
|
||||
{ type: "string", format: "email" },
|
||||
"invalid email",
|
||||
),
|
||||
).toBe(true);
|
||||
expect(
|
||||
validation.assertions.validateSchema({ type: "string", minLength: -1 }),
|
||||
).toBe(false);
|
||||
} finally {
|
||||
validation.clear();
|
||||
}
|
||||
});
|
||||
|
||||
it("owns nested schema values independently of later caller mutation", () => {
|
||||
const validation = createMcpSchemaValidation();
|
||||
try {
|
||||
const schema = { enum: [{ state: "recorded" }, { state: "other" }] };
|
||||
const originalSchema = structuredClone(schema);
|
||||
const original = validation.getValidator(schema);
|
||||
expect(validation.assertions.validate(schema, { state: "recorded" })).toBe(
|
||||
true,
|
||||
);
|
||||
schema.enum[0]!.state = "changed";
|
||||
const changed = validation.getValidator(schema);
|
||||
expect(original({ state: "recorded" }).valid).toBe(true);
|
||||
expect(original({ state: "changed" }).valid).toBe(false);
|
||||
expect(changed({ state: "changed" }).valid).toBe(true);
|
||||
expect(changed({ state: "recorded" }).valid).toBe(false);
|
||||
expect(
|
||||
validation.assertions.validate(originalSchema, { state: "recorded" }),
|
||||
).toBe(true);
|
||||
expect(validation.assertions.validate(schema, { state: "recorded" })).toBe(
|
||||
false,
|
||||
);
|
||||
expect(validation.assertions.validate(schema, { state: "changed" })).toBe(
|
||||
true,
|
||||
);
|
||||
} finally {
|
||||
validation.clear();
|
||||
}
|
||||
});
|
||||
Vendored
+14
-2
@@ -5,17 +5,29 @@ import { toolContract } from "../../src/contracts/toolContracts.js";
|
||||
import { parseEvidence } from "../../src/domain/evidence.js";
|
||||
import type { BinarySession } from "../../src/application/binary/BinarySession.js";
|
||||
|
||||
/** Verify the actual advertised schemas, retaining a private Ajv registry. */
|
||||
interface AdvertisedSchemaAssertions {
|
||||
readonly errors: unknown;
|
||||
validateSchema(schema: Record<string, unknown>): boolean | Promise<unknown>;
|
||||
validate(
|
||||
schema: Record<string, unknown>,
|
||||
value: unknown,
|
||||
): boolean | Promise<unknown>;
|
||||
}
|
||||
|
||||
/** Verify actual advertised schemas; callers may own file-local preparation. */
|
||||
export const advertisedMcpToolSchemas = async (
|
||||
client: Client,
|
||||
name: string,
|
||||
ajv: AdvertisedSchemaAssertions = new Ajv2020({
|
||||
strict: false,
|
||||
validateFormats: false,
|
||||
}),
|
||||
) => {
|
||||
const advertised = (await client.listTools()).tools.find(
|
||||
(tool) => tool.name === name,
|
||||
);
|
||||
if (advertised?.outputSchema === undefined)
|
||||
throw new Error(`${name} must publish both schemas`);
|
||||
const ajv = new Ajv2020({ strict: false, validateFormats: false });
|
||||
const inputSchema: Record<string, unknown> = advertised.inputSchema;
|
||||
const outputSchema: Record<string, unknown> = advertised.outputSchema;
|
||||
expect(ajv.validateSchema(inputSchema)).toBe(true);
|
||||
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
import type {
|
||||
JsonSchemaType,
|
||||
JsonSchemaValidator,
|
||||
} from "@modelcontextprotocol/client";
|
||||
import { AjvJsonSchemaValidator } from "@modelcontextprotocol/client/validators/ajv";
|
||||
import { Ajv2020 } from "ajv/dist/2020.js";
|
||||
import type { ValidateFunction } from "ajv";
|
||||
|
||||
/** Share immutable schema preparation within one test file, never its sessions. */
|
||||
export const createMcpSchemaValidation = () => {
|
||||
const sdkValidators = new Map<string, JsonSchemaValidator<unknown>>();
|
||||
const assertionValidators = new Map<string, ValidateFunction>();
|
||||
const meta = new Ajv2020({ strict: false, validateFormats: false });
|
||||
let errors: unknown = null;
|
||||
return {
|
||||
getValidator<T>(schema: JsonSchemaType): JsonSchemaValidator<T> {
|
||||
const key = JSON.stringify(schema);
|
||||
let validator = sdkValidators.get(key);
|
||||
if (validator === undefined) {
|
||||
// Each full schema owns its default provider, including dialect dispatch.
|
||||
// Distinct schemas may legally reuse the same $id in separate servers.
|
||||
validator = new AjvJsonSchemaValidator().getValidator(JSON.parse(key));
|
||||
sdkValidators.set(key, validator);
|
||||
}
|
||||
return validator as JsonSchemaValidator<T>;
|
||||
},
|
||||
assertions: {
|
||||
get errors() {
|
||||
return errors;
|
||||
},
|
||||
validateSchema(schema: Record<string, unknown>) {
|
||||
const valid = meta.validateSchema(schema);
|
||||
errors = meta.errors;
|
||||
return valid;
|
||||
},
|
||||
validate(schema: Record<string, unknown>, value: unknown) {
|
||||
const key = JSON.stringify(schema);
|
||||
let validator = assertionValidators.get(key);
|
||||
if (validator === undefined) {
|
||||
// Preserve the explicit assertions' existing options, independently
|
||||
// of the SDK default provider's format/all-errors policy.
|
||||
validator = new Ajv2020({
|
||||
strict: false,
|
||||
validateFormats: false,
|
||||
}).compile(JSON.parse(key));
|
||||
assertionValidators.set(key, validator);
|
||||
}
|
||||
const valid = validator(value);
|
||||
errors = validator.errors;
|
||||
return valid;
|
||||
},
|
||||
},
|
||||
clear() {
|
||||
sdkValidators.clear();
|
||||
assertionValidators.clear();
|
||||
meta.removeSchema();
|
||||
errors = null;
|
||||
},
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user