fix(enhanced): bound batch decompile input size (#5937)

This commit is contained in:
Lucien authored and GitHub committed 2026-11-05 16:57:52 +08:00
1 parent fc3ff7b833
commit 585fe56712
4 files changed
+106

No files matched your search

@@ -0,0 +1,35 @@
import { describe, expect, it } from "vitest";
import { z } from "zod";
import type { AnalysisOperationPort } from "./AnalysisProvider.js";
import { EnhancedTools } from "./EnhancedTools.js";
import { AnalysisInputError } from "../domain/analysisErrorCore.js";
import { MAX_ENHANCED_RECORDS } from "../contracts/enhancedInputs.js";
describe("EnhancedTools input validation", () => {
it("rejects an oversized batch_decompile before provider admission", async () => {
let calls = 0;
const analysis: AnalysisOperationPort = {
execute: async () => {
calls += 1;
throw new Error("provider must not be called for invalid input");
},
};
const result = await new EnhancedTools(analysis).execute(
"batch_decompile",
{
addresses: Array.from(
{ length: MAX_ENHANCED_RECORDS + 1 },
() => "0x0",
),
},
);
expect(result.ok).toBe(false);
if (result.ok) return;
expect(result.error).toBeInstanceOf(AnalysisInputError);
expect(result.error.cause).toBeInstanceOf(z.ZodError);
expect(calls).toBe(0);
});
});
+37
View File
@@ -0,0 +1,37 @@
import { Ajv2020 } from "ajv/dist/2020.js";
import { describe, expect, it } from "vitest";
import {
enhancedInputSchemas,
MAX_ENHANCED_RECORDS,
} from "./enhancedInputs.js";
import { advertisedInputJsonSchema } from "./toolSchemaMetadata.js";
import { toolContract } from "./toolContracts.js";
describe("batch_decompile input contract", () => {
it("bounds addresses in the canonical and advertised schemas", () => {
const addresses = Array.from({ length: MAX_ENHANCED_RECORDS }, () => "0x0");
const schema = enhancedInputSchemas.batch_decompile;
expect(schema.safeParse({ addresses }).success).toBe(true);
expect(schema.safeParse({ addresses: [...addresses, "0x1"] }).success).toBe(
false,
);
const advertised = advertisedInputJsonSchema(
toolContract("batch_decompile"),
);
const advertisedAddresses = (
advertised.properties as Record<string, unknown>
).addresses;
expect(advertisedAddresses).toMatchObject({
type: "array",
maxItems: MAX_ENHANCED_RECORDS,
});
const ajv = new Ajv2020({ strict: false, validateFormats: false });
expect(ajv.validateSchema(advertised)).toBe(true);
const validate = ajv.compile(advertised);
expect(validate({ addresses })).toBe(true);
expect(validate({ addresses: [...addresses, "0x1"] })).toBe(false);
});
});
+1
View File
@@ -26,6 +26,7 @@ export const enhancedInputSchemas = {
batch_decompile: z.strictObject({
addresses: z
.array(z.string().describe("A provider-normalized procedure address"))
.max(MAX_ENHANCED_RECORDS)
.default([]),
}),
decompile_all: decompileDirectoryInputSchema,
@@ -12,6 +12,7 @@ import { batchDecompileResultSchema } from "../../../src/domain/native/batchDeco
import { ENHANCED_TOOL_CONTRACTS } from "../../../src/contracts/enhancedToolContracts.js";
import type { RelationshipCoverage } from "../../../src/domain/relationshipCoverage.js";
import { err, ok as resultOk } from "../../../src/domain/result.js";
import { MAX_ENHANCED_RECORDS } from "../../../src/contracts/enhancedInputs.js";
import { parseMcpToolError } from "../../fixtures/mcpToolError.js";
import {
@@ -485,6 +486,38 @@ describe("enhanced MCP batch cancellation", () => {
});
describe("enhanced MCP batches", () => {
it("advertises and enforces the batch input budget before provider calls", async () => {
let calls = 0;
const client = await connect({
execute: async () => {
calls += 1;
throw new Error("provider must not be called for invalid input");
},
});
const { tools } = await client.listTools();
expect(
tools.find(({ name }) => name === "batch_decompile")?.inputSchema,
).toMatchObject({
properties: { addresses: { maxItems: MAX_ENHANCED_RECORDS } },
});
const result = await client.callTool({
name: "batch_decompile",
arguments: {
addresses: Array.from(
{ length: MAX_ENHANCED_RECORDS + 1 },
() => "0x0",
),
},
});
expect(result.isError).toBe(true);
expect(parseMcpToolError(result)).toMatchObject({
error: { code: "invalid_request", category: "invalid_input" },
});
expect(calls).toBe(0);
});
it("returns every batch item in caller order with its own result", async () => {
const addresses = Array.from(
{ length: 37 },