469 lines
15 KiB
TypeScript
469 lines
15 KiB
TypeScript
/**
|
||
* Tests for search provider selection, preference persistence, and key helpers.
|
||
*
|
||
* Covers:
|
||
* - resolveSearchProvider() scenarios (keys × preferences)
|
||
* - Preference get/set round-trip via AuthStorage
|
||
* - Key helper functions
|
||
*/
|
||
|
||
import assert from "node:assert/strict";
|
||
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||
import { tmpdir } from "node:os";
|
||
import { join } from "node:path";
|
||
import { afterEach, test } from "vitest";
|
||
|
||
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
||
|
||
function withEnv(
|
||
vars: Record<string, string | undefined>,
|
||
fn: () => void,
|
||
): void {
|
||
const originals: Record<string, string | undefined> = {};
|
||
for (const key of Object.keys(vars)) {
|
||
originals[key] = process.env[key];
|
||
if (vars[key] === undefined) {
|
||
delete process.env[key];
|
||
} else {
|
||
process.env[key] = vars[key];
|
||
}
|
||
}
|
||
try {
|
||
fn();
|
||
} finally {
|
||
for (const key of Object.keys(originals)) {
|
||
if (originals[key] === undefined) {
|
||
delete process.env[key];
|
||
} else {
|
||
process.env[key] = originals[key];
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
function makeTmpAuth(data: Record<string, unknown> = {}): {
|
||
authPath: string;
|
||
cleanup: () => void;
|
||
} {
|
||
const tmp = mkdtempSync(join(tmpdir(), "sf-provider-test-"));
|
||
const authPath = join(tmp, "auth.json");
|
||
writeFileSync(authPath, JSON.stringify(data));
|
||
return {
|
||
authPath,
|
||
cleanup: () => rmSync(tmp, { recursive: true, force: true }),
|
||
};
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
// 1. resolveSearchProvider — 8 scenarios
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
|
||
test("resolveSearchProvider returns tavily when only TAVILY_API_KEY is set", async (_t) => {
|
||
const { resolveSearchProvider } = await import(
|
||
"../resources/extensions/search-the-web/provider.ts"
|
||
);
|
||
const { cleanup } = makeTmpAuth();
|
||
afterEach(() => {
|
||
cleanup();
|
||
});
|
||
|
||
withEnv(
|
||
{
|
||
TAVILY_API_KEY: "tvly-test",
|
||
BRAVE_API_KEY: undefined,
|
||
MINIMAX_API_KEY: undefined,
|
||
MINIMAX_CODE_PLAN_KEY: undefined,
|
||
MINIMAX_CODING_API_KEY: undefined,
|
||
},
|
||
() => {
|
||
// Override preference read to use our temp auth (auto)
|
||
const result = resolveSearchProvider("auto");
|
||
assert.equal(result, "tavily");
|
||
},
|
||
);
|
||
});
|
||
|
||
test("resolveSearchProvider returns brave when only BRAVE_API_KEY is set", async () => {
|
||
const { resolveSearchProvider } = await import(
|
||
"../resources/extensions/search-the-web/provider.ts"
|
||
);
|
||
withEnv(
|
||
{
|
||
TAVILY_API_KEY: undefined,
|
||
BRAVE_API_KEY: "BSA-test",
|
||
MINIMAX_API_KEY: undefined,
|
||
MINIMAX_CODE_PLAN_KEY: undefined,
|
||
MINIMAX_CODING_API_KEY: undefined,
|
||
},
|
||
() => {
|
||
const result = resolveSearchProvider("auto");
|
||
assert.equal(result, "brave");
|
||
},
|
||
);
|
||
});
|
||
|
||
test("resolveSearchProvider returns serper when only SERPER_API_KEY is set", async () => {
|
||
const { resolveSearchProvider } = await import(
|
||
"../resources/extensions/search-the-web/provider.ts"
|
||
);
|
||
withEnv(
|
||
{
|
||
TAVILY_API_KEY: undefined,
|
||
BRAVE_API_KEY: undefined,
|
||
MINIMAX_API_KEY: undefined,
|
||
MINIMAX_CODE_PLAN_KEY: undefined,
|
||
MINIMAX_CODING_API_KEY: undefined,
|
||
SERPER_API_KEY: "serper-test",
|
||
},
|
||
() => {
|
||
const result = resolveSearchProvider("auto");
|
||
assert.equal(result, "serper");
|
||
},
|
||
);
|
||
});
|
||
|
||
test("resolveSearchProvider returns exa when only EXA_API_KEY is set", async () => {
|
||
const { resolveSearchProvider } = await import(
|
||
"../resources/extensions/search-the-web/provider.ts"
|
||
);
|
||
withEnv(
|
||
{
|
||
TAVILY_API_KEY: undefined,
|
||
BRAVE_API_KEY: undefined,
|
||
MINIMAX_API_KEY: undefined,
|
||
MINIMAX_CODE_PLAN_KEY: undefined,
|
||
MINIMAX_CODING_API_KEY: undefined,
|
||
SERPER_API_KEY: undefined,
|
||
EXA_API_KEY: "exa-test",
|
||
},
|
||
() => {
|
||
const result = resolveSearchProvider("auto");
|
||
assert.equal(result, "exa");
|
||
},
|
||
);
|
||
});
|
||
|
||
test("resolveSearchProvider returns tavily when both keys set and preference is auto", async () => {
|
||
const { resolveSearchProvider } = await import(
|
||
"../resources/extensions/search-the-web/provider.ts"
|
||
);
|
||
withEnv({ TAVILY_API_KEY: "tvly-test", BRAVE_API_KEY: "BSA-test" }, () => {
|
||
const result = resolveSearchProvider("auto");
|
||
assert.equal(result, "tavily");
|
||
});
|
||
});
|
||
|
||
test("resolveSearchProvider returns tavily when both keys set and preference is tavily", async () => {
|
||
const { resolveSearchProvider } = await import(
|
||
"../resources/extensions/search-the-web/provider.ts"
|
||
);
|
||
withEnv({ TAVILY_API_KEY: "tvly-test", BRAVE_API_KEY: "BSA-test" }, () => {
|
||
const result = resolveSearchProvider("tavily");
|
||
assert.equal(result, "tavily");
|
||
});
|
||
});
|
||
|
||
test("resolveSearchProvider returns brave when both keys set and preference is brave", async () => {
|
||
const { resolveSearchProvider } = await import(
|
||
"../resources/extensions/search-the-web/provider.ts"
|
||
);
|
||
withEnv({ TAVILY_API_KEY: "tvly-test", BRAVE_API_KEY: "BSA-test" }, () => {
|
||
const result = resolveSearchProvider("brave");
|
||
assert.equal(result, "brave");
|
||
});
|
||
});
|
||
|
||
test("resolveSearchProvider returns combosearch when preference is combosearch and any source is available", async () => {
|
||
const { resolveSearchProvider } = await import(
|
||
"../resources/extensions/search-the-web/provider.ts"
|
||
);
|
||
withEnv(
|
||
{
|
||
TAVILY_API_KEY: "tvly-test",
|
||
BRAVE_API_KEY: undefined,
|
||
OLLAMA_API_KEY: undefined,
|
||
},
|
||
() => {
|
||
const result = resolveSearchProvider("combosearch");
|
||
assert.equal(result, "combosearch");
|
||
},
|
||
);
|
||
});
|
||
|
||
test("resolveSearchProvider returns null when neither key is set", async () => {
|
||
const { resolveSearchProvider } = await import(
|
||
"../resources/extensions/search-the-web/provider.ts"
|
||
);
|
||
withEnv(
|
||
{
|
||
TAVILY_API_KEY: undefined,
|
||
BRAVE_API_KEY: undefined,
|
||
MINIMAX_API_KEY: undefined,
|
||
MINIMAX_CODE_PLAN_KEY: undefined,
|
||
MINIMAX_CODING_API_KEY: undefined,
|
||
OLLAMA_API_KEY: undefined,
|
||
},
|
||
() => {
|
||
const result = resolveSearchProvider("auto");
|
||
assert.equal(result, null);
|
||
},
|
||
);
|
||
});
|
||
|
||
test("resolveSearchProvider treats invalid preference as auto", async () => {
|
||
const { resolveSearchProvider } = await import(
|
||
"../resources/extensions/search-the-web/provider.ts"
|
||
);
|
||
withEnv({ TAVILY_API_KEY: "tvly-test", BRAVE_API_KEY: "BSA-test" }, () => {
|
||
const result = resolveSearchProvider("google");
|
||
assert.equal(
|
||
result,
|
||
"tavily",
|
||
"invalid preference falls back to auto → tavily first",
|
||
);
|
||
});
|
||
});
|
||
|
||
test("resolveSearchProvider falls back to other provider when preferred key missing", async () => {
|
||
const { resolveSearchProvider } = await import(
|
||
"../resources/extensions/search-the-web/provider.ts"
|
||
);
|
||
// Prefer tavily but only brave key exists → falls back to brave
|
||
withEnv(
|
||
{
|
||
TAVILY_API_KEY: undefined,
|
||
BRAVE_API_KEY: "BSA-test",
|
||
MINIMAX_API_KEY: undefined,
|
||
MINIMAX_CODE_PLAN_KEY: undefined,
|
||
MINIMAX_CODING_API_KEY: undefined,
|
||
},
|
||
() => {
|
||
const result = resolveSearchProvider("tavily");
|
||
assert.equal(
|
||
result,
|
||
"brave",
|
||
"falls back to brave when tavily preferred but key missing",
|
||
);
|
||
},
|
||
);
|
||
// Prefer brave but only tavily key exists → falls back to tavily
|
||
withEnv(
|
||
{
|
||
TAVILY_API_KEY: "tvly-test",
|
||
BRAVE_API_KEY: undefined,
|
||
MINIMAX_API_KEY: undefined,
|
||
MINIMAX_CODE_PLAN_KEY: undefined,
|
||
MINIMAX_CODING_API_KEY: undefined,
|
||
},
|
||
() => {
|
||
const result = resolveSearchProvider("brave");
|
||
assert.equal(
|
||
result,
|
||
"tavily",
|
||
"falls back to tavily when brave preferred but key missing",
|
||
);
|
||
},
|
||
);
|
||
withEnv(
|
||
{
|
||
TAVILY_API_KEY: undefined,
|
||
BRAVE_API_KEY: undefined,
|
||
MINIMAX_API_KEY: undefined,
|
||
MINIMAX_CODE_PLAN_KEY: undefined,
|
||
MINIMAX_CODING_API_KEY: undefined,
|
||
SERPER_API_KEY: "serper-test",
|
||
},
|
||
() => {
|
||
const result = resolveSearchProvider("brave");
|
||
assert.equal(
|
||
result,
|
||
"serper",
|
||
"falls back to serper when brave preferred but only serper key exists",
|
||
);
|
||
},
|
||
);
|
||
withEnv(
|
||
{
|
||
TAVILY_API_KEY: undefined,
|
||
BRAVE_API_KEY: undefined,
|
||
MINIMAX_API_KEY: undefined,
|
||
MINIMAX_CODE_PLAN_KEY: undefined,
|
||
MINIMAX_CODING_API_KEY: undefined,
|
||
SERPER_API_KEY: undefined,
|
||
EXA_API_KEY: "exa-test",
|
||
},
|
||
() => {
|
||
const result = resolveSearchProvider("brave");
|
||
assert.equal(
|
||
result,
|
||
"exa",
|
||
"falls back to exa when brave preferred but only exa key exists",
|
||
);
|
||
},
|
||
);
|
||
});
|
||
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
// 2. Preference get/set round-trip
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
|
||
test("getSearchProviderPreference returns auto when no preference stored", async (_t) => {
|
||
const { getSearchProviderPreference } = await import(
|
||
"../resources/extensions/search-the-web/provider.ts"
|
||
);
|
||
const { authPath, cleanup } = makeTmpAuth();
|
||
afterEach(() => {
|
||
cleanup();
|
||
});
|
||
|
||
const pref = getSearchProviderPreference(authPath);
|
||
assert.equal(pref, "auto");
|
||
});
|
||
|
||
test("getSearchProviderPreference reads from auth.json via AuthStorage", async (_t) => {
|
||
const { getSearchProviderPreference } = await import(
|
||
"../resources/extensions/search-the-web/provider.ts"
|
||
);
|
||
const { authPath, cleanup } = makeTmpAuth({
|
||
search_provider: { type: "api_key", key: "tavily" },
|
||
});
|
||
afterEach(() => {
|
||
cleanup();
|
||
});
|
||
|
||
const pref = getSearchProviderPreference(authPath);
|
||
assert.equal(pref, "tavily");
|
||
});
|
||
|
||
test("setSearchProviderPreference writes to auth.json via AuthStorage", async (_t) => {
|
||
const { getSearchProviderPreference, setSearchProviderPreference } =
|
||
await import("../resources/extensions/search-the-web/provider.ts");
|
||
const { authPath, cleanup } = makeTmpAuth();
|
||
afterEach(() => {
|
||
cleanup();
|
||
});
|
||
|
||
setSearchProviderPreference("brave", authPath);
|
||
const pref = getSearchProviderPreference(authPath);
|
||
assert.equal(pref, "brave");
|
||
|
||
// Round-trip: change to tavily
|
||
setSearchProviderPreference("tavily", authPath);
|
||
assert.equal(getSearchProviderPreference(authPath), "tavily");
|
||
|
||
setSearchProviderPreference("combosearch", authPath);
|
||
assert.equal(getSearchProviderPreference(authPath), "combosearch");
|
||
|
||
// Round-trip: change to auto
|
||
setSearchProviderPreference("auto", authPath);
|
||
assert.equal(getSearchProviderPreference(authPath), "auto");
|
||
});
|
||
|
||
test("getSearchProviderPreference returns auto for invalid stored value", async (_t) => {
|
||
const { getSearchProviderPreference } = await import(
|
||
"../resources/extensions/search-the-web/provider.ts"
|
||
);
|
||
const { authPath, cleanup } = makeTmpAuth({
|
||
search_provider: { type: "api_key", key: "google" },
|
||
});
|
||
afterEach(() => {
|
||
cleanup();
|
||
});
|
||
|
||
const pref = getSearchProviderPreference(authPath);
|
||
assert.equal(pref, "auto", "invalid stored value falls back to auto");
|
||
});
|
||
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
// 3. Key helper functions
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
|
||
test("getTavilyApiKey reads from process.env.TAVILY_API_KEY", async () => {
|
||
const { getTavilyApiKey } = await import(
|
||
"../resources/extensions/search-the-web/provider.ts"
|
||
);
|
||
withEnv({ TAVILY_API_KEY: "tvly-test-key" }, () => {
|
||
assert.equal(getTavilyApiKey(), "tvly-test-key");
|
||
});
|
||
withEnv({ TAVILY_API_KEY: undefined }, () => {
|
||
assert.equal(getTavilyApiKey(), "");
|
||
});
|
||
});
|
||
|
||
test("getBraveApiKey reads from process.env.BRAVE_API_KEY", async () => {
|
||
const { getBraveApiKey } = await import(
|
||
"../resources/extensions/search-the-web/provider.ts"
|
||
);
|
||
withEnv({ BRAVE_API_KEY: "BSA-test-key" }, () => {
|
||
assert.equal(getBraveApiKey(), "BSA-test-key");
|
||
});
|
||
withEnv({ BRAVE_API_KEY: undefined }, () => {
|
||
assert.equal(getBraveApiKey(), "");
|
||
});
|
||
});
|
||
|
||
test("getSerperApiKey reads from process.env.SERPER_API_KEY", async () => {
|
||
const { getSerperApiKey } = await import(
|
||
"../resources/extensions/search-the-web/provider.ts"
|
||
);
|
||
withEnv({ SERPER_API_KEY: "serper-test-key" }, () => {
|
||
assert.equal(getSerperApiKey(), "serper-test-key");
|
||
});
|
||
withEnv({ SERPER_API_KEY: undefined }, () => {
|
||
assert.equal(getSerperApiKey(), "");
|
||
});
|
||
});
|
||
|
||
test("getExaApiKey reads from process.env.EXA_API_KEY", async () => {
|
||
const { getExaApiKey } = await import(
|
||
"../resources/extensions/search-the-web/provider.ts"
|
||
);
|
||
withEnv({ EXA_API_KEY: "exa-test-key" }, () => {
|
||
assert.equal(getExaApiKey(), "exa-test-key");
|
||
});
|
||
withEnv({ EXA_API_KEY: undefined }, () => {
|
||
assert.equal(getExaApiKey(), "");
|
||
});
|
||
});
|
||
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
// 4. Boundary contract — S01→S02 public API surface
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
|
||
test("provider.ts exports exactly the expected functions", async () => {
|
||
const provider = await import(
|
||
"../resources/extensions/search-the-web/provider.ts"
|
||
);
|
||
|
||
const expectedExports = [
|
||
"resolveSearchProvider",
|
||
"getTavilyApiKey",
|
||
"getBraveApiKey",
|
||
"braveHeaders",
|
||
"getOllamaApiKey",
|
||
"getSerperApiKey",
|
||
"getExaApiKey",
|
||
"getMiniMaxSearchApiKey",
|
||
"getSearchProviderPreference",
|
||
"setSearchProviderPreference",
|
||
] as const;
|
||
|
||
// Each expected export exists and is a function
|
||
for (const name of expectedExports) {
|
||
assert.equal(
|
||
typeof provider[name],
|
||
"function",
|
||
`${name} should be an exported function`,
|
||
);
|
||
}
|
||
|
||
// No unexpected function exports (types are erased at runtime, so only check functions)
|
||
const actualFunctions = Object.keys(provider).filter(
|
||
(k) => typeof (provider as Record<string, unknown>)[k] === "function",
|
||
);
|
||
assert.deepEqual(
|
||
actualFunctions.sort(),
|
||
[...expectedExports].sort(),
|
||
"provider.ts should export exactly the expected functions (no extra function exports)",
|
||
);
|
||
});
|