import test from "node:test"; import assert from "node:assert/strict"; import { mkdtemp, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { loadWebSearchConfig, WebSearchConfigError } from "./config.ts"; async function writeTempConfig(contents: unknown) { const dir = await mkdtemp(join(tmpdir(), "pi-web-search-config-")); const file = join(dir, "web-search.json"); const body = typeof contents === "string" ? contents : JSON.stringify(contents, null, 2); await writeFile(file, body, "utf8"); return file; } test("loadWebSearchConfig returns a normalized default provider and provider lookup", async () => { const file = await writeTempConfig({ defaultProvider: "exa-main", providers: [ { name: "exa-main", type: "exa", apiKey: "exa-test-key", options: { defaultSearchLimit: 7, defaultFetchTextMaxCharacters: 9000, }, }, ], }); const config = await loadWebSearchConfig(file); assert.equal(config.defaultProviderName, "exa-main"); assert.equal(config.defaultProvider.name, "exa-main"); assert.equal(config.providersByName.get("exa-main")?.apiKey, "exa-test-key"); assert.equal(config.providers[0]?.options?.defaultSearchLimit, 7); }); test("loadWebSearchConfig normalizes a Tavily default with Exa fallback", async () => { const file = await writeTempConfig({ defaultProvider: "tavily-main", providers: [ { name: "tavily-main", type: "tavily", apiKey: "tvly-test-key", }, { name: "exa-fallback", type: "exa", apiKey: "exa-test-key", }, ], }); const config = await loadWebSearchConfig(file); assert.equal(config.defaultProviderName, "tavily-main"); assert.equal(config.defaultProvider.type, "tavily"); assert.equal(config.providersByName.get("exa-fallback")?.type, "exa"); }); test("loadWebSearchConfig rejects a missing default provider target", async () => { const file = await writeTempConfig({ defaultProvider: "missing", providers: [ { name: "exa-main", type: "exa", apiKey: "exa-test-key", }, ], }); await assert.rejects( () => loadWebSearchConfig(file), (error) => error instanceof WebSearchConfigError && /defaultProvider \"missing\"/.test(error.message), ); }); test("loadWebSearchConfig rejects a missing file with a helpful example message", async () => { const file = join(tmpdir(), "pi-web-search-does-not-exist.json"); await assert.rejects( () => loadWebSearchConfig(file), (error) => error instanceof WebSearchConfigError && error.message.includes(file) && error.message.includes('"defaultProvider"') && error.message.includes('"providers"'), ); }); test("loadWebSearchConfig accepts self-hosted Firecrawl without an apiKey and normalizes its baseUrl", async () => { const file = await writeTempConfig({ defaultProvider: "firecrawl-main", providers: [ { name: "firecrawl-main", type: "firecrawl", baseUrl: "https://firecrawl.internal.example/v2/", fallbackProviders: ["exa-fallback"], }, { name: "exa-fallback", type: "exa", apiKey: "exa-test-key", }, ], }); const config = await loadWebSearchConfig(file); const provider = config.providersByName.get("firecrawl-main"); assert.equal(provider?.type, "firecrawl"); assert.equal(provider?.baseUrl, "https://firecrawl.internal.example/v2"); assert.equal(provider?.apiKey, undefined); assert.deepEqual(provider?.fallbackProviders, ["exa-fallback"]); }); test("loadWebSearchConfig rejects Firecrawl cloud config without an apiKey", async () => { const file = await writeTempConfig({ defaultProvider: "firecrawl-main", providers: [ { name: "firecrawl-main", type: "firecrawl", }, ], }); await assert.rejects( () => loadWebSearchConfig(file), (error) => error instanceof WebSearchConfigError && /Firecrawl provider \"firecrawl-main\"/.test(error.message) && /apiKey/.test(error.message), ); }); test("loadWebSearchConfig rejects unknown fallback providers", async () => { const file = await writeTempConfig({ defaultProvider: "firecrawl-main", providers: [ { name: "firecrawl-main", type: "firecrawl", apiKey: "fc-test-key", fallbackProviders: ["missing-provider"], }, ], }); await assert.rejects( () => loadWebSearchConfig(file), (error) => error instanceof WebSearchConfigError && /fallback provider/.test(error.message) && /missing-provider/.test(error.message), ); }); test("loadWebSearchConfig rejects fallback cycles", async () => { const file = await writeTempConfig({ defaultProvider: "firecrawl-main", providers: [ { name: "firecrawl-main", type: "firecrawl", apiKey: "fc-test-key", fallbackProviders: ["exa-fallback"], }, { name: "exa-fallback", type: "exa", apiKey: "exa-test-key", fallbackProviders: ["firecrawl-main"], }, ], }); await assert.rejects( () => loadWebSearchConfig(file), (error) => error instanceof WebSearchConfigError && /cycle/i.test(error.message) && /firecrawl-main/.test(error.message) && /exa-fallback/.test(error.message), ); });