singularity-forge/src/tests/provider.test.ts
2026-05-05 14:46:18 +02:00

469 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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)",
);
});