1124 lines
33 KiB
TypeScript
1124 lines
33 KiB
TypeScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { mkdtempSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
|
|
process.env.DATA_DIR = mkdtempSync(join(tmpdir(), "omniroute-search-"));
|
|
|
|
const { handleSearch } = await import("../../open-sse/handlers/search.ts");
|
|
|
|
test("handleSearch builds Serper web requests and normalizes organic results", async () => {
|
|
const originalFetch = globalThis.fetch;
|
|
let captured;
|
|
|
|
globalThis.fetch = async (url, init = {}) => {
|
|
captured = {
|
|
url: String(url),
|
|
headers: init.headers,
|
|
body: JSON.parse(String(init.body || "{}")),
|
|
};
|
|
|
|
return new Response(
|
|
JSON.stringify({
|
|
organic: [
|
|
{
|
|
title: "Latest AI",
|
|
link: "https://www.example.com/news?id=1",
|
|
snippet: "Top story",
|
|
date: "2026-04-05",
|
|
},
|
|
{
|
|
title: "Second",
|
|
link: "https://docs.example.com/page",
|
|
description: "Fallback description",
|
|
},
|
|
],
|
|
searchParameters: { totalResults: 20 },
|
|
}),
|
|
{ status: 200, headers: { "content-type": "application/json" } }
|
|
);
|
|
};
|
|
|
|
try {
|
|
const result = await handleSearch({
|
|
query: " latest ai ",
|
|
provider: "serper-search",
|
|
maxResults: 3,
|
|
searchType: "web",
|
|
country: "US",
|
|
language: "en",
|
|
credentials: { apiKey: "serper-key" },
|
|
log: null,
|
|
});
|
|
|
|
assert.equal(result.success, true);
|
|
assert.equal(captured.url, "https://google.serper.dev/search");
|
|
assert.equal(captured.headers["X-API-Key"], "serper-key");
|
|
assert.deepEqual(captured.body, {
|
|
q: "latest ai",
|
|
num: 3,
|
|
gl: "us",
|
|
hl: "en",
|
|
});
|
|
assert.equal(result.data.provider, "serper-search");
|
|
assert.equal(result.data.query, "latest ai");
|
|
assert.equal(result.data.results[0].display_url, "example.com/news");
|
|
assert.equal(result.data.results[0].citation.provider, "serper-search");
|
|
assert.equal(result.data.metrics.total_results_available, 20);
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
test("handleSearch builds Brave news requests and normalizes favicon metadata", async () => {
|
|
const originalFetch = globalThis.fetch;
|
|
let capturedUrl;
|
|
let capturedHeaders;
|
|
|
|
globalThis.fetch = async (url, init = {}) => {
|
|
capturedUrl = String(url);
|
|
capturedHeaders = init.headers;
|
|
|
|
return new Response(
|
|
JSON.stringify({
|
|
results: [
|
|
{
|
|
title: "Brave item",
|
|
url: "https://news.example.com/story",
|
|
description: "Breaking news",
|
|
age: "2026-04-05",
|
|
meta_url: { favicon: "https://news.example.com/favicon.ico" },
|
|
},
|
|
],
|
|
}),
|
|
{ status: 200, headers: { "content-type": "application/json" } }
|
|
);
|
|
};
|
|
|
|
try {
|
|
const result = await handleSearch({
|
|
query: "market update",
|
|
provider: "brave-search",
|
|
maxResults: 1,
|
|
searchType: "news",
|
|
country: "BR",
|
|
language: "pt",
|
|
credentials: { apiKey: "brave-key" },
|
|
log: null,
|
|
});
|
|
|
|
const url = new URL(capturedUrl);
|
|
assert.equal(url.origin + url.pathname, "https://api.search.brave.com/res/v1/news/search");
|
|
assert.equal(url.searchParams.get("q"), "market update");
|
|
assert.equal(url.searchParams.get("count"), "1");
|
|
assert.equal(url.searchParams.get("country"), "BR");
|
|
assert.equal(url.searchParams.get("search_lang"), "pt");
|
|
assert.equal(capturedHeaders["X-Subscription-Token"], "brave-key");
|
|
assert.equal(result.success, true);
|
|
assert.equal(result.data.results[0].favicon_url, "https://news.example.com/favicon.ico");
|
|
assert.equal(result.data.results[0].published_at, "2026-04-05");
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
test("handleSearch builds Perplexity requests, forwards filters, and enforces maxResults", async () => {
|
|
const originalFetch = globalThis.fetch;
|
|
let captured;
|
|
|
|
globalThis.fetch = async (url, init = {}) => {
|
|
captured = {
|
|
url: String(url),
|
|
headers: init.headers,
|
|
body: JSON.parse(String(init.body || "{}")),
|
|
};
|
|
|
|
return new Response(
|
|
JSON.stringify({
|
|
results: [
|
|
{ title: "One", url: "https://one.example.com", snippet: "First", date: "2026-04-05" },
|
|
{ title: "Two", url: "https://two.example.com", snippet: "Second" },
|
|
],
|
|
}),
|
|
{ status: 200, headers: { "content-type": "application/json" } }
|
|
);
|
|
};
|
|
|
|
try {
|
|
const result = await handleSearch({
|
|
query: "ai agents",
|
|
provider: "perplexity-search",
|
|
maxResults: 1,
|
|
searchType: "web",
|
|
country: "US",
|
|
language: "en",
|
|
domainFilter: ["example.com", "-spam.com"],
|
|
credentials: { apiKey: "perplexity-key" },
|
|
log: null,
|
|
});
|
|
|
|
assert.equal(captured.url, "https://api.perplexity.ai/search");
|
|
assert.equal(captured.headers.Authorization, "Bearer perplexity-key");
|
|
assert.deepEqual(captured.body, {
|
|
query: "ai agents",
|
|
max_results: 1,
|
|
country: "US",
|
|
search_language_filter: ["en"],
|
|
search_domain_filter: ["example.com", "-spam.com"],
|
|
});
|
|
assert.equal(result.success, true);
|
|
assert.equal(result.data.results.length, 1);
|
|
assert.equal(result.data.usage.queries_used, 1);
|
|
assert.equal(result.data.usage.search_cost_usd, 0.005);
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
test("handleSearch builds Exa requests with include/exclude domains and preserves rich result fields", async () => {
|
|
const originalFetch = globalThis.fetch;
|
|
let captured;
|
|
|
|
globalThis.fetch = async (_url, init = {}) => {
|
|
captured = JSON.parse(String(init.body || "{}"));
|
|
|
|
return new Response(
|
|
JSON.stringify({
|
|
results: [
|
|
{
|
|
title: "Exa result",
|
|
url: "https://exa.example.com/page",
|
|
highlights: ["Highlighted snippet"],
|
|
score: 1.2,
|
|
publishedDate: "2026-04-04",
|
|
favicon: "https://exa.example.com/favicon.ico",
|
|
author: "Author",
|
|
image: "https://exa.example.com/image.png",
|
|
text: "Full document text",
|
|
},
|
|
],
|
|
}),
|
|
{ status: 200, headers: { "content-type": "application/json" } }
|
|
);
|
|
};
|
|
|
|
try {
|
|
const result = await handleSearch({
|
|
query: "agentic workflows",
|
|
provider: "exa-search",
|
|
maxResults: 5,
|
|
searchType: "news",
|
|
domainFilter: ["allowed.com", "-blocked.com"],
|
|
credentials: { apiKey: "exa-key" },
|
|
log: null,
|
|
});
|
|
|
|
assert.deepEqual(captured, {
|
|
query: "agentic workflows",
|
|
numResults: 5,
|
|
type: "auto",
|
|
text: true,
|
|
highlights: true,
|
|
includeDomains: ["allowed.com"],
|
|
excludeDomains: ["blocked.com"],
|
|
category: "news",
|
|
});
|
|
assert.equal(result.success, true);
|
|
assert.equal(result.data.results[0].snippet, "Highlighted snippet");
|
|
assert.equal(result.data.results[0].score, 1);
|
|
assert.equal(result.data.results[0].content.text, "Full document text");
|
|
assert.equal(result.data.results[0].metadata.author, "Author");
|
|
assert.equal(result.data.results[0].metadata.image_url, "https://exa.example.com/image.png");
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
test("handleSearch builds Tavily requests with topic and raw content normalization", async () => {
|
|
const originalFetch = globalThis.fetch;
|
|
let captured;
|
|
|
|
globalThis.fetch = async (_url, init = {}) => {
|
|
captured = JSON.parse(String(init.body || "{}"));
|
|
|
|
return new Response(
|
|
JSON.stringify({
|
|
results: [
|
|
{
|
|
title: "Tavily result",
|
|
url: "https://tavily.example.com/page",
|
|
content: "Summary",
|
|
score: 0.7,
|
|
published_date: "2026-04-03",
|
|
raw_content: "Long form content",
|
|
},
|
|
],
|
|
}),
|
|
{ status: 200, headers: { "content-type": "application/json" } }
|
|
);
|
|
};
|
|
|
|
try {
|
|
const result = await handleSearch({
|
|
query: "policy changes",
|
|
provider: "tavily-search",
|
|
maxResults: 2,
|
|
searchType: "news",
|
|
country: "us",
|
|
domainFilter: ["good.com", "-bad.com"],
|
|
credentials: { apiKey: "tavily-key" },
|
|
log: null,
|
|
});
|
|
|
|
assert.deepEqual(captured, {
|
|
query: "policy changes",
|
|
max_results: 2,
|
|
topic: "news",
|
|
include_domains: ["good.com"],
|
|
exclude_domains: ["bad.com"],
|
|
country: "us",
|
|
});
|
|
assert.equal(result.success, true);
|
|
assert.equal(result.data.results[0].snippet, "Summary");
|
|
assert.equal(result.data.results[0].content.text, "Long form content");
|
|
assert.equal(result.data.results[0].citation.rank, 1);
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
test("handleSearch builds Google PSE requests with key/cx query params and normalizes items", async () => {
|
|
const originalFetch = globalThis.fetch;
|
|
let capturedUrl;
|
|
|
|
globalThis.fetch = async (url) => {
|
|
capturedUrl = String(url);
|
|
|
|
return new Response(
|
|
JSON.stringify({
|
|
items: [
|
|
{
|
|
title: "Programmable result",
|
|
link: "https://google.example.com/page",
|
|
snippet: "Google snippet",
|
|
pagemap: {
|
|
cse_image: [{ src: "https://google.example.com/image.png" }],
|
|
},
|
|
},
|
|
],
|
|
searchInformation: { totalResults: "42" },
|
|
}),
|
|
{ status: 200, headers: { "content-type": "application/json" } }
|
|
);
|
|
};
|
|
|
|
try {
|
|
const result = await handleSearch({
|
|
query: "google custom search",
|
|
provider: "google-pse-search",
|
|
maxResults: 3,
|
|
searchType: "web",
|
|
country: "US",
|
|
language: "en",
|
|
credentials: { apiKey: "google-key", providerSpecificData: { cx: "engine-id" } },
|
|
log: null,
|
|
});
|
|
|
|
const url = new URL(capturedUrl);
|
|
assert.equal(url.origin + url.pathname, "https://www.googleapis.com/customsearch/v1");
|
|
assert.equal(url.searchParams.get("key"), "google-key");
|
|
assert.equal(url.searchParams.get("cx"), "engine-id");
|
|
assert.equal(url.searchParams.get("q"), "google custom search");
|
|
assert.equal(url.searchParams.get("num"), "3");
|
|
assert.equal(result.success, true);
|
|
assert.equal(result.data.results[0].metadata.image_url, "https://google.example.com/image.png");
|
|
assert.equal(result.data.metrics.total_results_available, 42);
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
test("handleSearch builds Linkup requests and normalizes searchResults payload", async () => {
|
|
const originalFetch = globalThis.fetch;
|
|
let captured;
|
|
|
|
globalThis.fetch = async (url, init = {}) => {
|
|
captured = {
|
|
url: String(url),
|
|
headers: init.headers,
|
|
body: JSON.parse(String(init.body || "{}")),
|
|
};
|
|
|
|
return new Response(
|
|
JSON.stringify({
|
|
results: [
|
|
{
|
|
name: "Linkup result",
|
|
url: "https://linkup.example.com/page",
|
|
content: "Retrieved content",
|
|
type: "web",
|
|
},
|
|
],
|
|
}),
|
|
{ status: 200, headers: { "content-type": "application/json" } }
|
|
);
|
|
};
|
|
|
|
try {
|
|
const result = await handleSearch({
|
|
query: "linkup test",
|
|
provider: "linkup-search",
|
|
maxResults: 4,
|
|
searchType: "web",
|
|
domainFilter: ["example.com", "-blocked.com"],
|
|
credentials: { apiKey: "linkup-key" },
|
|
log: null,
|
|
});
|
|
|
|
assert.equal(captured.url, "https://api.linkup.so/v1/search");
|
|
assert.equal(captured.headers.Authorization, "Bearer linkup-key");
|
|
assert.deepEqual(captured.body, {
|
|
q: "linkup test",
|
|
depth: "standard",
|
|
outputType: "searchResults",
|
|
maxResults: 4,
|
|
includeDomains: ["example.com"],
|
|
excludeDomains: ["blocked.com"],
|
|
});
|
|
assert.equal(result.success, true);
|
|
assert.equal(result.data.results[0].title, "Linkup result");
|
|
assert.equal(result.data.results[0].content.text, "Retrieved content");
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
test("handleSearch builds SearchAPI requests and normalizes organic results", async () => {
|
|
const originalFetch = globalThis.fetch;
|
|
let capturedUrl;
|
|
|
|
globalThis.fetch = async (url) => {
|
|
capturedUrl = String(url);
|
|
|
|
return new Response(
|
|
JSON.stringify({
|
|
search_information: { total_results: 18 },
|
|
organic_results: [
|
|
{
|
|
title: "SearchAPI result",
|
|
link: "https://searchapi.example.com/page",
|
|
snippet: "SearchAPI snippet",
|
|
date: "2026-04-06",
|
|
source: "SearchAPI Source",
|
|
thumbnail: "https://searchapi.example.com/thumb.png",
|
|
},
|
|
],
|
|
}),
|
|
{ status: 200, headers: { "content-type": "application/json" } }
|
|
);
|
|
};
|
|
|
|
try {
|
|
const result = await handleSearch({
|
|
query: "searchapi query",
|
|
provider: "searchapi-search",
|
|
maxResults: 2,
|
|
searchType: "news",
|
|
country: "US",
|
|
language: "en",
|
|
credentials: { apiKey: "searchapi-key" },
|
|
log: null,
|
|
});
|
|
|
|
const url = new URL(capturedUrl);
|
|
assert.equal(url.origin + url.pathname, "https://www.searchapi.io/api/v1/search");
|
|
assert.equal(url.searchParams.get("engine"), "google_news");
|
|
assert.equal(url.searchParams.get("api_key"), "searchapi-key");
|
|
assert.equal(url.searchParams.get("gl"), "us");
|
|
assert.equal(url.searchParams.get("hl"), "en");
|
|
assert.equal(result.success, true);
|
|
assert.equal(result.data.results[0].metadata.author, "SearchAPI Source");
|
|
assert.equal(
|
|
result.data.results[0].metadata.image_url,
|
|
"https://searchapi.example.com/thumb.png"
|
|
);
|
|
assert.equal(result.data.metrics.total_results_available, 18);
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
test("handleSearch builds You.com requests with livecrawl and normalizes unified response sections", async () => {
|
|
const originalFetch = globalThis.fetch;
|
|
let capturedUrl;
|
|
let capturedHeaders;
|
|
|
|
globalThis.fetch = async (url, init = {}) => {
|
|
capturedUrl = String(url);
|
|
capturedHeaders = init.headers;
|
|
|
|
return new Response(
|
|
JSON.stringify({
|
|
results: {
|
|
web: [
|
|
{
|
|
title: "You.com result",
|
|
url: "https://you.example.com/page",
|
|
description: "Fallback description",
|
|
snippets: ["Primary snippet"],
|
|
page_age: "2026-04-23T10:00:00Z",
|
|
favicon_url: "https://you.example.com/favicon.ico",
|
|
thumbnail_url: "https://you.example.com/thumb.png",
|
|
markdown: "# Full page markdown",
|
|
},
|
|
],
|
|
news: [],
|
|
},
|
|
metadata: { search_uuid: "uuid-1", latency: 0.5 },
|
|
}),
|
|
{ status: 200, headers: { "content-type": "application/json" } }
|
|
);
|
|
};
|
|
|
|
try {
|
|
const result = await handleSearch({
|
|
query: "you search",
|
|
provider: "youcom-search",
|
|
maxResults: 2,
|
|
searchType: "web",
|
|
country: "US",
|
|
language: "en",
|
|
timeRange: "week",
|
|
offset: 4,
|
|
domainFilter: ["docs.you.com"],
|
|
contentOptions: { full_page: true, format: "markdown" },
|
|
credentials: { apiKey: "you-key" },
|
|
log: null,
|
|
});
|
|
|
|
const url = new URL(capturedUrl);
|
|
assert.equal(url.origin + url.pathname, "https://ydc-index.io/v1/search");
|
|
assert.equal(url.searchParams.get("query"), "you search");
|
|
assert.equal(url.searchParams.get("count"), "2");
|
|
assert.equal(url.searchParams.get("freshness"), "week");
|
|
assert.equal(url.searchParams.get("offset"), "2");
|
|
assert.equal(url.searchParams.get("country"), "US");
|
|
assert.equal(url.searchParams.get("language"), "en");
|
|
assert.equal(url.searchParams.get("include_domains"), "docs.you.com");
|
|
assert.equal(url.searchParams.get("livecrawl"), "web");
|
|
assert.equal(url.searchParams.get("livecrawl_formats"), "markdown");
|
|
assert.equal(capturedHeaders["X-API-Key"], "you-key");
|
|
assert.equal(result.success, true);
|
|
assert.equal(result.data.provider, "youcom-search");
|
|
assert.equal(result.data.results[0].snippet, "Primary snippet");
|
|
assert.equal(result.data.results[0].content?.format, "markdown");
|
|
assert.equal(result.data.results[0].content?.text, "# Full page markdown");
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
test("handleSearch builds SearXNG requests with custom baseUrl and no apiKey", async () => {
|
|
const originalFetch = globalThis.fetch;
|
|
let capturedUrl;
|
|
|
|
globalThis.fetch = async (url) => {
|
|
capturedUrl = String(url);
|
|
|
|
return new Response(
|
|
JSON.stringify({
|
|
results: [
|
|
{
|
|
title: "SearXNG result",
|
|
url: "https://searx.example.com/page",
|
|
content: "Metasearch snippet",
|
|
publishedDate: "2026-04-07",
|
|
engines: ["duckduckgo", "brave"],
|
|
},
|
|
],
|
|
}),
|
|
{ status: 200, headers: { "content-type": "application/json" } }
|
|
);
|
|
};
|
|
|
|
try {
|
|
const result = await handleSearch({
|
|
query: "privacy search",
|
|
provider: "searxng-search",
|
|
maxResults: 5,
|
|
searchType: "news",
|
|
language: "pt-BR",
|
|
credentials: {
|
|
providerSpecificData: { baseUrl: "http://localhost:9999" },
|
|
},
|
|
log: null,
|
|
});
|
|
|
|
const url = new URL(capturedUrl);
|
|
assert.equal(url.origin + url.pathname, "http://localhost:9999/search");
|
|
assert.equal(url.searchParams.get("format"), "json");
|
|
assert.equal(url.searchParams.get("categories"), "news");
|
|
assert.equal(url.searchParams.get("language"), "pt-BR");
|
|
assert.equal(result.success, true);
|
|
assert.equal(result.data.provider, "searxng-search");
|
|
assert.equal(result.data.results[0].metadata.source_type, "duckduckgo, brave");
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
test("handleSearch rejects queries with invalid control characters", async () => {
|
|
const result = await handleSearch({
|
|
query: "bad\u0000query",
|
|
provider: "serper-search",
|
|
maxResults: 5,
|
|
searchType: "web",
|
|
credentials: { apiKey: "x" },
|
|
log: null,
|
|
});
|
|
|
|
assert.equal(result.success, false);
|
|
assert.equal(result.status, 400);
|
|
assert.equal(result.error, "Query contains invalid control characters");
|
|
});
|
|
|
|
test("handleSearch rejects queries that become empty after normalization", async () => {
|
|
const result = await handleSearch({
|
|
query: " \n\t ",
|
|
provider: "serper-search",
|
|
maxResults: 5,
|
|
searchType: "web",
|
|
credentials: { apiKey: "x" },
|
|
log: null,
|
|
});
|
|
|
|
assert.equal(result.success, false);
|
|
assert.equal(result.status, 400);
|
|
assert.equal(result.error, "Query is empty after normalization");
|
|
});
|
|
|
|
test("handleSearch rejects unknown providers", async () => {
|
|
const result = await handleSearch({
|
|
query: "hello",
|
|
provider: "unknown-search",
|
|
maxResults: 5,
|
|
searchType: "web",
|
|
credentials: { apiKey: "x" },
|
|
log: null,
|
|
});
|
|
|
|
assert.equal(result.success, false);
|
|
assert.equal(result.status, 400);
|
|
assert.equal(result.error, "Unknown search provider: unknown-search");
|
|
});
|
|
|
|
test("handleSearch requires credentials for the configured provider", async () => {
|
|
const result = await handleSearch({
|
|
query: "hello",
|
|
provider: "serper-search",
|
|
maxResults: 5,
|
|
searchType: "web",
|
|
credentials: {},
|
|
log: null,
|
|
});
|
|
|
|
assert.equal(result.success, false);
|
|
assert.equal(result.status, 401);
|
|
assert.equal(result.error, "No credentials for search provider: serper-search");
|
|
});
|
|
|
|
test("handleSearch fails over to the alternate provider on retriable upstream errors", async () => {
|
|
const originalFetch = globalThis.fetch;
|
|
const calls = [];
|
|
|
|
globalThis.fetch = async (url) => {
|
|
calls.push(String(url));
|
|
|
|
if (String(url).startsWith("https://google.serper.dev/")) {
|
|
return new Response("upstream broken", { status: 500 });
|
|
}
|
|
|
|
return new Response(
|
|
JSON.stringify({
|
|
web: {
|
|
results: [
|
|
{
|
|
title: "Fallback result",
|
|
url: "https://fallback.example.com",
|
|
description: "Recovered",
|
|
},
|
|
],
|
|
totalCount: 1,
|
|
},
|
|
}),
|
|
{ status: 200, headers: { "content-type": "application/json" } }
|
|
);
|
|
};
|
|
|
|
try {
|
|
const result = await handleSearch({
|
|
query: "fallback test",
|
|
provider: "serper-search",
|
|
maxResults: 5,
|
|
searchType: "web",
|
|
credentials: { apiKey: "serper-key" },
|
|
alternateProvider: "brave-search",
|
|
alternateCredentials: { apiKey: "brave-key" },
|
|
log: null,
|
|
});
|
|
|
|
assert.equal(result.success, true);
|
|
assert.deepEqual(calls, [
|
|
"https://google.serper.dev/search",
|
|
"https://api.search.brave.com/res/v1/web/search?q=fallback+test&count=5",
|
|
]);
|
|
assert.equal(result.data.provider, "brave-search");
|
|
assert.equal(result.data.results[0].title, "Fallback result");
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
test("handleSearch does not fail over on non-retriable upstream errors", async () => {
|
|
const originalFetch = globalThis.fetch;
|
|
let callCount = 0;
|
|
|
|
globalThis.fetch = async () => {
|
|
callCount += 1;
|
|
return new Response("unauthorized", { status: 401 });
|
|
};
|
|
|
|
try {
|
|
const result = await handleSearch({
|
|
query: "no failover",
|
|
provider: "serper-search",
|
|
maxResults: 5,
|
|
searchType: "web",
|
|
credentials: { apiKey: "serper-key" },
|
|
alternateProvider: "brave-search",
|
|
alternateCredentials: { apiKey: "brave-key" },
|
|
log: null,
|
|
});
|
|
|
|
assert.equal(result.success, false);
|
|
assert.equal(result.status, 401);
|
|
assert.equal(result.error, "Search provider serper-search returned 401");
|
|
assert.equal(callCount, 1);
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
// ─── Ollama Search Tests ──────────────────────────────────
|
|
|
|
test("handleSearch builds Ollama POST request with bearer auth", async () => {
|
|
const originalFetch = globalThis.fetch;
|
|
let captured;
|
|
|
|
globalThis.fetch = async (url, init = {}) => {
|
|
captured = {
|
|
url: String(url),
|
|
headers: init.headers,
|
|
body: JSON.parse(String(init.body || "{}")),
|
|
};
|
|
|
|
return new Response(
|
|
JSON.stringify({
|
|
results: [
|
|
{ title: "Ollama result", url: "https://ollama.example.com", content: "Test content" },
|
|
],
|
|
}),
|
|
{ status: 200, headers: { "content-type": "application/json" } }
|
|
);
|
|
};
|
|
|
|
try {
|
|
const result = await handleSearch({
|
|
query: "latest AI news",
|
|
provider: "ollama-search",
|
|
maxResults: 5,
|
|
searchType: "web",
|
|
credentials: { apiKey: "ollama-token-123" },
|
|
log: null,
|
|
});
|
|
|
|
assert.equal(captured.url, "https://ollama.com/api/web_search");
|
|
assert.equal(captured.headers["Content-Type"], "application/json");
|
|
assert.equal(captured.headers.Authorization, "Bearer ollama-token-123");
|
|
assert.deepEqual(captured.body, { query: "latest AI news", max_results: 5 });
|
|
assert.equal(result.success, true);
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
test("handleSearch normalizes Ollama response fields and full_text content", async () => {
|
|
const originalFetch = globalThis.fetch;
|
|
|
|
globalThis.fetch = async () => {
|
|
return new Response(
|
|
JSON.stringify({
|
|
results: [
|
|
{
|
|
title: "Ollama Web Search",
|
|
url: "https://ollama.com/blog/web-search",
|
|
content: "Ollama now supports native web search capabilities",
|
|
},
|
|
{
|
|
title: "Getting Started",
|
|
url: "https://ollama.com/docs",
|
|
content: "Learn how to use Ollama for LLM inference",
|
|
},
|
|
],
|
|
}),
|
|
{ status: 200, headers: { "content-type": "application/json" } }
|
|
);
|
|
};
|
|
|
|
try {
|
|
const result = await handleSearch({
|
|
query: "ollama query",
|
|
provider: "ollama-search",
|
|
maxResults: 5,
|
|
searchType: "web",
|
|
credentials: { apiKey: "ollama-key" },
|
|
log: null,
|
|
});
|
|
|
|
assert.equal(result.success, true);
|
|
assert.equal(result.data.results.length, 2);
|
|
assert.equal(result.data.metrics.total_results_available, 2);
|
|
|
|
assert.equal(result.data.results[0].title, "Ollama Web Search");
|
|
assert.equal(result.data.results[0].url, "https://ollama.com/blog/web-search");
|
|
assert.equal(
|
|
result.data.results[0].snippet,
|
|
"Ollama now supports native web search capabilities"
|
|
);
|
|
assert.equal(
|
|
result.data.results[0].content.text,
|
|
"Ollama now supports native web search capabilities"
|
|
);
|
|
assert.equal(result.data.results[0].content.format, "text");
|
|
assert.equal(result.data.results[0].position, 1);
|
|
assert.equal(result.data.results[0].citation.provider, "ollama-search");
|
|
assert.equal(result.data.results[0].citation.rank, 1);
|
|
|
|
assert.equal(result.data.results[1].title, "Getting Started");
|
|
assert.equal(result.data.results[1].position, 2);
|
|
assert.equal(result.data.results[1].citation.rank, 2);
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
test("handleSearch handles empty Ollama results array", async () => {
|
|
const originalFetch = globalThis.fetch;
|
|
|
|
globalThis.fetch = async () => {
|
|
return new Response(JSON.stringify({ results: [] }), {
|
|
status: 200,
|
|
headers: { "content-type": "application/json" },
|
|
});
|
|
};
|
|
|
|
try {
|
|
const result = await handleSearch({
|
|
query: "empty query",
|
|
provider: "ollama-search",
|
|
maxResults: 5,
|
|
searchType: "web",
|
|
credentials: { apiKey: "ollama-key" },
|
|
log: null,
|
|
});
|
|
|
|
assert.equal(result.success, true);
|
|
assert.equal(result.data.results.length, 0);
|
|
assert.equal(result.data.metrics.total_results_available, 0);
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
test("handleSearch handles Ollama response with missing results field", async () => {
|
|
const originalFetch = globalThis.fetch;
|
|
|
|
globalThis.fetch = async () => {
|
|
return new Response(JSON.stringify({ unrelated: "data" }), {
|
|
status: 200,
|
|
headers: { "content-type": "application/json" },
|
|
});
|
|
};
|
|
|
|
try {
|
|
const result = await handleSearch({
|
|
query: "some query",
|
|
provider: "ollama-search",
|
|
maxResults: 5,
|
|
searchType: "web",
|
|
credentials: { apiKey: "ollama-key" },
|
|
log: null,
|
|
});
|
|
|
|
assert.equal(result.success, true);
|
|
assert.equal(result.data.results.length, 0);
|
|
assert.equal(result.data.metrics.total_results_available, 0);
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
// ─── Z.AI Coding Plan Search Tests ────────────────────
|
|
|
|
const ZAI_MOCK_RESULTS = [
|
|
{
|
|
title: "Z.AI Coding Plan Search",
|
|
link: "https://docs.z.ai/search",
|
|
content: "Z.AI now supports web search capabilities via Coding Plan",
|
|
publish_date: "2026-04-20T10:00:00Z",
|
|
icon: "https://docs.z.ai/favicon.ico",
|
|
media: "web",
|
|
},
|
|
{
|
|
title: "Getting Started with Z.AI",
|
|
link: "https://docs.z.ai/getting-started",
|
|
content: "Learn how to use Z.AI for search and inference",
|
|
publish_date: "2026-04-18T08:00:00Z",
|
|
icon: "https://docs.z.ai/favicon.ico",
|
|
media: "web",
|
|
},
|
|
];
|
|
|
|
test("handleSearch searches with Z.AI Coding Plan via MCP", async () => {
|
|
const originalFetch = globalThis.fetch;
|
|
const calls: { url: string; init: RequestInit }[] = [];
|
|
let capturedArgs: Record<string, unknown> = {};
|
|
|
|
globalThis.fetch = async (url, init = {}) => {
|
|
calls.push({ url: String(url), init });
|
|
const body = init.body ? JSON.parse(String(init.body)) : {};
|
|
|
|
// MCP initialize handshake
|
|
if (body.method === "initialize") {
|
|
return new Response(
|
|
JSON.stringify({
|
|
jsonrpc: "2.0",
|
|
result: {
|
|
protocolVersion: "2024-11-05",
|
|
capabilities: {},
|
|
serverInfo: { name: "zai-mcp", version: "1.0" },
|
|
},
|
|
id: body.id,
|
|
}),
|
|
{
|
|
status: 200,
|
|
headers: { "content-type": "application/json", "mcp-session-id": "session-abc-123" },
|
|
}
|
|
);
|
|
}
|
|
|
|
// notifications/initialized
|
|
if (body.method === "notifications/initialized") {
|
|
return new Response(null, { status: 202 });
|
|
}
|
|
|
|
// tools/call
|
|
if (body.method === "tools/call") {
|
|
capturedArgs = body.params.arguments;
|
|
return new Response(
|
|
JSON.stringify({
|
|
jsonrpc: "2.0",
|
|
result: {
|
|
content: [{ type: "text", text: JSON.stringify(JSON.stringify(ZAI_MOCK_RESULTS)) }],
|
|
},
|
|
id: body.id,
|
|
}),
|
|
{ status: 200, headers: { "content-type": "application/json" } }
|
|
);
|
|
}
|
|
|
|
// GET (initial probe / transport check)
|
|
return new Response(null, { status: 405 });
|
|
};
|
|
|
|
try {
|
|
const result = await handleSearch({
|
|
query: "zai mcp search",
|
|
provider: "zai-search",
|
|
maxResults: 2,
|
|
searchType: "web",
|
|
credentials: { apiKey: "zai-token-abc" },
|
|
log: null,
|
|
});
|
|
|
|
assert.equal(result.success, true);
|
|
assert.equal(capturedArgs.search_query, "zai mcp search");
|
|
assert.equal(result.data.provider, "zai-search");
|
|
assert.equal(result.data.results.length, 2);
|
|
assert.equal(result.data.results[0].title, "Z.AI Coding Plan Search");
|
|
assert.equal(result.data.results[0].url, "https://docs.z.ai/search");
|
|
assert.equal(
|
|
result.data.results[0].snippet,
|
|
"Z.AI now supports web search capabilities via Coding Plan"
|
|
);
|
|
assert.equal(result.data.results[0].display_url, "docs.z.ai/search");
|
|
assert.equal(result.data.results[0].favicon_url, "https://docs.z.ai/favicon.ico");
|
|
assert.equal(result.data.results[0].citation.provider, "zai-search");
|
|
assert.equal(result.data.results[1].title, "Getting Started with Z.AI");
|
|
assert.equal(result.data.results[1].citation.provider, "zai-search");
|
|
assert.equal(result.data.usage.queries_used, 1);
|
|
assert.equal(result.data.usage.search_cost_usd, 0);
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
test("handleSearch handles Z.AI Coding Plan empty MCP results", async () => {
|
|
const originalFetch = globalThis.fetch;
|
|
|
|
globalThis.fetch = async (url, init = {}) => {
|
|
const body = init.body ? JSON.parse(String(init.body)) : {};
|
|
|
|
if (body.method === "initialize") {
|
|
return new Response(
|
|
JSON.stringify({
|
|
jsonrpc: "2.0",
|
|
result: {
|
|
protocolVersion: "2024-11-05",
|
|
capabilities: {},
|
|
serverInfo: { name: "zai-mcp", version: "1.0" },
|
|
},
|
|
id: body.id,
|
|
}),
|
|
{
|
|
status: 200,
|
|
headers: { "content-type": "application/json", "mcp-session-id": "session-empty" },
|
|
}
|
|
);
|
|
}
|
|
|
|
if (body.method === "notifications/initialized") {
|
|
return new Response(null, { status: 202 });
|
|
}
|
|
|
|
if (body.method === "tools/call") {
|
|
return new Response(
|
|
JSON.stringify({
|
|
jsonrpc: "2.0",
|
|
result: {
|
|
content: [{ type: "text", text: JSON.stringify(JSON.stringify([])) }],
|
|
},
|
|
id: body.id,
|
|
}),
|
|
{ status: 200, headers: { "content-type": "application/json" } }
|
|
);
|
|
}
|
|
|
|
return new Response(null, { status: 405 });
|
|
};
|
|
|
|
try {
|
|
const result = await handleSearch({
|
|
query: "empty zai mcp result",
|
|
provider: "zai-search",
|
|
maxResults: 5,
|
|
searchType: "web",
|
|
credentials: { apiKey: "zai-key" },
|
|
log: null,
|
|
});
|
|
|
|
assert.equal(result.success, true);
|
|
assert.equal(result.data.provider, "zai-search");
|
|
assert.equal(result.data.results.length, 0);
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
test("handleSearch handles Z.AI Coding Plan MCP connection error", async () => {
|
|
const originalFetch = globalThis.fetch;
|
|
|
|
globalThis.fetch = async () => {
|
|
throw new Error("Connection refused");
|
|
};
|
|
|
|
try {
|
|
const result = await handleSearch({
|
|
query: "zai mcp error",
|
|
provider: "zai-search",
|
|
maxResults: 5,
|
|
searchType: "web",
|
|
credentials: { apiKey: "zai-key" },
|
|
log: null,
|
|
});
|
|
|
|
assert.equal(result.success, false);
|
|
assert.equal(result.status, 502);
|
|
assert.ok(result.error.includes("error"));
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
test("handleSearch handles Z.AI Coding Plan non-array MCP result", async () => {
|
|
const originalFetch = globalThis.fetch;
|
|
|
|
globalThis.fetch = async (url, init = {}) => {
|
|
const body = init.body ? JSON.parse(String(init.body)) : {};
|
|
|
|
if (body.method === "initialize") {
|
|
return new Response(
|
|
JSON.stringify({
|
|
jsonrpc: "2.0",
|
|
result: {
|
|
protocolVersion: "2024-11-05",
|
|
capabilities: {},
|
|
serverInfo: { name: "zai-mcp", version: "1.0" },
|
|
},
|
|
id: body.id,
|
|
}),
|
|
{
|
|
status: 200,
|
|
headers: { "content-type": "application/json", "mcp-session-id": "session-nonarray" },
|
|
}
|
|
);
|
|
}
|
|
|
|
if (body.method === "notifications/initialized") {
|
|
return new Response(null, { status: 202 });
|
|
}
|
|
|
|
if (body.method === "tools/call") {
|
|
return new Response(
|
|
JSON.stringify({
|
|
jsonrpc: "2.0",
|
|
result: {
|
|
content: [{ type: "text", text: JSON.stringify(JSON.stringify({ not: "an array" })) }],
|
|
},
|
|
id: body.id,
|
|
}),
|
|
{ status: 200, headers: { "content-type": "application/json" } }
|
|
);
|
|
}
|
|
|
|
return new Response(null, { status: 405 });
|
|
};
|
|
|
|
try {
|
|
const result = await handleSearch({
|
|
query: "zai non-array",
|
|
provider: "zai-search",
|
|
maxResults: 5,
|
|
searchType: "web",
|
|
credentials: { apiKey: "zai-key" },
|
|
log: null,
|
|
});
|
|
|
|
assert.equal(result.success, true);
|
|
assert.equal(result.data.results.length, 0);
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|