import { afterEach, beforeEach, describe, expect, test } from "../test-helpers.js"; import { SearchError } from "../../src/search/types.js"; let searchPerplexity: typeof import("../../src/search/client.js").searchPerplexity; function createSseResponse(events: Array>, status = 200): Response { const streamText = [ ...events.map((event) => `data: ${JSON.stringify(event)}\n\n`), "data: [DONE]\n\n", ].join(""); return new Response(streamText, { status, headers: { "content-type": "text/event-stream" }, }); } describe("searchPerplexity", () => { const originalFetch = globalThis.fetch; beforeEach(async () => { const mod = await import(`../../src/search/client.js?t=${Date.now()}`); searchPerplexity = mod.searchPerplexity; }); afterEach(() => { globalThis.fetch = originalFetch; }); test("builds request body and headers according to protocol", async () => { let capturedUrl: RequestInfo | URL | undefined; let capturedInit: RequestInit | undefined; globalThis.fetch = (async (url: RequestInfo | URL, init?: RequestInit) => { capturedUrl = url; capturedInit = init; return createSseResponse([ { status: "COMPLETED", final: true, blocks: [ { intended_usage: "markdown_block", markdown_block: { answer: "answer text" } }, { intended_usage: "web_results", web_result_block: { web_results: [ { name: "Source", url: "https://example.com", snippet: "snippet", timestamp: "2026-02-16T10:00:00.000Z" }, ], }, }, ], }, ]); }) as unknown as typeof fetch; const controller = new AbortController(); const result = await searchPerplexity( { query: "latest Node release notes", recency: "week", model: "pplx_pro_upgraded", incognito: true }, "jwt-token", controller.signal, ); expect(String(capturedUrl)).toBe("https://www.perplexity.ai/rest/sse/perplexity_ask"); expect(capturedInit?.method).toBe("POST"); expect(capturedInit?.signal).toBe(controller.signal); const headers = new Headers(capturedInit?.headers); expect(headers.get("Authorization")).toBe("Bearer jwt-token"); expect(headers.get("Accept")).toBe("text/event-stream"); expect(headers.get("X-App-ApiVersion")).toBe("2.18"); expect(headers.get("X-Request-ID")).toBeTruthy(); const body = JSON.parse(String(capturedInit?.body)) as { query_str: string; params: { query_str: string; mode: string; model_preference: string; is_incognito: boolean; search_recency_filter: string | null; frontend_uuid: string; frontend_context_uuid: string; }; }; expect(body.query_str).toBe("latest Node release notes"); expect(body.params.query_str).toBe("latest Node release notes"); expect(body.params.mode).toBe("copilot"); expect(body.params.model_preference).toBe("pplx_pro_upgraded"); expect(body.params.is_incognito).toBe(true); expect(body.params.search_recency_filter).toBe("week"); expect(body.params.frontend_uuid).toBeTruthy(); expect(body.params.frontend_context_uuid).toBeTruthy(); expect(result.answer).toBe("answer text"); expect(result.sources).toHaveLength(1); }); test("passes model and incognito through to request body", async () => { let capturedInit: RequestInit | undefined; globalThis.fetch = (async (_url: RequestInfo | URL, init?: RequestInit) => { capturedInit = init; return createSseResponse([ { status: "COMPLETED", final: true, text: "answer", blocks: [] }, ]); }) as unknown as typeof fetch; await searchPerplexity( { query: "q", model: "claude46sonnetthinking", incognito: false }, "jwt-token", ); const body = JSON.parse(String(capturedInit?.body)) as { params: { model_preference: string; is_incognito: boolean }; }; expect(body.params.model_preference).toBe("claude46sonnetthinking"); expect(body.params.is_incognito).toBe(false); }); test("uses Cookie header for browser-cookie credentials", async () => { let capturedInit: RequestInit | undefined; globalThis.fetch = (async (_url: RequestInfo | URL, init?: RequestInit) => { capturedInit = init; return createSseResponse([ { status: "COMPLETED", final: true, text: "answer", blocks: [] }, ]); }) as unknown as typeof fetch; await searchPerplexity( { query: "q", model: "pplx_pro_upgraded", incognito: true }, { type: "oauth", cookies: "__Secure-next-auth.session-token=session; cf_clearance=clearance" }, ); const headers = new Headers(capturedInit?.headers); expect(headers.get("Cookie")).toBe("__Secure-next-auth.session-token=session; cf_clearance=clearance"); expect(headers.get("Authorization")).toBeNull(); }); test("passes incognito true through to request body", async () => { let capturedInit: RequestInit | undefined; globalThis.fetch = (async (_url: RequestInfo | URL, init?: RequestInit) => { capturedInit = init; return createSseResponse([ { status: "COMPLETED", final: true, text: "answer", blocks: [] }, ]); }) as unknown as typeof fetch; await searchPerplexity({ query: "q", model: "pplx_pro_upgraded", incognito: true }, "jwt-token"); const body = JSON.parse(String(capturedInit?.body)) as { params: { is_incognito: boolean }; }; expect(body.params.is_incognito).toBe(true); }); test("cancels the response body after a terminal event", async () => { let cancelCalled = false; const stream = new ReadableStream({ start(controller) { controller.enqueue(new TextEncoder().encode('data: {"status":"COMPLETED","final":true,"text":"answer"}\n\n')); }, cancel() { cancelCalled = true; }, }); globalThis.fetch = (async () => new Response(stream, { status: 200, headers: { "content-type": "text/event-stream" }, })) as unknown as typeof fetch; const result = await searchPerplexity({ query: "q", model: "pplx_pro_upgraded", incognito: true }, "jwt"); expect(result.answer).toBe("answer"); expect(cancelCalled).toBe(true); }); test("maps 401 and 403 responses to AUTH error", async () => { for (const status of [401, 403]) { globalThis.fetch = (async () => new Response("auth fail", { status })) as unknown as typeof fetch; await expect(searchPerplexity({ query: "q", model: "pplx_pro_upgraded", incognito: true }, "jwt")).rejects.toMatchObject({ name: "SearchError", code: "AUTH", }); } }); test("maps 429 responses to RATE_LIMIT error", async () => { globalThis.fetch = (async () => new Response("rate limited", { status: 429 })) as unknown as typeof fetch; await expect(searchPerplexity({ query: "q", model: "pplx_pro_upgraded", incognito: true }, "jwt")).rejects.toMatchObject({ name: "SearchError", code: "RATE_LIMIT", }); }); test("deduplicates sources by normalized URL", async () => { globalThis.fetch = (async () => createSseResponse([ { status: "COMPLETED", final: true, blocks: [ { intended_usage: "markdown_block", markdown_block: { answer: "answer text" } }, { intended_usage: "web_results", web_result_block: { web_results: [ { name: "A", url: "https://example.com/path" }, { name: "A duplicate", url: "https://example.com/path/" }, { name: "B", url: "https://another.example/path" }, ], }, }, ], }, ])) as unknown as typeof fetch; const result = await searchPerplexity({ query: "q", model: "pplx_pro_upgraded", incognito: true }, "jwt"); expect(result.sources).toHaveLength(2); expect(result.sources[0].url).toBe("https://example.com/path"); expect(result.sources[1].url).toBe("https://another.example/path"); }); test("answer extraction prioritizes markdown_block over ask_text and text", async () => { globalThis.fetch = (async () => createSseResponse([ { status: "COMPLETED", final: true, text: "fallback text", blocks: [ { intended_usage: "ask_text", markdown_block: { answer: "ask text" } }, { intended_usage: "markdown_block", markdown_block: { answer: "markdown answer" } }, ], sources_list: [{ title: "S", url: "https://example.com" }], }, ])) as unknown as typeof fetch; const result = await searchPerplexity({ query: "q", model: "pplx_pro_upgraded", incognito: true }, "jwt"); expect(result.answer).toBe("markdown answer"); }); test("answer extraction falls back to ask_text then text", async () => { globalThis.fetch = (async () => createSseResponse([ { status: "COMPLETED", final: true, text: "fallback text", blocks: [ { intended_usage: "ask_text", markdown_block: { answer: "ask answer" } }, ], sources_list: [{ title: "S", url: "https://example.com" }], }, ])) as unknown as typeof fetch; const askTextResult = await searchPerplexity({ query: "q", model: "pplx_pro_upgraded", incognito: true }, "jwt"); expect(askTextResult.answer).toBe("ask answer"); globalThis.fetch = (async () => createSseResponse([ { status: "COMPLETED", final: true, text: "text fallback", sources_list: [{ title: "S", url: "https://example.com" }], }, ])) as unknown as typeof fetch; const textResult = await searchPerplexity({ query: "q", model: "pplx_pro_upgraded", incognito: true }, "jwt"); expect(textResult.answer).toBe("text fallback"); }); test("returns EMPTY error when response has no answer and no sources", async () => { globalThis.fetch = (async () => createSseResponse([{ status: "COMPLETED", final: true }])) as unknown as typeof fetch; let thrown: unknown; try { await searchPerplexity({ query: "q", model: "pplx_pro_upgraded", incognito: true }, "jwt"); } catch (error) { thrown = error; } expect(thrown).toBeInstanceOf(SearchError); expect((thrown as SearchError).code).toBe("EMPTY"); }); });