From 3cb75d46bf6ee0a44d846a4d3e2cc3196e9ba418 Mon Sep 17 00:00:00 2001 From: gensokyo-dev Date: Fri, 13 Nov 2026 16:52:21 +0000 Subject: [PATCH] test(mcp): guard portable pattern escapes in every schema profile The advertised-pattern guard only inspected the default profile and only rejected the `\0` and `\u0000` NUL spellings, so a `\uD800` range shipped in 6.9.0 unnoticed. Reject any unescaped `\u` or `\0` escape, and run that check for each input and output schema profile. Co-Authored-By: gensokyo-dev Co-Authored-By: Claude Opus 5.5 --- .../mcp/advertisedPatternsVMode.test.ts | 60 +++++++++++++++---- 1 file changed, 49 insertions(+), 11 deletions(-) diff --git a/tests/boundary/mcp/advertisedPatternsVMode.test.ts b/tests/boundary/mcp/advertisedPatternsVMode.test.ts index 1291d79d9a..022b43d673 100644 --- a/tests/boundary/mcp/advertisedPatternsVMode.test.ts +++ b/tests/boundary/mcp/advertisedPatternsVMode.test.ts @@ -64,11 +64,16 @@ const collectPatterns = ( } }; -it("advertises portable NUL escapes and patterns that compile in all JS modes", async () => { +const listAdvertisedTools = async ( + environment?: Readonly, +) => { const session = createTestBinarySession(() => { throw new Error("No deep provider may start for a schema projection"); }); - const server = createServer({ kind: "session", session }); + const server = createServer( + { kind: "session", session }, + environment === undefined ? {} : { environment }, + ); const client = new Client({ name: "advertised-patterns", version: "1" }); onTestFinished(async () => { await client.close(); @@ -79,8 +84,20 @@ it("advertises portable NUL escapes and patterns that compile in all JS modes", InMemoryTransport.createLinkedPair(); await server.connect(serverTransport); await client.connect(clientTransport); + return (await client.listTools()).tools; +}; - const advertised = (await client.listTools()).tools; +// A `\u` or `\0` escape that is not itself escaped. RE2 has no `\u` escape, +// Rust refuses `\u` surrogate ranges, and DeepSeek rejects the short NUL escape +// even though V8 accepts all of them; any one refuses the complete tools +// request. Unlike the compile checks above, this guard covers every profile. +const NON_PORTABLE_ESCAPE = /(?:^|[^\\])(?:\\\\)*\\[0u]/u; + +const nonPortableEscapes = (patterns: readonly AdvertisedPattern[]) => + patterns.filter(({ pattern }) => NON_PORTABLE_ESCAPE.test(pattern)); + +it("advertises portable NUL escapes and patterns that compile in all JS modes", async () => { + const advertised = await listAdvertisedTools(); const patterns: AdvertisedPattern[] = []; const canonicalBase64Leaves: Record[] = []; for (const tool of advertised) { @@ -129,14 +146,9 @@ it("advertises portable NUL escapes and patterns that compile in all JS modes", patterns.filter(({ pattern }) => /\(\? pattern.includes("\\0") || pattern.includes("\\u0000"), - ), - ).toEqual([]); + // Keep the shared hex NUL spelling across the advertised catalog; actual + // provider acceptance is verified separately from JS compilation. + expect(nonPortableEscapes(patterns)).toEqual([]); expect(patterns.some(({ pattern }) => pattern.includes("\\x00"))).toBe(true); expect(canonicalBase64Leaves.length).toBeGreaterThan(0); @@ -154,3 +166,29 @@ it("advertises portable NUL escapes and patterns that compile in all JS modes", ), ); }); + +it.each( + (["compact", "full"] as const).flatMap((input) => + (["full", "compact", "off"] as const).map((output) => [input, output]), + ), +)( + "advertises portable escapes for %s input and %s output schemas", + async (input, output) => { + const advertised = await listAdvertisedTools({ + ...process.env, + REA_MCP_INPUT_SCHEMA_PROFILE: input, + REA_MCP_OUTPUT_SCHEMA_PROFILE: output, + }); + const patterns: AdvertisedPattern[] = []; + for (const tool of advertised) { + collectPatterns(tool.inputSchema, "inputSchema", tool.name, patterns); + if (tool.outputSchema !== undefined) + collectPatterns(tool.outputSchema, "outputSchema", tool.name, patterns); + } + // Every profile carries local path inputs, so the guard is not vacuous. + expect(patterns.some(({ pattern }) => pattern.includes("\\x00"))).toBe( + true, + ); + expect(nonPortableEscapes(patterns)).toEqual([]); + }, +);