mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-26 00:52:18 +03:00
feat(search): add Xquik X search provider (#11370)
Merged into release/v3.8.51 via batch validation: xquik provider suites green on the combined tree (193 node:test assertions incl. your 7 new cases), check:provider-consistency OK (353 canonical providers), static gates green. Well-scoped fallbackOnly X-provider with clean citation building — thanks @kriptoburak!
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
* Integration tests for GET /api/search/providers — extended catalog (F4).
|
||||
*
|
||||
* Tests:
|
||||
* - Returns 20 items total (16 search + 4 fetch providers).
|
||||
* - Returns 22 items total (18 search + 4 fetch providers).
|
||||
* - Each item carries the correct `kind` field.
|
||||
* - Status reflects actual DB credential state:
|
||||
* - "configured" when an active, non-rate-limited connection exists.
|
||||
@@ -48,10 +48,10 @@ const route = await import("../../src/app/api/search/providers/route.ts");
|
||||
// Constants
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// 17 search-kind providers: serper, brave, perplexity, exa, tavily, firecrawl,
|
||||
// 18 search-kind providers: serper, brave, perplexity, exa, tavily, firecrawl,
|
||||
// google-pse, linkup, searchapi, youcom, searxng, ollama, zai, jina-search,
|
||||
// context7 (#11140), duckduckgo-free, x-search (registry open-sse/config/searchRegistry.ts).
|
||||
const EXPECTED_SEARCH_COUNT = 17;
|
||||
// context7 (#11140), duckduckgo-free, x-search, xquik-search.
|
||||
const EXPECTED_SEARCH_COUNT = 18;
|
||||
const EXPECTED_FETCH_COUNT = 4;
|
||||
const EXPECTED_TOTAL = EXPECTED_SEARCH_COUNT + EXPECTED_FETCH_COUNT;
|
||||
|
||||
@@ -138,7 +138,7 @@ test("search-providers-catalog: returns 401 for unauthenticated requests when au
|
||||
assert.ok(!bodyStr.includes(" at /"), "error body must not contain stack trace");
|
||||
});
|
||||
|
||||
test("search-providers-catalog: returns 21 providers (17 search + 4 fetch)", async () => {
|
||||
test("search-providers-catalog: returns 22 providers (18 search + 4 fetch)", async () => {
|
||||
const req = await buildAuthRequest();
|
||||
const res = await route.GET(req);
|
||||
|
||||
@@ -361,6 +361,11 @@ test("search-providers-catalog: search providers have correct fields", async ()
|
||||
assert.ok(xSearch, "x-search must be in search providers");
|
||||
assert.equal(xSearch.kind, "search");
|
||||
assert.deepEqual(xSearch.searchTypes, ["x"]);
|
||||
|
||||
const xquikSearch = searchProviders.find((p: { id: string }) => p.id === "xquik-search");
|
||||
assert.ok(xquikSearch, "xquik-search must be in search providers");
|
||||
assert.equal(xquikSearch.kind, "search");
|
||||
assert.deepEqual(xquikSearch.searchTypes, ["x"]);
|
||||
});
|
||||
|
||||
test("search-providers-catalog: response validates against SearchProviderCatalogResponseSchema", async () => {
|
||||
|
||||
@@ -87,6 +87,11 @@
|
||||
"status": 400,
|
||||
"throws": true
|
||||
},
|
||||
"xquik-search": {
|
||||
"message": "Provider \"xquik-search\" is a search provider and does not support chat completions; use the /v1/search endpoint instead.",
|
||||
"status": 400,
|
||||
"throws": true
|
||||
},
|
||||
"youcom-search": {
|
||||
"message": "Provider \"youcom-search\" is a search provider and does not support chat completions; use the /v1/search endpoint instead.",
|
||||
"status": 400,
|
||||
|
||||
@@ -36,9 +36,10 @@ test("SEARCH_PROVIDERS has all registered providers", () => {
|
||||
assert.ok(SEARCH_PROVIDERS["jina-search"], "jina-search should exist");
|
||||
assert.ok(SEARCH_PROVIDERS["duckduckgo-free"], "duckduckgo-free should exist");
|
||||
assert.ok(SEARCH_PROVIDERS["x-search"], "x-search should exist");
|
||||
// #11140: context7 (library-docs search) is the 17th registered provider
|
||||
assert.ok(SEARCH_PROVIDERS["xquik-search"], "xquik-search should exist");
|
||||
// #11140: context7 provides library-docs search
|
||||
assert.ok(SEARCH_PROVIDERS["context7"], "context7 should exist");
|
||||
assert.equal(Object.keys(SEARCH_PROVIDERS).length, 17);
|
||||
assert.equal(Object.keys(SEARCH_PROVIDERS).length, 18);
|
||||
});
|
||||
|
||||
test("duckduckgo-free config is a no-key, fallback-only provider", () => {
|
||||
@@ -172,11 +173,11 @@ test("zai-search config is correct", () => {
|
||||
|
||||
test("getAllSearchProviders returns flat list", () => {
|
||||
const all = getAllSearchProviders();
|
||||
// #11140: 17 providers with context7 registered
|
||||
assert.equal(all.length, 17);
|
||||
assert.equal(all.length, 18);
|
||||
assert.ok(all.some((p) => p.id === "duckduckgo-free"));
|
||||
assert.ok(all.some((p) => p.id === "jina-search"));
|
||||
assert.ok(all.some((p) => p.id === "x-search"));
|
||||
assert.ok(all.some((p) => p.id === "xquik-search"));
|
||||
assert.ok(all.some((p) => p.id === "serper-search"));
|
||||
assert.ok(all.some((p) => p.id === "brave-search"));
|
||||
assert.ok(all.some((p) => p.id === "perplexity-search"));
|
||||
@@ -420,6 +421,7 @@ test("v1SearchSchema accepts new search providers", async () => {
|
||||
"duckduckgo-free",
|
||||
"firecrawl",
|
||||
"x-search",
|
||||
"xquik-search",
|
||||
] as const;
|
||||
|
||||
for (const provider of providers) {
|
||||
|
||||
@@ -52,7 +52,7 @@ test("v1 search GET lists all search providers", async () => {
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(body.object, "list");
|
||||
assert.equal(body.data.length, 17);
|
||||
assert.equal(body.data.length, 18);
|
||||
assert.deepEqual(ids, [
|
||||
"serper-search",
|
||||
"brave-search",
|
||||
@@ -71,6 +71,7 @@ test("v1 search GET lists all search providers", async () => {
|
||||
"context7",
|
||||
"duckduckgo-free",
|
||||
"x-search",
|
||||
"xquik-search",
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
159
tests/unit/xquik-search-provider.test.ts
Normal file
159
tests/unit/xquik-search-provider.test.ts
Normal file
@@ -0,0 +1,159 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
const {
|
||||
SEARCH_PROVIDERS,
|
||||
getSearchProvider,
|
||||
resolveSearchProvider,
|
||||
selectProvider,
|
||||
supportsSearchType,
|
||||
} = await import("../../open-sse/config/searchRegistry.ts");
|
||||
const { SEARCH_VALIDATOR_CONFIGS } =
|
||||
await import("../../src/lib/providers/validation/searchProviders.ts");
|
||||
const { XQUIK_SEARCH_PROVIDER_ID, buildXquikSearchRequest, extractXquikSearchHits } =
|
||||
await import("../../open-sse/handlers/search/xquikSearch.ts");
|
||||
const { handleSearch } = await import("../../open-sse/handlers/search.ts");
|
||||
const { v1SearchSchema } = await import("../../src/shared/validation/schemas.ts");
|
||||
|
||||
test("xquik-search is an explicit X-only fallback provider", () => {
|
||||
const config = getSearchProvider(XQUIK_SEARCH_PROVIDER_ID);
|
||||
assert.ok(config);
|
||||
assert.equal(config.id, "xquik-search");
|
||||
assert.equal(config.baseUrl, "https://xquik.com/api/v1/x/tweets/search");
|
||||
assert.equal(config.authHeader, "x-api-key");
|
||||
assert.equal(config.fallbackOnly, true);
|
||||
assert.deepEqual(config.searchTypes, ["x"]);
|
||||
assert.equal(supportsSearchType(config, "x"), true);
|
||||
assert.equal(supportsSearchType(config, "web"), false);
|
||||
assert.equal(selectProvider(undefined, "x")?.id, "x-search");
|
||||
});
|
||||
|
||||
test("xquik search aliases resolve without changing the xAI provider", () => {
|
||||
assert.equal(resolveSearchProvider("xquik")?.id, "xquik-search");
|
||||
assert.equal(resolveSearchProvider("xquik_search")?.id, "xquik-search");
|
||||
assert.equal(resolveSearchProvider("x-search")?.id, "x-search");
|
||||
});
|
||||
|
||||
test("buildXquikSearchRequest uses the published REST contract", () => {
|
||||
const config = SEARCH_PROVIDERS["xquik-search"];
|
||||
const { url, init } = buildXquikSearchRequest(config, {
|
||||
query: 'from:openai "agents sdk"',
|
||||
maxResults: 3,
|
||||
token: "xq_test_key",
|
||||
});
|
||||
|
||||
const parsedUrl = new URL(url);
|
||||
assert.equal(parsedUrl.origin, "https://xquik.com");
|
||||
assert.equal(parsedUrl.pathname, "/api/v1/x/tweets/search");
|
||||
assert.equal(parsedUrl.searchParams.get("q"), 'from:openai "agents sdk"');
|
||||
assert.equal(parsedUrl.searchParams.get("queryType"), "Latest");
|
||||
assert.equal(parsedUrl.searchParams.get("limit"), "3");
|
||||
assert.equal(init.method, "GET");
|
||||
assert.deepEqual(init.headers, {
|
||||
Accept: "application/json",
|
||||
"x-api-key": "xq_test_key",
|
||||
});
|
||||
});
|
||||
|
||||
test("extractXquikSearchHits creates canonical X citations from typed tweet rows", () => {
|
||||
const hits = extractXquikSearchHits(
|
||||
{
|
||||
tweets: [
|
||||
{
|
||||
id: "1912345678901234567",
|
||||
text: "Agents SDK update",
|
||||
createdAt: "2026-08-24T07:00:00.000Z",
|
||||
author: { username: "openai", name: "OpenAI" },
|
||||
},
|
||||
{
|
||||
id: "not-a-tweet-id",
|
||||
text: "Invalid rows must not become links",
|
||||
author: { username: "attacker", name: "Attacker" },
|
||||
},
|
||||
],
|
||||
},
|
||||
5
|
||||
);
|
||||
|
||||
assert.deepEqual(hits, [
|
||||
{
|
||||
title: "@openai",
|
||||
url: "https://x.com/openai/status/1912345678901234567",
|
||||
snippet: "Agents SDK update",
|
||||
author: "openai",
|
||||
publishedAt: "2026-08-24T07:00:00.000Z",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test("xquik provider validation sends its API key only in x-api-key", () => {
|
||||
const request = SEARCH_VALIDATOR_CONFIGS["xquik-search"]("xq_test_key");
|
||||
const parsedUrl = new URL(request.url);
|
||||
assert.equal(parsedUrl.pathname, "/api/v1/x/tweets/search");
|
||||
assert.equal(parsedUrl.searchParams.get("q"), "test");
|
||||
assert.equal(parsedUrl.searchParams.get("limit"), "1");
|
||||
assert.deepEqual(request.init.headers, {
|
||||
Accept: "application/json",
|
||||
"x-api-key": "xq_test_key",
|
||||
});
|
||||
});
|
||||
|
||||
test("v1SearchSchema canonicalizes xquik aliases and forces search_type x", () => {
|
||||
for (const provider of ["xquik", "xquik_search", "xquik-search"]) {
|
||||
const parsed = v1SearchSchema.parse({
|
||||
query: "agents sdk",
|
||||
provider,
|
||||
search_type: "web",
|
||||
});
|
||||
assert.equal(parsed.provider, "xquik-search");
|
||||
assert.equal(parsed.search_type, "x");
|
||||
}
|
||||
});
|
||||
|
||||
test("handleSearch maps Xquik tweets into the unified search response", async () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
let capturedUrl = "";
|
||||
let capturedInit: RequestInit | undefined;
|
||||
|
||||
globalThis.fetch = (async (url, init) => {
|
||||
capturedUrl = String(url);
|
||||
capturedInit = init;
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
tweets: [
|
||||
{
|
||||
id: "1912345678901234567",
|
||||
text: "Agents SDK update",
|
||||
createdAt: "2026-08-24T07:00:00.000Z",
|
||||
author: { username: "openai", name: "OpenAI" },
|
||||
},
|
||||
],
|
||||
has_next_page: false,
|
||||
next_cursor: "",
|
||||
}),
|
||||
{ status: 200, headers: { "content-type": "application/json" } }
|
||||
);
|
||||
}) as typeof fetch;
|
||||
|
||||
try {
|
||||
const result = await handleSearch({
|
||||
query: "agents sdk",
|
||||
provider: "xquik-search",
|
||||
maxResults: 5,
|
||||
searchType: "x",
|
||||
credentials: { apiKey: "xq_test_key" },
|
||||
log: null,
|
||||
});
|
||||
|
||||
assert.equal(result.success, true, JSON.stringify(result));
|
||||
assert.match(capturedUrl, /^https:\/\/xquik\.com\/api\/v1\/x\/tweets\/search\?/);
|
||||
assert.equal((capturedInit?.headers as Record<string, string>)["x-api-key"], "xq_test_key");
|
||||
assert.equal(result.data?.provider, "xquik-search");
|
||||
assert.equal(result.data?.results[0].title, "@openai");
|
||||
assert.equal(result.data?.results[0].url, "https://x.com/openai/status/1912345678901234567");
|
||||
assert.equal(result.data?.results[0].snippet, "Agents SDK update");
|
||||
assert.equal(result.data?.results[0].metadata?.source_type, "x");
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user