feat(coding-agent/web): expanded web search and fetching providers

- Expanded web search and fetching providers with robust parsing, authentication storage integration, and response validation.
- Added support for new configurations including SearXNG safesearch, Cloudflare AI Gateway endpoints, and dynamic Firecrawl base URLs.
- Implemented comprehensive test suites covering error handling, content filtering, and provider-specific response behaviors.
This commit is contained in:
can1357
2026-08-10 11:06:22 +02:00
parent 45e12e5bb7
commit baf8a1df7e
31 changed files with 1660 additions and 224 deletions
@@ -84,10 +84,16 @@ describe("TinyFish web search provider", () => {
});
expect(captured).toHaveLength(1);
expect(captured[0].searchParams.get("query")).toBe(
'"error handling" rust site:github.com -site:gitlab.com filetype:pdf',
);
expectTinyFishParams(captured[0], ["query", "num_results", "page"]);
expect(captured[0].searchParams.get("query")).toBe('"error handling" rust filetype:pdf');
expect(captured[0].searchParams.get("include_domains")).toBe("github.com");
expect(captured[0].searchParams.get("exclude_domains")).toBe("gitlab.com");
expectTinyFishParams(captured[0], [
"query",
"num_results",
"page",
"include_domains",
"exclude_domains",
]);
});
it("sends directive-free queries verbatim", async () => {
@@ -232,6 +238,43 @@ describe("TinyFish web search provider", () => {
expect(response.sources.at(-1)?.url).toBe("https://example.com/raw-page-11");
});
it("deduplicates and normalizes results across pages", async () => {
const captured: URL[] = [];
const firstPage = tinyFishResults("dedupe", 10);
firstPage[0] = {
title: " Primary title ",
url: " https://example.com/dedupe-0 ",
snippet: " spaced \n snippet ",
site_name: " Example ",
};
firstPage[1] = {
title: "Duplicate title",
url: "https://example.com/dedupe-0",
snippet: "duplicate snippet",
};
const fetchMock: FetchImpl = async input => {
const url = input instanceof URL ? input : new URL(typeof input === "string" ? input : input.url);
captured.push(url);
const page = Number(url.searchParams.get("page") ?? 0);
const results = page === 0 ? firstPage : tinyFishResults("dedupe", 1, 10);
return new Response(JSON.stringify(tinyFishPage(results, page, 11)), {
status: 200,
headers: { "Content-Type": "application/json" },
});
};
const response = await searchTinyFish({ ...makeParams("dedupe fish"), limit: 10, fetch: fetchMock });
expect(captured.map(url => url.searchParams.get("page"))).toEqual(["0", "1"]);
expect(response.sources).toHaveLength(10);
expect(response.sources[0]).toMatchObject({
title: "Primary title",
url: "https://example.com/dedupe-0",
snippet: "spaced snippet",
});
expect(response.sources.at(-1)?.url).toBe("https://example.com/dedupe-10");
});
it("stops early for limit 20 when page 0 returns fewer than 10 raw results", async () => {
const captured: URL[] = [];
const fetchMock: FetchImpl = async input => {