285 lines
9.9 KiB
TypeScript
285 lines
9.9 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
import { env } from "../src/env.js";
|
|
import { resetBraveRateLimitStateForTests, searchBrave } from "../src/search/brave.js";
|
|
|
|
test("searchBrave authenticates, builds filters, and normalizes web results", async () => {
|
|
const originalFetch = globalThis.fetch;
|
|
const originalApiKey = env.BRAVE_SEARCH_API_KEY;
|
|
const fetchCalls: Array<{ input: RequestInfo | URL; init?: RequestInit }> = [];
|
|
resetBraveRateLimitStateForTests();
|
|
env.BRAVE_SEARCH_API_KEY = "test-brave-key";
|
|
globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
fetchCalls.push({ input, init });
|
|
return new Response(
|
|
JSON.stringify({
|
|
web: {
|
|
results: [
|
|
{
|
|
title: " Brave result ",
|
|
url: "https://docs.example.com/article",
|
|
description: "Main\n snippet",
|
|
extra_snippets: ["Extra snippet one", "Extra snippet two"],
|
|
page_age: "2026-07-18T12:00:00Z",
|
|
profile: { name: "Example Docs" },
|
|
},
|
|
{
|
|
title: "Excluded result",
|
|
url: "https://blocked.example.com/article",
|
|
description: "Should be filtered",
|
|
},
|
|
],
|
|
},
|
|
}),
|
|
{
|
|
status: 200,
|
|
headers: {
|
|
"content-type": "application/json; charset=utf-8",
|
|
"x-request-id": "brave-request-1",
|
|
},
|
|
}
|
|
);
|
|
}) as typeof fetch;
|
|
|
|
try {
|
|
const response = await searchBrave("latest docs", {
|
|
numResults: 5,
|
|
includeDomains: ["https://example.com/path"],
|
|
excludeDomains: ["blocked.example.com"],
|
|
});
|
|
|
|
assert.equal(fetchCalls.length, 1);
|
|
const requestUrl = new URL(String(fetchCalls[0]?.input));
|
|
assert.equal(requestUrl.origin + requestUrl.pathname, "https://api.search.brave.com/res/v1/web/search");
|
|
assert.equal(requestUrl.searchParams.get("q"), "latest docs site:example.com -site:blocked.example.com");
|
|
assert.equal(requestUrl.searchParams.get("count"), "5");
|
|
assert.equal(requestUrl.searchParams.get("safesearch"), "moderate");
|
|
assert.equal(requestUrl.searchParams.get("result_filter"), "web");
|
|
assert.equal(requestUrl.searchParams.get("text_decorations"), "false");
|
|
assert.equal(requestUrl.searchParams.get("extra_snippets"), "true");
|
|
assert.equal((fetchCalls[0]?.init?.headers as Record<string, string>)["X-Subscription-Token"], "test-brave-key");
|
|
|
|
assert.deepEqual(response, {
|
|
query: "latest docs",
|
|
requestId: "brave-request-1",
|
|
results: [
|
|
{
|
|
title: "Brave result",
|
|
url: "https://docs.example.com/article",
|
|
publishedDate: "2026-07-18T12:00:00Z",
|
|
author: "Example Docs",
|
|
summary: "Main snippet",
|
|
text: "Main snippet\n\nExtra snippet one\n\nExtra snippet two",
|
|
highlights: ["Main snippet", "Extra snippet one", "Extra snippet two"],
|
|
},
|
|
],
|
|
});
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
env.BRAVE_SEARCH_API_KEY = originalApiKey;
|
|
}
|
|
});
|
|
|
|
test("searchBrave rejects requests without an API key", async () => {
|
|
const originalApiKey = env.BRAVE_SEARCH_API_KEY;
|
|
resetBraveRateLimitStateForTests();
|
|
env.BRAVE_SEARCH_API_KEY = undefined;
|
|
try {
|
|
await assert.rejects(() => searchBrave("test", { numResults: 1 }), /BRAVE_SEARCH_API_KEY not set/);
|
|
} finally {
|
|
env.BRAVE_SEARCH_API_KEY = originalApiKey;
|
|
}
|
|
});
|
|
|
|
test("searchBrave reports non-JSON responses", async () => {
|
|
const originalFetch = globalThis.fetch;
|
|
const originalApiKey = env.BRAVE_SEARCH_API_KEY;
|
|
resetBraveRateLimitStateForTests();
|
|
env.BRAVE_SEARCH_API_KEY = "test-brave-key";
|
|
globalThis.fetch = (async () =>
|
|
new Response("upstream error", {
|
|
status: 200,
|
|
headers: { "content-type": "text/plain" },
|
|
})) as typeof fetch;
|
|
|
|
try {
|
|
await assert.rejects(
|
|
() => searchBrave("test", { numResults: 1 }),
|
|
/Brave Search API returned text\/plain/
|
|
);
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
env.BRAVE_SEARCH_API_KEY = originalApiKey;
|
|
}
|
|
});
|
|
|
|
test("searchBrave evenly paces concurrent bursts using Brave's shortest policy window", async () => {
|
|
const originalFetch = globalThis.fetch;
|
|
const originalApiKey = env.BRAVE_SEARCH_API_KEY;
|
|
const requestStartedAt: number[] = [];
|
|
resetBraveRateLimitStateForTests();
|
|
env.BRAVE_SEARCH_API_KEY = "test-brave-key";
|
|
globalThis.fetch = (async () => {
|
|
requestStartedAt.push(Date.now());
|
|
return new Response(JSON.stringify({ web: { results: [] } }), {
|
|
status: 200,
|
|
headers: {
|
|
"content-type": "application/json",
|
|
"x-ratelimit-policy": "1;w=1, 2000;w=2678400",
|
|
"x-ratelimit-remaining": "1, 1999",
|
|
"x-ratelimit-reset": "1, 2678400",
|
|
},
|
|
});
|
|
}) as typeof fetch;
|
|
|
|
try {
|
|
await Promise.all([
|
|
searchBrave("burst one", { numResults: 1 }),
|
|
searchBrave("burst two", { numResults: 1 }),
|
|
searchBrave("burst three", { numResults: 1 }),
|
|
]);
|
|
|
|
assert.equal(requestStartedAt.length, 3);
|
|
assert.ok(requestStartedAt[1]! - requestStartedAt[0]! >= 1_000);
|
|
assert.ok(requestStartedAt[2]! - requestStartedAt[1]! >= 1_000);
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
env.BRAVE_SEARCH_API_KEY = originalApiKey;
|
|
resetBraveRateLimitStateForTests();
|
|
}
|
|
});
|
|
|
|
test("searchBrave adapts its pacing to a 50 request-per-second Search plan", async () => {
|
|
const originalFetch = globalThis.fetch;
|
|
const originalApiKey = env.BRAVE_SEARCH_API_KEY;
|
|
const requestStartedAt: number[] = [];
|
|
resetBraveRateLimitStateForTests();
|
|
env.BRAVE_SEARCH_API_KEY = "test-brave-key";
|
|
globalThis.fetch = (async () => {
|
|
requestStartedAt.push(Date.now());
|
|
return new Response(JSON.stringify({ web: { results: [] } }), {
|
|
status: 200,
|
|
headers: {
|
|
"content-type": "application/json",
|
|
"x-ratelimit-policy": "50;w=1, 0;w=2678400",
|
|
"x-ratelimit-remaining": "49, 0",
|
|
"x-ratelimit-reset": "1, 2678400",
|
|
},
|
|
});
|
|
}) as typeof fetch;
|
|
|
|
try {
|
|
await searchBrave("learn upgraded policy", { numResults: 1 });
|
|
await Promise.all(Array.from({ length: 8 }, (_, index) => searchBrave(`fast burst ${index}`, { numResults: 1 })));
|
|
|
|
assert.equal(requestStartedAt.length, 9);
|
|
const burstStartedAt = requestStartedAt.slice(1);
|
|
for (let index = 1; index < burstStartedAt.length; index += 1) {
|
|
assert.ok(burstStartedAt[index]! - burstStartedAt[index - 1]! >= 18);
|
|
}
|
|
assert.ok(burstStartedAt.at(-1)! - burstStartedAt[0]! < 500);
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
env.BRAVE_SEARCH_API_KEY = originalApiKey;
|
|
resetBraveRateLimitStateForTests();
|
|
}
|
|
});
|
|
|
|
test("searchBrave retries 429 responses after the burst window resets", async () => {
|
|
const originalFetch = globalThis.fetch;
|
|
const originalApiKey = env.BRAVE_SEARCH_API_KEY;
|
|
let fetchCount = 0;
|
|
resetBraveRateLimitStateForTests();
|
|
env.BRAVE_SEARCH_API_KEY = "test-brave-key";
|
|
globalThis.fetch = (async () => {
|
|
fetchCount += 1;
|
|
const rateLimitHeaders = {
|
|
"content-type": "application/json",
|
|
"x-ratelimit-policy": "1;w=1, 2000;w=2678400",
|
|
"x-ratelimit-remaining": fetchCount === 1 ? "0, 1999" : "1, 1998",
|
|
"x-ratelimit-reset": "1, 2678400",
|
|
};
|
|
if (fetchCount === 1) {
|
|
return new Response(JSON.stringify({ error: { detail: "Rate limit exceeded" } }), {
|
|
status: 429,
|
|
headers: rateLimitHeaders,
|
|
});
|
|
}
|
|
return new Response(JSON.stringify({ web: { results: [] } }), { status: 200, headers: rateLimitHeaders });
|
|
}) as typeof fetch;
|
|
|
|
try {
|
|
const startedAt = Date.now();
|
|
await searchBrave("retry burst", { numResults: 1 });
|
|
assert.equal(fetchCount, 2);
|
|
assert.ok(Date.now() - startedAt >= 1_000);
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
env.BRAVE_SEARCH_API_KEY = originalApiKey;
|
|
resetBraveRateLimitStateForTests();
|
|
}
|
|
});
|
|
|
|
test("searchBrave does not wait for exhausted long-term quotas", async () => {
|
|
const originalFetch = globalThis.fetch;
|
|
const originalApiKey = env.BRAVE_SEARCH_API_KEY;
|
|
resetBraveRateLimitStateForTests();
|
|
env.BRAVE_SEARCH_API_KEY = "test-brave-key";
|
|
globalThis.fetch = (async () =>
|
|
new Response(JSON.stringify({ error: { detail: "Quota exceeded" } }), {
|
|
status: 429,
|
|
headers: {
|
|
"content-type": "application/json",
|
|
"x-ratelimit-policy": "1;w=1, 2000;w=2678400",
|
|
"x-ratelimit-remaining": "0, 0",
|
|
"x-ratelimit-reset": "1, 100000",
|
|
},
|
|
})) as typeof fetch;
|
|
|
|
try {
|
|
const startedAt = Date.now();
|
|
await assert.rejects(
|
|
() => searchBrave("quota exhausted", { numResults: 1 }),
|
|
/rate limit quota is exhausted beyond the retry window/
|
|
);
|
|
assert.ok(Date.now() - startedAt < 1_000);
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
env.BRAVE_SEARCH_API_KEY = originalApiKey;
|
|
resetBraveRateLimitStateForTests();
|
|
}
|
|
});
|
|
|
|
test("searchBrave blocks locally after a successful request exhausts the long-term quota", async () => {
|
|
const originalFetch = globalThis.fetch;
|
|
const originalApiKey = env.BRAVE_SEARCH_API_KEY;
|
|
let fetchCount = 0;
|
|
resetBraveRateLimitStateForTests();
|
|
env.BRAVE_SEARCH_API_KEY = "test-brave-key";
|
|
globalThis.fetch = (async () => {
|
|
fetchCount += 1;
|
|
return new Response(JSON.stringify({ web: { results: [] } }), {
|
|
status: 200,
|
|
headers: {
|
|
"content-type": "application/json",
|
|
"x-ratelimit-policy": "1;w=1, 2000;w=2678400",
|
|
"x-ratelimit-remaining": "0, 0",
|
|
"x-ratelimit-reset": "1, 100000",
|
|
},
|
|
});
|
|
}) as typeof fetch;
|
|
|
|
try {
|
|
await searchBrave("last allowed query", { numResults: 1 });
|
|
await assert.rejects(
|
|
() => searchBrave("over quota query", { numResults: 1 }),
|
|
/long-term quota is exhausted/
|
|
);
|
|
assert.equal(fetchCount, 1);
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
env.BRAVE_SEARCH_API_KEY = originalApiKey;
|
|
resetBraveRateLimitStateForTests();
|
|
}
|
|
});
|