Files
OmniRoute/tests/unit/mcp-web-search-provider-enum-contract.test.ts
Sahil Singh 8ff3a1dda3 fix(mcp): dynamically generate web search provider enum from registry (#10209)
* fix(mcp): dynamically generate web search provider enum from registry

* test(mcp): add contract test for dynamic web search provider enum

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* fix(mcp): restore search.ts eslint suppression, type builder maps, fix firecrawl searchType arg

The enum-dynamic refactor dropped search.ts's no-explicit-any suppression
while a new Record<string,any> map re-introduced anys, and the response
normalizer map swapped the firecrawl searchType argument with query. Type
both maps explicitly, restore the base suppression (33 pre-existing anys),
and pass searchType (not query) to normalizeFirecrawlSearchResponse.

Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>

---------

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: adevwithpurpose <adevwithpurpose@users.noreply.github.com>
Co-authored-by: sadSanta-07 <sadSanta-07@users.noreply.github.com>
Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
2026-08-17 07:01:46 -03:00

74 lines
3.4 KiB
TypeScript

// #10209 — contract test: the MCP `omniroute_web_search` `provider` enum is now
// generated dynamically from the search registry (`getActiveSearchProviders`).
// This pins the invariant that the dynamic enum does not silently break the Zod
// schema exposed to MCP clients (or the tool's scope), regardless of future
// registry additions/removals or `disabled` flags. If the registry drifts, this
// test turns red before any client sees a broken/widened contract.
import test from "node:test";
import assert from "node:assert/strict";
const { getActiveSearchProviders } = await import("../../open-sse/mcp-server/schemas/providerEnums.ts");
const { SEARCH_PROVIDERS } = await import("../../open-sse/config/searchRegistry.ts");
const { webSearchInput, webSearchTool } = await import("../../open-sse/mcp-server/schemas/tools.ts");
// The provider set the tool historically exposed to MCP clients (the hardcoded
// enum this PR replaced). Every one of these MUST keep parsing so existing
// clients are never broken by the dynamic enum.
const LEGACY_CONTRACT_PROVIDERS = [
"serper-search",
"brave-search",
"perplexity-search",
"exa-search",
"tavily-search",
"google-pse-search",
"linkup-search",
"searchapi-search",
"searxng-search",
] as const;
function activeRegistryIds(): string[] {
return Object.values(SEARCH_PROVIDERS)
.filter((p) => !p.disabled)
.map((p) => p.id)
.sort();
}
test("getActiveSearchProviders() is a non-empty tuple of active registry providers", () => {
const active = getActiveSearchProviders();
assert.ok(Array.isArray(active), "should return an array (Zod enum tuple)");
assert.ok(active.length > 0, "dynamic enum must never be empty");
assert.deepEqual([...active].sort(), activeRegistryIds());
});
test("web_search inputSchema provider enum equals the active provider set", () => {
const shape = webSearchInput.shape as { provider: { unwrap: () => unknown } };
const providerManager = shape.provider; // ZodOptional around the ZodEnum
const unwrapped = providerManager.unwrap() as { options: string[] };
const options = unwrapped.options;
assert.ok(Array.isArray(options), "provider field should be an enum");
assert.ok(options.length > 0, "provider enum should have at least one value");
assert.deepEqual([...options].sort(), activeRegistryIds());
});
test("dynamic enum is a superset of the legacy contract (existing clients not broken)", () => {
for (const id of LEGACY_CONTRACT_PROVIDERS) {
const result = webSearchInput.safeParse({ query: "test", provider: id });
assert.ok(result.success, `legacy provider "${id}" must still parse`);
}
// Every active registry provider must also be selectable by an MCP client.
for (const id of activeRegistryIds()) {
const result = webSearchInput.safeParse({ query: "test", provider: id });
assert.ok(result.success, `active registry provider "${id}" must parse`);
}
});
test("unknown provider values are rejected (contract stays tight)", () => {
const result = webSearchInput.safeParse({ query: "test", provider: "no-such-search-provider" });
assert.equal(result.success, false);
});
test("tool registration + scope are unaffected by the dynamic enum", () => {
assert.equal(webSearchTool.name, "omniroute_web_search");
assert.equal(webSearchTool.inputSchema, webSearchInput);
assert.ok(webSearchTool.scopes.includes("execute:search"), "web search tool scope must be retained");
});