mirror of
https://github.com/morluto/rea.git
synced 2026-10-30 19:35:56 +00:00
Since #1277 an error result reaches the client without structuredContent, but toErrorToolResult and the transport recovery still budgeted it as structured JSON plus escaped text. An error whose text fit between about half and all of the response budget was replaced by resource_constraint, and without Evidence retention the original diagnostic was lost. Count only the escaped text for errors. The boundary, process-global and real Ghidra large-result probes now use diagnostics that exceed the text-only budget. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
77 lines
2.9 KiB
TypeScript
77 lines
2.9 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
|
|
import type { JsonValue } from "../domain/jsonValue.js";
|
|
import { encodeToolResult } from "./toolResultEncoding.js";
|
|
|
|
const projectedResult = (candidate: JsonValue) => ({
|
|
content: [{ type: "text", text: JSON.stringify(candidate) }],
|
|
structuredContent: candidate,
|
|
});
|
|
|
|
describe("MCP result encoding budget", () => {
|
|
it.each([
|
|
{ result: { value: 1 } },
|
|
{ result: ["中文", "😀", "\ud800", '"\\\n'] },
|
|
{ result: "x".repeat(8191) + "😀" + '中文\n"'.repeat(20000) },
|
|
])("counts actual UTF-8 structured and escaped text bytes", (candidate) => {
|
|
const encoded = encodeToolResult(
|
|
candidate,
|
|
Buffer.byteLength(JSON.stringify(projectedResult(candidate))),
|
|
);
|
|
if (!encoded.ok) throw new Error("Expected complete encoding");
|
|
expect(encoded.text).toBe(JSON.stringify(candidate));
|
|
expect(encoded.bytes).toBe(
|
|
Buffer.byteLength(JSON.stringify(projectedResult(candidate))),
|
|
);
|
|
});
|
|
|
|
it("accounts for complete Evidence in structured and text representations", () => {
|
|
const normalized = { value: '中文"'.repeat(30000) };
|
|
const candidate = {
|
|
normalized_result: normalized,
|
|
raw_result: { observation: "distinct provider representation" },
|
|
};
|
|
const completeBytes = Buffer.byteLength(
|
|
JSON.stringify(projectedResult(candidate)),
|
|
);
|
|
expect(encodeToolResult(candidate, completeBytes).ok).toBe(true);
|
|
expect(encodeToolResult(candidate, completeBytes - 1)).toMatchObject({
|
|
ok: false,
|
|
bytesAtLeast: completeBytes,
|
|
});
|
|
});
|
|
|
|
it.each([
|
|
{ error: { code: "invalid_request", message: '中文"\n'.repeat(30000) } },
|
|
{ error: { message: "😀\ud800" } },
|
|
])("counts only escaped text for text-only error delivery", (candidate) => {
|
|
const textBytes = Buffer.byteLength(
|
|
JSON.stringify({
|
|
content: [{ type: "text", text: JSON.stringify(candidate) }],
|
|
}),
|
|
);
|
|
const encoded = encodeToolResult(candidate, textBytes, "text");
|
|
if (!encoded.ok) throw new Error("Expected complete encoding");
|
|
expect(encoded.text).toBe(JSON.stringify(candidate));
|
|
expect(encoded.bytes).toBe(textBytes);
|
|
expect(encodeToolResult(candidate, textBytes - 1, "text")).toMatchObject({
|
|
ok: false,
|
|
bytesAtLeast: textBytes,
|
|
});
|
|
});
|
|
|
|
it("stops before reading the remainder of an oversized result", () => {
|
|
const candidate: JsonValue = { text: "x".repeat(1000000) };
|
|
Object.defineProperty(candidate, "remainder", {
|
|
enumerable: true,
|
|
get() {
|
|
throw new Error("The remainder must not be read");
|
|
},
|
|
});
|
|
// Only an already-known first property is visited before exhausting the
|
|
// budget. The serializer's property discovery must not eagerly read values.
|
|
expect(() => encodeToolResult(candidate, 1000)).not.toThrow();
|
|
expect(encodeToolResult(candidate, 1000).ok).toBe(false);
|
|
});
|
|
});
|