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();
});
});