mirror of
https://github.com/morluto/rea.git
synced 2026-11-13 18:22:42 +00:00
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 <gensokyo-dev@users.noreply.github.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b67c1e8ce6
commit
3cb75d46bf
1 file changed
+49
-11
@@ -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<NodeJS.ProcessEnv>,
|
||||
) => {
|
||||
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<string, unknown>[] = [];
|
||||
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 }) => /\(\?<?[=!]|\\[1-9]|\\k</u.test(pattern)),
|
||||
).toEqual([]);
|
||||
|
||||
// DeepSeek rejects the short NUL escape even though V8 accepts it in all
|
||||
// three modes. Keep the shared hex spelling across the advertised catalog;
|
||||
// actual provider acceptance is verified separately from JS compilation.
|
||||
expect(
|
||||
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([]);
|
||||
},
|
||||
);
|
||||
Reference in new issue
Block a user