Files
openclaw/src/secrets/runtime-web-tools.test.ts
Luke 7761e7626f Providers: add Opencode Go support (#42313)
* feat(providers): add opencode-go provider support and onboarding

* Onboard: unify OpenCode auth handling openclaw#42313 thanks @ImLukeF

* Docs: merge OpenCode Zen and Go docs openclaw#42313 thanks @ImLukeF

* Update CHANGELOG.md

---------

Co-authored-by: Ubuntu <ubuntu@vps-90352893.vps.ovh.ca>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
2026-03-11 01:31:06 -04:00

452 lines
14 KiB
TypeScript

import { afterEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../config/config.js";
import * as secretResolve from "./resolve.js";
import { createResolverContext } from "./runtime-shared.js";
import { resolveRuntimeWebTools } from "./runtime-web-tools.js";
type ProviderUnderTest = "brave" | "gemini" | "grok" | "kimi" | "perplexity";
function asConfig(value: unknown): OpenClawConfig {
return value as OpenClawConfig;
}
async function runRuntimeWebTools(params: { config: OpenClawConfig; env?: NodeJS.ProcessEnv }) {
const sourceConfig = structuredClone(params.config);
const resolvedConfig = structuredClone(params.config);
const context = createResolverContext({
sourceConfig,
env: params.env ?? {},
});
const metadata = await resolveRuntimeWebTools({
sourceConfig,
resolvedConfig,
context,
});
return { metadata, resolvedConfig, context };
}
function createProviderSecretRefConfig(
provider: ProviderUnderTest,
envRefId: string,
): OpenClawConfig {
const search: Record<string, unknown> = {
enabled: true,
provider,
};
if (provider === "brave") {
search.apiKey = { source: "env", provider: "default", id: envRefId };
} else {
search[provider] = {
apiKey: { source: "env", provider: "default", id: envRefId },
};
}
return asConfig({
tools: {
web: {
search,
},
},
});
}
function readProviderKey(config: OpenClawConfig, provider: ProviderUnderTest): unknown {
if (provider === "brave") {
return config.tools?.web?.search?.apiKey;
}
if (provider === "gemini") {
return config.tools?.web?.search?.gemini?.apiKey;
}
if (provider === "grok") {
return config.tools?.web?.search?.grok?.apiKey;
}
if (provider === "kimi") {
return config.tools?.web?.search?.kimi?.apiKey;
}
return config.tools?.web?.search?.perplexity?.apiKey;
}
describe("runtime web tools resolution", () => {
afterEach(() => {
vi.restoreAllMocks();
});
it.each([
{
provider: "brave" as const,
envRefId: "BRAVE_PROVIDER_REF",
resolvedKey: "brave-provider-key",
},
{
provider: "gemini" as const,
envRefId: "GEMINI_PROVIDER_REF",
resolvedKey: "gemini-provider-key",
},
{
provider: "grok" as const,
envRefId: "GROK_PROVIDER_REF",
resolvedKey: "grok-provider-key",
},
{
provider: "kimi" as const,
envRefId: "KIMI_PROVIDER_REF",
resolvedKey: "kimi-provider-key",
},
{
provider: "perplexity" as const,
envRefId: "PERPLEXITY_PROVIDER_REF",
resolvedKey: "pplx-provider-key",
},
])(
"resolves configured provider SecretRef for $provider",
async ({ provider, envRefId, resolvedKey }) => {
const { metadata, resolvedConfig, context } = await runRuntimeWebTools({
config: createProviderSecretRefConfig(provider, envRefId),
env: {
[envRefId]: resolvedKey,
},
});
expect(metadata.search.providerConfigured).toBe(provider);
expect(metadata.search.providerSource).toBe("configured");
expect(metadata.search.selectedProvider).toBe(provider);
expect(metadata.search.selectedProviderKeySource).toBe("secretRef");
expect(readProviderKey(resolvedConfig, provider)).toBe(resolvedKey);
expect(context.warnings.map((warning) => warning.code)).not.toContain(
"WEB_SEARCH_KEY_UNRESOLVED_NO_FALLBACK",
);
if (provider === "perplexity") {
expect(metadata.search.perplexityTransport).toBe("search_api");
}
},
);
it("auto-detects provider precedence across all configured providers", async () => {
const { metadata, resolvedConfig, context } = await runRuntimeWebTools({
config: asConfig({
tools: {
web: {
search: {
apiKey: { source: "env", provider: "default", id: "BRAVE_REF" },
gemini: {
apiKey: { source: "env", provider: "default", id: "GEMINI_REF" },
},
grok: {
apiKey: { source: "env", provider: "default", id: "GROK_REF" },
},
kimi: {
apiKey: { source: "env", provider: "default", id: "KIMI_REF" },
},
perplexity: {
apiKey: { source: "env", provider: "default", id: "PERPLEXITY_REF" },
},
},
},
},
}),
env: {
BRAVE_REF: "brave-precedence-key",
GEMINI_REF: "gemini-precedence-key",
GROK_REF: "grok-precedence-key",
KIMI_REF: "kimi-precedence-key",
PERPLEXITY_REF: "pplx-precedence-key",
},
});
expect(metadata.search.providerSource).toBe("auto-detect");
expect(metadata.search.selectedProvider).toBe("brave");
expect(resolvedConfig.tools?.web?.search?.apiKey).toBe("brave-precedence-key");
expect(context.warnings).toEqual(
expect.arrayContaining([
expect.objectContaining({ path: "tools.web.search.gemini.apiKey" }),
expect.objectContaining({ path: "tools.web.search.grok.apiKey" }),
expect.objectContaining({ path: "tools.web.search.kimi.apiKey" }),
expect.objectContaining({ path: "tools.web.search.perplexity.apiKey" }),
]),
);
});
it("auto-detects first available provider and keeps lower-priority refs inactive", async () => {
const { metadata, resolvedConfig, context } = await runRuntimeWebTools({
config: asConfig({
tools: {
web: {
search: {
apiKey: { source: "env", provider: "default", id: "BRAVE_API_KEY_REF" },
gemini: {
apiKey: {
source: "env",
provider: "default",
id: "MISSING_GEMINI_API_KEY_REF",
},
},
},
},
},
}),
env: {
BRAVE_API_KEY_REF: "brave-runtime-key", // pragma: allowlist secret
},
});
expect(metadata.search.providerSource).toBe("auto-detect");
expect(metadata.search.selectedProvider).toBe("brave");
expect(metadata.search.selectedProviderKeySource).toBe("secretRef");
expect(resolvedConfig.tools?.web?.search?.apiKey).toBe("brave-runtime-key");
expect(resolvedConfig.tools?.web?.search?.gemini?.apiKey).toEqual({
source: "env",
provider: "default",
id: "MISSING_GEMINI_API_KEY_REF",
});
expect(context.warnings).toEqual(
expect.arrayContaining([
expect.objectContaining({
code: "SECRETS_REF_IGNORED_INACTIVE_SURFACE",
path: "tools.web.search.gemini.apiKey",
}),
]),
);
expect(context.warnings.map((warning) => warning.code)).not.toContain(
"WEB_SEARCH_KEY_UNRESOLVED_NO_FALLBACK",
);
});
it("auto-detects the next provider when a higher-priority ref is unresolved", async () => {
const { metadata, resolvedConfig, context } = await runRuntimeWebTools({
config: asConfig({
tools: {
web: {
search: {
apiKey: { source: "env", provider: "default", id: "MISSING_BRAVE_API_KEY_REF" },
gemini: {
apiKey: { source: "env", provider: "default", id: "GEMINI_API_KEY_REF" },
},
},
},
},
}),
env: {
GEMINI_API_KEY_REF: "gemini-runtime-key", // pragma: allowlist secret
},
});
expect(metadata.search.providerSource).toBe("auto-detect");
expect(metadata.search.selectedProvider).toBe("gemini");
expect(resolvedConfig.tools?.web?.search?.gemini?.apiKey).toBe("gemini-runtime-key");
expect(context.warnings).toEqual(
expect.arrayContaining([
expect.objectContaining({
code: "SECRETS_REF_IGNORED_INACTIVE_SURFACE",
path: "tools.web.search.apiKey",
}),
]),
);
expect(context.warnings.map((warning) => warning.code)).not.toContain(
"WEB_SEARCH_KEY_UNRESOLVED_NO_FALLBACK",
);
});
it("warns when provider is invalid and falls back to auto-detect", async () => {
const { metadata, resolvedConfig, context } = await runRuntimeWebTools({
config: asConfig({
tools: {
web: {
search: {
provider: "invalid-provider",
gemini: {
apiKey: { source: "env", provider: "default", id: "GEMINI_API_KEY_REF" },
},
},
},
},
}),
env: {
GEMINI_API_KEY_REF: "gemini-runtime-key", // pragma: allowlist secret
},
});
expect(metadata.search.providerConfigured).toBeUndefined();
expect(metadata.search.providerSource).toBe("auto-detect");
expect(metadata.search.selectedProvider).toBe("gemini");
expect(resolvedConfig.tools?.web?.search?.gemini?.apiKey).toBe("gemini-runtime-key");
expect(metadata.search.diagnostics).toEqual(
expect.arrayContaining([
expect.objectContaining({
code: "WEB_SEARCH_PROVIDER_INVALID_AUTODETECT",
path: "tools.web.search.provider",
}),
]),
);
expect(context.warnings).toEqual(
expect.arrayContaining([
expect.objectContaining({
code: "WEB_SEARCH_PROVIDER_INVALID_AUTODETECT",
path: "tools.web.search.provider",
}),
]),
);
});
it("fails fast when configured provider ref is unresolved with no fallback", async () => {
const sourceConfig = asConfig({
tools: {
web: {
search: {
provider: "gemini",
gemini: {
apiKey: { source: "env", provider: "default", id: "MISSING_GEMINI_API_KEY_REF" },
},
},
},
},
});
const resolvedConfig = structuredClone(sourceConfig);
const context = createResolverContext({
sourceConfig,
env: {},
});
await expect(
resolveRuntimeWebTools({
sourceConfig,
resolvedConfig,
context,
}),
).rejects.toThrow("[WEB_SEARCH_KEY_UNRESOLVED_NO_FALLBACK]");
expect(context.warnings).toEqual(
expect.arrayContaining([
expect.objectContaining({
code: "WEB_SEARCH_KEY_UNRESOLVED_NO_FALLBACK",
path: "tools.web.search.gemini.apiKey",
}),
]),
);
});
it("does not resolve Firecrawl SecretRef when Firecrawl is inactive", async () => {
const resolveSpy = vi.spyOn(secretResolve, "resolveSecretRefValues");
const { metadata, context } = await runRuntimeWebTools({
config: asConfig({
tools: {
web: {
fetch: {
enabled: false,
firecrawl: {
apiKey: { source: "env", provider: "default", id: "MISSING_FIRECRAWL_REF" },
},
},
},
},
}),
});
expect(resolveSpy).not.toHaveBeenCalled();
expect(metadata.fetch.firecrawl.active).toBe(false);
expect(metadata.fetch.firecrawl.apiKeySource).toBe("secretRef");
expect(context.warnings).toEqual(
expect.arrayContaining([
expect.objectContaining({
code: "SECRETS_REF_IGNORED_INACTIVE_SURFACE",
path: "tools.web.fetch.firecrawl.apiKey",
}),
]),
);
});
it("does not resolve Firecrawl SecretRef when Firecrawl is disabled", async () => {
const resolveSpy = vi.spyOn(secretResolve, "resolveSecretRefValues");
const { metadata, context } = await runRuntimeWebTools({
config: asConfig({
tools: {
web: {
fetch: {
enabled: true,
firecrawl: {
enabled: false,
apiKey: { source: "env", provider: "default", id: "MISSING_FIRECRAWL_REF" },
},
},
},
},
}),
});
expect(resolveSpy).not.toHaveBeenCalled();
expect(metadata.fetch.firecrawl.active).toBe(false);
expect(metadata.fetch.firecrawl.apiKeySource).toBe("secretRef");
expect(context.warnings).toEqual(
expect.arrayContaining([
expect.objectContaining({
code: "SECRETS_REF_IGNORED_INACTIVE_SURFACE",
path: "tools.web.fetch.firecrawl.apiKey",
}),
]),
);
});
it("uses env fallback for unresolved Firecrawl SecretRef when active", async () => {
const { metadata, resolvedConfig, context } = await runRuntimeWebTools({
config: asConfig({
tools: {
web: {
fetch: {
firecrawl: {
apiKey: { source: "env", provider: "default", id: "MISSING_FIRECRAWL_REF" },
},
},
},
},
}),
env: {
FIRECRAWL_API_KEY: "firecrawl-fallback-key", // pragma: allowlist secret
},
});
expect(metadata.fetch.firecrawl.active).toBe(true);
expect(metadata.fetch.firecrawl.apiKeySource).toBe("env");
expect(resolvedConfig.tools?.web?.fetch?.firecrawl?.apiKey).toBe("firecrawl-fallback-key");
expect(context.warnings).toEqual(
expect.arrayContaining([
expect.objectContaining({
code: "WEB_FETCH_FIRECRAWL_KEY_UNRESOLVED_FALLBACK_USED",
path: "tools.web.fetch.firecrawl.apiKey",
}),
]),
);
});
it("fails fast when active Firecrawl SecretRef is unresolved with no fallback", async () => {
const sourceConfig = asConfig({
tools: {
web: {
fetch: {
firecrawl: {
apiKey: { source: "env", provider: "default", id: "MISSING_FIRECRAWL_REF" },
},
},
},
},
});
const resolvedConfig = structuredClone(sourceConfig);
const context = createResolverContext({
sourceConfig,
env: {},
});
await expect(
resolveRuntimeWebTools({
sourceConfig,
resolvedConfig,
context,
}),
).rejects.toThrow("[WEB_FETCH_FIRECRAWL_KEY_UNRESOLVED_NO_FALLBACK]");
expect(context.warnings).toEqual(
expect.arrayContaining([
expect.objectContaining({
code: "WEB_FETCH_FIRECRAWL_KEY_UNRESOLVED_NO_FALLBACK",
path: "tools.web.fetch.firecrawl.apiKey",
}),
]),
);
});
});