import { afterEach, describe, expect, it, vi } from "vitest"; import { DDG_HTML_URL } from "../constants.js"; import * as ddg from "../ddg.js"; import { buildProviders } from "../search.js"; import * as zai from "../zai.js"; afterEach(() => vi.restoreAllMocks()); async function search(signal?: AbortSignal, timeout = 5000) { vi.spyOn(zai, "isZaiAvailable").mockResolvedValue(false); const providers = await buildProviders({ query: "Node & Rust + docs", count: 5, timeout, signal, }); const provider = providers.find((entry) => entry.source === "ddg"); if (!provider) throw new Error("DDG provider missing"); return provider.search(); } describe("DDG form POST through the provider HTTP path", () => { it("keeps nested result blocks separate and decodes snippet entities", () => { const card = (id: number) => ``; const result = ddg.parseDdgResults(card(1) + card(2)); expect(result.results).toHaveLength(2); expect(result.results.map(({ snippet }) => snippet)).toEqual([ "don't & 🦀 � �", "don't & 🦀 � �", ]); expect(result.diagnostics.rawBlockCount).toBe(2); }); it("posts the no-JS form with origin headers and preserves full snippets", async () => { vi.spyOn(ddg, "throttleWait").mockReturnValue(0); const snippet = "full excerpt ".repeat(3000).trim(); // Reduced live HTML: nested result__extras must not terminate the outer result block. const html = ``; const network = vi .spyOn(globalThis, "fetch") .mockResolvedValue(new Response(html)); expect(await search()).toEqual([ { title: "Docs", url: "https://example.com/docs", snippet }, ]); const [url, init] = network.mock.calls[0]; expect(url).toBe(DDG_HTML_URL); expect(init?.method).toBe("POST"); expect(init?.redirect).toBe("manual"); expect(init?.headers).toMatchObject({ "Content-Type": "application/x-www-form-urlencoded", Origin: "https://html.duckduckgo.com", Referer: "https://html.duckduckgo.com/", }); expect(Object.fromEntries(new URLSearchParams(String(init?.body)))).toEqual( { q: "Node & Rust + docs", b: "", kl: "wt-wt" }, ); }); it("still reports CAPTCHA as failure rather than empty results", async () => { vi.spyOn(ddg, "throttleWait").mockReturnValue(0); vi.spyOn(globalThis, "fetch").mockResolvedValue( new Response('
Challenge
', { status: 202, }), ); await expect(search()).rejects.toThrow(/captcha/i); }); it("refuses redirects rather than forwarding the search form", async () => { vi.spyOn(ddg, "throttleWait").mockReturnValue(0); const network = vi.spyOn(globalThis, "fetch").mockResolvedValue( new Response(null, { status: 302, headers: { Location: "http://127.0.0.1/" }, }), ); await expect(search()).rejects.toThrow(/302/); expect(network).toHaveBeenCalledTimes(1); expect(network.mock.calls[0][1]?.redirect).toBe("manual"); }); it("honors cancellation before I/O", async () => { const network = vi.spyOn(globalThis, "fetch"); await expect( search(AbortSignal.abort(new Error("cancelled"))), ).rejects.toThrow("cancelled"); expect(network).not.toHaveBeenCalled(); }); it("bounds the throttle wait by the request deadline", async () => { vi.spyOn(ddg, "throttleWait").mockReturnValue(5000); const network = vi.spyOn(globalThis, "fetch"); await expect(search(new AbortController().signal, 10)).rejects.toThrow( /abort/i, ); expect(network).not.toHaveBeenCalled(); }); });