mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:30:42 +00:00
test: narrow web search contract runtime loads
Honor targeted includes in the contracts Vitest lane and compare bundled web-search fast-path artifacts against plugin-owned runtime artifacts instead of loading whole plugin entries. Split Google and Firecrawl runtime-only work behind lazy seams so provider registration stays metadata-light. Also keep Perplexity contract metadata aligned by sharing its runtime transport resolution with the contract artifact.
This commit is contained in:
@@ -1,27 +1,22 @@
|
||||
import { Type } from "@sinclair/typebox";
|
||||
import {
|
||||
enablePluginInConfig,
|
||||
getScopedCredentialValue,
|
||||
resolveProviderWebSearchPluginConfig,
|
||||
setScopedCredentialValue,
|
||||
setProviderWebSearchPluginConfigValue,
|
||||
createWebSearchProviderContractFields,
|
||||
type WebSearchProviderPlugin,
|
||||
} from "openclaw/plugin-sdk/provider-web-search";
|
||||
import { runFirecrawlSearch } from "./firecrawl-client.js";
|
||||
} from "openclaw/plugin-sdk/provider-web-search-contract";
|
||||
|
||||
const GenericFirecrawlSearchSchema = Type.Object(
|
||||
{
|
||||
query: Type.String({ description: "Search query string." }),
|
||||
count: Type.Optional(
|
||||
Type.Number({
|
||||
description: "Number of results to return (1-10).",
|
||||
minimum: 1,
|
||||
maximum: 10,
|
||||
}),
|
||||
),
|
||||
const FIRECRAWL_CREDENTIAL_PATH = "plugins.entries.firecrawl.config.webSearch.apiKey";
|
||||
const GenericFirecrawlSearchSchema = {
|
||||
type: "object",
|
||||
properties: {
|
||||
query: { type: "string", description: "Search query string." },
|
||||
count: {
|
||||
type: "number",
|
||||
description: "Number of results to return (1-10).",
|
||||
minimum: 1,
|
||||
maximum: 10,
|
||||
},
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
additionalProperties: false,
|
||||
} satisfies Record<string, unknown>;
|
||||
|
||||
export function createFirecrawlWebSearchProvider(): WebSearchProviderPlugin {
|
||||
return {
|
||||
@@ -35,27 +30,25 @@ export function createFirecrawlWebSearchProvider(): WebSearchProviderPlugin {
|
||||
signupUrl: "https://www.firecrawl.dev/",
|
||||
docsUrl: "https://docs.openclaw.ai/tools/firecrawl",
|
||||
autoDetectOrder: 60,
|
||||
credentialPath: "plugins.entries.firecrawl.config.webSearch.apiKey",
|
||||
inactiveSecretPaths: ["plugins.entries.firecrawl.config.webSearch.apiKey"],
|
||||
getCredentialValue: (searchConfig) => getScopedCredentialValue(searchConfig, "firecrawl"),
|
||||
setCredentialValue: (searchConfigTarget, value) =>
|
||||
setScopedCredentialValue(searchConfigTarget, "firecrawl", value),
|
||||
getConfiguredCredentialValue: (config) =>
|
||||
resolveProviderWebSearchPluginConfig(config, "firecrawl")?.apiKey,
|
||||
setConfiguredCredentialValue: (configTarget, value) => {
|
||||
setProviderWebSearchPluginConfigValue(configTarget, "firecrawl", "apiKey", value);
|
||||
},
|
||||
applySelectionConfig: (config) => enablePluginInConfig(config, "firecrawl").config,
|
||||
credentialPath: FIRECRAWL_CREDENTIAL_PATH,
|
||||
...createWebSearchProviderContractFields({
|
||||
credentialPath: FIRECRAWL_CREDENTIAL_PATH,
|
||||
searchCredential: { type: "scoped", scopeId: "firecrawl" },
|
||||
configuredCredential: { pluginId: "firecrawl" },
|
||||
selectionPluginId: "firecrawl",
|
||||
}),
|
||||
createTool: (ctx) => ({
|
||||
description:
|
||||
"Search the web using Firecrawl. Returns structured results with snippets from Firecrawl Search. Use firecrawl_search for Firecrawl-specific knobs like sources or categories.",
|
||||
parameters: GenericFirecrawlSearchSchema,
|
||||
execute: async (args) =>
|
||||
await runFirecrawlSearch({
|
||||
execute: async (args) => {
|
||||
const { runFirecrawlSearch } = await import("./firecrawl-client.js");
|
||||
return await runFirecrawlSearch({
|
||||
cfg: ctx.config,
|
||||
query: typeof args.query === "string" ? args.query : "",
|
||||
count: typeof args.count === "number" ? args.count : undefined,
|
||||
}),
|
||||
});
|
||||
},
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
194
extensions/google/src/gemini-web-search-provider.runtime.ts
Normal file
194
extensions/google/src/gemini-web-search-provider.runtime.ts
Normal file
@@ -0,0 +1,194 @@
|
||||
import {
|
||||
buildSearchCacheKey,
|
||||
buildUnsupportedSearchFilterResponse,
|
||||
DEFAULT_SEARCH_COUNT,
|
||||
readCachedSearchPayload,
|
||||
readConfiguredSecretString,
|
||||
readNumberParam,
|
||||
readProviderEnvValue,
|
||||
readStringParam,
|
||||
resolveCitationRedirectUrl,
|
||||
resolveSearchCacheTtlMs,
|
||||
resolveSearchCount,
|
||||
resolveSearchTimeoutSeconds,
|
||||
type SearchConfigRecord,
|
||||
withTrustedWebSearchEndpoint,
|
||||
wrapWebContent,
|
||||
writeCachedSearchPayload,
|
||||
} from "openclaw/plugin-sdk/provider-web-search";
|
||||
import { DEFAULT_GOOGLE_API_BASE_URL } from "../api.js";
|
||||
import {
|
||||
resolveGeminiConfig,
|
||||
resolveGeminiModel,
|
||||
type GeminiConfig,
|
||||
} from "./gemini-web-search-provider.shared.js";
|
||||
|
||||
const GEMINI_API_BASE = DEFAULT_GOOGLE_API_BASE_URL;
|
||||
|
||||
type GeminiGroundingResponse = {
|
||||
candidates?: Array<{
|
||||
content?: {
|
||||
parts?: Array<{
|
||||
text?: string;
|
||||
}>;
|
||||
};
|
||||
groundingMetadata?: {
|
||||
groundingChunks?: Array<{
|
||||
web?: {
|
||||
uri?: string;
|
||||
title?: string;
|
||||
};
|
||||
}>;
|
||||
};
|
||||
}>;
|
||||
error?: {
|
||||
code?: number;
|
||||
message?: string;
|
||||
status?: string;
|
||||
};
|
||||
};
|
||||
|
||||
export function resolveGeminiRuntimeApiKey(gemini?: GeminiConfig): string | undefined {
|
||||
return (
|
||||
readConfiguredSecretString(gemini?.apiKey, "tools.web.search.gemini.apiKey") ??
|
||||
readProviderEnvValue(["GEMINI_API_KEY"])
|
||||
);
|
||||
}
|
||||
|
||||
async function runGeminiSearch(params: {
|
||||
query: string;
|
||||
apiKey: string;
|
||||
model: string;
|
||||
timeoutSeconds: number;
|
||||
}): Promise<{ content: string; citations: Array<{ url: string; title?: string }> }> {
|
||||
const endpoint = `${GEMINI_API_BASE}/models/${params.model}:generateContent`;
|
||||
|
||||
return withTrustedWebSearchEndpoint(
|
||||
{
|
||||
url: endpoint,
|
||||
timeoutSeconds: params.timeoutSeconds,
|
||||
init: {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-goog-api-key": params.apiKey,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
contents: [{ parts: [{ text: params.query }] }],
|
||||
tools: [{ google_search: {} }],
|
||||
}),
|
||||
},
|
||||
},
|
||||
async (res) => {
|
||||
if (!res.ok) {
|
||||
const safeDetail = ((await res.text()) || res.statusText).replace(
|
||||
/key=[^&\s]+/giu,
|
||||
"key=***",
|
||||
);
|
||||
throw new Error(`Gemini API error (${res.status}): ${safeDetail}`);
|
||||
}
|
||||
|
||||
let data: GeminiGroundingResponse;
|
||||
try {
|
||||
data = (await res.json()) as GeminiGroundingResponse;
|
||||
} catch (error) {
|
||||
const safeError = String(error).replace(/key=[^&\s]+/giu, "key=***");
|
||||
throw new Error(`Gemini API returned invalid JSON: ${safeError}`, { cause: error });
|
||||
}
|
||||
|
||||
if (data.error) {
|
||||
const rawMessage = data.error.message || data.error.status || "unknown";
|
||||
throw new Error(
|
||||
`Gemini API error (${data.error.code}): ${rawMessage.replace(/key=[^&\s]+/giu, "key=***")}`,
|
||||
);
|
||||
}
|
||||
|
||||
const candidate = data.candidates?.[0];
|
||||
const content =
|
||||
candidate?.content?.parts
|
||||
?.map((part) => part.text)
|
||||
.filter(Boolean)
|
||||
.join("\n") ?? "No response";
|
||||
const rawCitations = (candidate?.groundingMetadata?.groundingChunks ?? [])
|
||||
.filter((chunk) => chunk.web?.uri)
|
||||
.map((chunk) => ({
|
||||
url: chunk.web!.uri!,
|
||||
title: chunk.web?.title || undefined,
|
||||
}));
|
||||
|
||||
const citations: Array<{ url: string; title?: string }> = [];
|
||||
for (let index = 0; index < rawCitations.length; index += 10) {
|
||||
const batch = rawCitations.slice(index, index + 10);
|
||||
const resolved = await Promise.all(
|
||||
batch.map(async (citation) => ({
|
||||
...citation,
|
||||
url: await resolveCitationRedirectUrl(citation.url),
|
||||
})),
|
||||
);
|
||||
citations.push(...resolved);
|
||||
}
|
||||
|
||||
return { content, citations };
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export async function executeGeminiSearch(
|
||||
args: Record<string, unknown>,
|
||||
searchConfig?: SearchConfigRecord,
|
||||
): Promise<Record<string, unknown>> {
|
||||
const unsupportedResponse = buildUnsupportedSearchFilterResponse(args, "gemini");
|
||||
if (unsupportedResponse) {
|
||||
return unsupportedResponse;
|
||||
}
|
||||
|
||||
const geminiConfig = resolveGeminiConfig(searchConfig);
|
||||
const apiKey = resolveGeminiRuntimeApiKey(geminiConfig);
|
||||
if (!apiKey) {
|
||||
return {
|
||||
error: "missing_gemini_api_key",
|
||||
message:
|
||||
"web_search (gemini) needs an API key. Set GEMINI_API_KEY in the Gateway environment, or configure tools.web.search.gemini.apiKey.",
|
||||
docs: "https://docs.openclaw.ai/tools/web",
|
||||
};
|
||||
}
|
||||
|
||||
const query = readStringParam(args, "query", { required: true });
|
||||
const count =
|
||||
readNumberParam(args, "count", { integer: true }) ?? searchConfig?.maxResults ?? undefined;
|
||||
const model = resolveGeminiModel(geminiConfig);
|
||||
const cacheKey = buildSearchCacheKey([
|
||||
"gemini",
|
||||
query,
|
||||
resolveSearchCount(count, DEFAULT_SEARCH_COUNT),
|
||||
model,
|
||||
]);
|
||||
const cached = readCachedSearchPayload(cacheKey);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
const start = Date.now();
|
||||
const result = await runGeminiSearch({
|
||||
query,
|
||||
apiKey,
|
||||
model,
|
||||
timeoutSeconds: resolveSearchTimeoutSeconds(searchConfig),
|
||||
});
|
||||
const payload = {
|
||||
query,
|
||||
provider: "gemini",
|
||||
model,
|
||||
tookMs: Date.now() - start,
|
||||
externalContent: {
|
||||
untrusted: true,
|
||||
source: "web_search",
|
||||
provider: "gemini",
|
||||
wrapped: true,
|
||||
},
|
||||
content: wrapWebContent(result.content),
|
||||
citations: result.citations,
|
||||
};
|
||||
writeCachedSearchPayload(cacheKey, payload, resolveSearchCacheTtlMs(searchConfig));
|
||||
return payload;
|
||||
}
|
||||
30
extensions/google/src/gemini-web-search-provider.shared.ts
Normal file
30
extensions/google/src/gemini-web-search-provider.shared.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
export const DEFAULT_GEMINI_WEB_SEARCH_MODEL = "gemini-2.5-flash";
|
||||
|
||||
export type GeminiConfig = {
|
||||
apiKey?: unknown;
|
||||
model?: unknown;
|
||||
};
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function trimToUndefined(value: unknown): string | undefined {
|
||||
return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
|
||||
}
|
||||
|
||||
export function resolveGeminiConfig(searchConfig?: Record<string, unknown>): GeminiConfig {
|
||||
const gemini = searchConfig?.gemini;
|
||||
return isRecord(gemini) ? gemini : {};
|
||||
}
|
||||
|
||||
export function resolveGeminiApiKey(
|
||||
gemini?: GeminiConfig,
|
||||
env: Record<string, string | undefined> = process.env,
|
||||
): string | undefined {
|
||||
return trimToUndefined(gemini?.apiKey) ?? trimToUndefined(env.GEMINI_API_KEY);
|
||||
}
|
||||
|
||||
export function resolveGeminiModel(gemini?: GeminiConfig): string {
|
||||
return trimToUndefined(gemini?.model) ?? DEFAULT_GEMINI_WEB_SEARCH_MODEL;
|
||||
}
|
||||
@@ -1,244 +1,42 @@
|
||||
import { Type } from "@sinclair/typebox";
|
||||
import {
|
||||
buildSearchCacheKey,
|
||||
buildUnsupportedSearchFilterResponse,
|
||||
DEFAULT_SEARCH_COUNT,
|
||||
getScopedCredentialValue,
|
||||
MAX_SEARCH_COUNT,
|
||||
createWebSearchProviderContractFields,
|
||||
mergeScopedSearchConfig,
|
||||
readCachedSearchPayload,
|
||||
readConfiguredSecretString,
|
||||
readNumberParam,
|
||||
readProviderEnvValue,
|
||||
readStringParam,
|
||||
resolveCitationRedirectUrl,
|
||||
resolveProviderWebSearchPluginConfig,
|
||||
resolveSearchCacheTtlMs,
|
||||
resolveSearchCount,
|
||||
resolveSearchTimeoutSeconds,
|
||||
setScopedCredentialValue,
|
||||
setProviderWebSearchPluginConfigValue,
|
||||
type SearchConfigRecord,
|
||||
type WebSearchProviderPlugin,
|
||||
type WebSearchProviderToolDefinition,
|
||||
withTrustedWebSearchEndpoint,
|
||||
wrapWebContent,
|
||||
writeCachedSearchPayload,
|
||||
} from "openclaw/plugin-sdk/provider-web-search";
|
||||
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
|
||||
import { DEFAULT_GOOGLE_API_BASE_URL } from "../api.js";
|
||||
} from "openclaw/plugin-sdk/provider-web-search-config-contract";
|
||||
import { resolveGeminiApiKey, resolveGeminiModel } from "./gemini-web-search-provider.shared.js";
|
||||
|
||||
const DEFAULT_GEMINI_MODEL = "gemini-2.5-flash";
|
||||
const GEMINI_API_BASE = DEFAULT_GOOGLE_API_BASE_URL;
|
||||
|
||||
type GeminiConfig = {
|
||||
apiKey?: string;
|
||||
model?: string;
|
||||
};
|
||||
|
||||
type GeminiGroundingResponse = {
|
||||
candidates?: Array<{
|
||||
content?: {
|
||||
parts?: Array<{
|
||||
text?: string;
|
||||
}>;
|
||||
};
|
||||
groundingMetadata?: {
|
||||
groundingChunks?: Array<{
|
||||
web?: {
|
||||
uri?: string;
|
||||
title?: string;
|
||||
};
|
||||
}>;
|
||||
};
|
||||
}>;
|
||||
error?: {
|
||||
code?: number;
|
||||
message?: string;
|
||||
status?: string;
|
||||
};
|
||||
};
|
||||
|
||||
function resolveGeminiConfig(searchConfig?: SearchConfigRecord): GeminiConfig {
|
||||
const gemini = searchConfig?.gemini;
|
||||
return gemini && typeof gemini === "object" && !Array.isArray(gemini)
|
||||
? (gemini as GeminiConfig)
|
||||
: {};
|
||||
}
|
||||
|
||||
function resolveGeminiApiKey(gemini?: GeminiConfig): string | undefined {
|
||||
return (
|
||||
readConfiguredSecretString(gemini?.apiKey, "tools.web.search.gemini.apiKey") ??
|
||||
readProviderEnvValue(["GEMINI_API_KEY"])
|
||||
);
|
||||
}
|
||||
|
||||
function resolveGeminiModel(gemini?: GeminiConfig): string {
|
||||
const model = normalizeOptionalString(gemini?.model) ?? "";
|
||||
return model || DEFAULT_GEMINI_MODEL;
|
||||
}
|
||||
|
||||
async function runGeminiSearch(params: {
|
||||
query: string;
|
||||
apiKey: string;
|
||||
model: string;
|
||||
timeoutSeconds: number;
|
||||
}): Promise<{ content: string; citations: Array<{ url: string; title?: string }> }> {
|
||||
const endpoint = `${GEMINI_API_BASE}/models/${params.model}:generateContent`;
|
||||
|
||||
return withTrustedWebSearchEndpoint(
|
||||
{
|
||||
url: endpoint,
|
||||
timeoutSeconds: params.timeoutSeconds,
|
||||
init: {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-goog-api-key": params.apiKey,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
contents: [{ parts: [{ text: params.query }] }],
|
||||
tools: [{ google_search: {} }],
|
||||
}),
|
||||
},
|
||||
const GEMINI_CREDENTIAL_PATH = "plugins.entries.google.config.webSearch.apiKey";
|
||||
const GEMINI_TOOL_PARAMETERS = {
|
||||
type: "object",
|
||||
properties: {
|
||||
query: { type: "string", description: "Search query string." },
|
||||
count: {
|
||||
type: "number",
|
||||
description: "Number of results to return (1-10).",
|
||||
minimum: 1,
|
||||
maximum: 10,
|
||||
},
|
||||
async (res) => {
|
||||
if (!res.ok) {
|
||||
const safeDetail = ((await res.text()) || res.statusText).replace(
|
||||
/key=[^&\s]+/gi,
|
||||
"key=***",
|
||||
);
|
||||
throw new Error(`Gemini API error (${res.status}): ${safeDetail}`);
|
||||
}
|
||||
|
||||
let data: GeminiGroundingResponse;
|
||||
try {
|
||||
data = (await res.json()) as GeminiGroundingResponse;
|
||||
} catch (error) {
|
||||
const safeError = String(error).replace(/key=[^&\s]+/gi, "key=***");
|
||||
throw new Error(`Gemini API returned invalid JSON: ${safeError}`, { cause: error });
|
||||
}
|
||||
|
||||
if (data.error) {
|
||||
const rawMessage = data.error.message || data.error.status || "unknown";
|
||||
throw new Error(
|
||||
`Gemini API error (${data.error.code}): ${rawMessage.replace(/key=[^&\s]+/gi, "key=***")}`,
|
||||
);
|
||||
}
|
||||
|
||||
const candidate = data.candidates?.[0];
|
||||
const content =
|
||||
candidate?.content?.parts
|
||||
?.map((part) => part.text)
|
||||
.filter(Boolean)
|
||||
.join("\n") ?? "No response";
|
||||
const rawCitations = (candidate?.groundingMetadata?.groundingChunks ?? [])
|
||||
.filter((chunk) => chunk.web?.uri)
|
||||
.map((chunk) => ({
|
||||
url: chunk.web!.uri!,
|
||||
title: chunk.web?.title || undefined,
|
||||
}));
|
||||
|
||||
const citations: Array<{ url: string; title?: string }> = [];
|
||||
for (let index = 0; index < rawCitations.length; index += 10) {
|
||||
const batch = rawCitations.slice(index, index + 10);
|
||||
const resolved = await Promise.all(
|
||||
batch.map(async (citation) => ({
|
||||
...citation,
|
||||
url: await resolveCitationRedirectUrl(citation.url),
|
||||
})),
|
||||
);
|
||||
citations.push(...resolved);
|
||||
}
|
||||
|
||||
return { content, citations };
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function createGeminiSchema() {
|
||||
return Type.Object({
|
||||
query: Type.String({ description: "Search query string." }),
|
||||
count: Type.Optional(
|
||||
Type.Number({
|
||||
description: "Number of results to return (1-10).",
|
||||
minimum: 1,
|
||||
maximum: MAX_SEARCH_COUNT,
|
||||
}),
|
||||
),
|
||||
country: Type.Optional(Type.String({ description: "Not supported by Gemini." })),
|
||||
language: Type.Optional(Type.String({ description: "Not supported by Gemini." })),
|
||||
freshness: Type.Optional(Type.String({ description: "Not supported by Gemini." })),
|
||||
date_after: Type.Optional(Type.String({ description: "Not supported by Gemini." })),
|
||||
date_before: Type.Optional(Type.String({ description: "Not supported by Gemini." })),
|
||||
});
|
||||
}
|
||||
country: { type: "string", description: "Not supported by Gemini." },
|
||||
language: { type: "string", description: "Not supported by Gemini." },
|
||||
freshness: { type: "string", description: "Not supported by Gemini." },
|
||||
date_after: { type: "string", description: "Not supported by Gemini." },
|
||||
date_before: { type: "string", description: "Not supported by Gemini." },
|
||||
},
|
||||
required: ["query"],
|
||||
} satisfies Record<string, unknown>;
|
||||
|
||||
function createGeminiToolDefinition(
|
||||
searchConfig?: SearchConfigRecord,
|
||||
searchConfig?: Record<string, unknown>,
|
||||
): WebSearchProviderToolDefinition {
|
||||
return {
|
||||
description:
|
||||
"Search the web using Gemini with Google Search grounding. Returns AI-synthesized answers with citations from Google Search.",
|
||||
parameters: createGeminiSchema(),
|
||||
parameters: GEMINI_TOOL_PARAMETERS,
|
||||
execute: async (args) => {
|
||||
const params = args;
|
||||
const unsupportedResponse = buildUnsupportedSearchFilterResponse(params, "gemini");
|
||||
if (unsupportedResponse) {
|
||||
return unsupportedResponse;
|
||||
}
|
||||
|
||||
const geminiConfig = resolveGeminiConfig(searchConfig);
|
||||
const apiKey = resolveGeminiApiKey(geminiConfig);
|
||||
if (!apiKey) {
|
||||
return {
|
||||
error: "missing_gemini_api_key",
|
||||
message:
|
||||
"web_search (gemini) needs an API key. Set GEMINI_API_KEY in the Gateway environment, or configure tools.web.search.gemini.apiKey.",
|
||||
docs: "https://docs.openclaw.ai/tools/web",
|
||||
};
|
||||
}
|
||||
|
||||
const query = readStringParam(params, "query", { required: true });
|
||||
const count =
|
||||
readNumberParam(params, "count", { integer: true }) ??
|
||||
searchConfig?.maxResults ??
|
||||
undefined;
|
||||
const model = resolveGeminiModel(geminiConfig);
|
||||
const cacheKey = buildSearchCacheKey([
|
||||
"gemini",
|
||||
query,
|
||||
resolveSearchCount(count, DEFAULT_SEARCH_COUNT),
|
||||
model,
|
||||
]);
|
||||
const cached = readCachedSearchPayload(cacheKey);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
const start = Date.now();
|
||||
const result = await runGeminiSearch({
|
||||
query,
|
||||
apiKey,
|
||||
model,
|
||||
timeoutSeconds: resolveSearchTimeoutSeconds(searchConfig),
|
||||
});
|
||||
const payload = {
|
||||
query,
|
||||
provider: "gemini",
|
||||
model,
|
||||
tookMs: Date.now() - start,
|
||||
externalContent: {
|
||||
untrusted: true,
|
||||
source: "web_search",
|
||||
provider: "gemini",
|
||||
wrapped: true,
|
||||
},
|
||||
content: wrapWebContent(result.content),
|
||||
citations: result.citations,
|
||||
};
|
||||
writeCachedSearchPayload(cacheKey, payload, resolveSearchCacheTtlMs(searchConfig));
|
||||
return payload;
|
||||
const { executeGeminiSearch } = await import("./gemini-web-search-provider.runtime.js");
|
||||
return await executeGeminiSearch(args, searchConfig);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -255,23 +53,19 @@ export function createGeminiWebSearchProvider(): WebSearchProviderPlugin {
|
||||
signupUrl: "https://aistudio.google.com/apikey",
|
||||
docsUrl: "https://docs.openclaw.ai/tools/web",
|
||||
autoDetectOrder: 20,
|
||||
credentialPath: "plugins.entries.google.config.webSearch.apiKey",
|
||||
inactiveSecretPaths: ["plugins.entries.google.config.webSearch.apiKey"],
|
||||
getCredentialValue: (searchConfig) => getScopedCredentialValue(searchConfig, "gemini"),
|
||||
setCredentialValue: (searchConfigTarget, value) =>
|
||||
setScopedCredentialValue(searchConfigTarget, "gemini", value),
|
||||
getConfiguredCredentialValue: (config) =>
|
||||
resolveProviderWebSearchPluginConfig(config, "google")?.apiKey,
|
||||
setConfiguredCredentialValue: (configTarget, value) => {
|
||||
setProviderWebSearchPluginConfigValue(configTarget, "google", "apiKey", value);
|
||||
},
|
||||
credentialPath: GEMINI_CREDENTIAL_PATH,
|
||||
...createWebSearchProviderContractFields({
|
||||
credentialPath: GEMINI_CREDENTIAL_PATH,
|
||||
searchCredential: { type: "scoped", scopeId: "gemini" },
|
||||
configuredCredential: { pluginId: "google" },
|
||||
}),
|
||||
createTool: (ctx) =>
|
||||
createGeminiToolDefinition(
|
||||
mergeScopedSearchConfig(
|
||||
ctx.searchConfig as SearchConfigRecord | undefined,
|
||||
ctx.searchConfig,
|
||||
"gemini",
|
||||
resolveProviderWebSearchPluginConfig(ctx.config, "google"),
|
||||
) as SearchConfigRecord | undefined,
|
||||
),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
export const DEFAULT_PERPLEXITY_BASE_URL = "https://openrouter.ai/api/v1";
|
||||
export const PERPLEXITY_DIRECT_BASE_URL = "https://api.perplexity.ai";
|
||||
|
||||
const PERPLEXITY_KEY_PREFIXES = ["pplx-"];
|
||||
const OPENROUTER_KEY_PREFIXES = ["sk-or-"];
|
||||
|
||||
export type PerplexityTransport = "search_api" | "chat_completions";
|
||||
export type PerplexityBaseUrlHint = "direct" | "openrouter";
|
||||
export type PerplexityRuntimeTransportContext = {
|
||||
searchConfig?: Record<string, unknown>;
|
||||
resolvedKey?: string;
|
||||
keySource: "config" | "secretRef" | "env" | "missing";
|
||||
fallbackEnvVar?: string;
|
||||
};
|
||||
|
||||
function trimToUndefined(value: unknown): string | undefined {
|
||||
return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
|
||||
}
|
||||
|
||||
function normalizeLowercaseStringOrEmpty(value: unknown): string {
|
||||
return trimToUndefined(value)?.toLowerCase() ?? "";
|
||||
}
|
||||
|
||||
export function inferPerplexityBaseUrlFromApiKey(
|
||||
apiKey?: string,
|
||||
): PerplexityBaseUrlHint | undefined {
|
||||
if (!apiKey) {
|
||||
return undefined;
|
||||
}
|
||||
const normalized = normalizeLowercaseStringOrEmpty(apiKey);
|
||||
if (PERPLEXITY_KEY_PREFIXES.some((prefix) => normalized.startsWith(prefix))) {
|
||||
return "direct";
|
||||
}
|
||||
if (OPENROUTER_KEY_PREFIXES.some((prefix) => normalized.startsWith(prefix))) {
|
||||
return "openrouter";
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function isDirectPerplexityBaseUrl(baseUrl: string): boolean {
|
||||
try {
|
||||
return (
|
||||
normalizeLowercaseStringOrEmpty(new URL(baseUrl.trim()).hostname) === "api.perplexity.ai"
|
||||
);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function resolvePerplexityRuntimeTransport(
|
||||
params: PerplexityRuntimeTransportContext,
|
||||
): PerplexityTransport | undefined {
|
||||
const perplexity = params.searchConfig?.perplexity;
|
||||
const scoped =
|
||||
perplexity && typeof perplexity === "object" && !Array.isArray(perplexity)
|
||||
? (perplexity as { baseUrl?: string; model?: string })
|
||||
: undefined;
|
||||
const configuredBaseUrl = trimToUndefined(scoped?.baseUrl) ?? "";
|
||||
const configuredModel = trimToUndefined(scoped?.model) ?? "";
|
||||
const baseUrl = (() => {
|
||||
if (configuredBaseUrl) {
|
||||
return configuredBaseUrl;
|
||||
}
|
||||
if (params.keySource === "env") {
|
||||
if (params.fallbackEnvVar === "PERPLEXITY_API_KEY") {
|
||||
return PERPLEXITY_DIRECT_BASE_URL;
|
||||
}
|
||||
if (params.fallbackEnvVar === "OPENROUTER_API_KEY") {
|
||||
return DEFAULT_PERPLEXITY_BASE_URL;
|
||||
}
|
||||
}
|
||||
if ((params.keySource === "config" || params.keySource === "secretRef") && params.resolvedKey) {
|
||||
return inferPerplexityBaseUrlFromApiKey(params.resolvedKey) === "openrouter"
|
||||
? DEFAULT_PERPLEXITY_BASE_URL
|
||||
: PERPLEXITY_DIRECT_BASE_URL;
|
||||
}
|
||||
return DEFAULT_PERPLEXITY_BASE_URL;
|
||||
})();
|
||||
return configuredBaseUrl || configuredModel || !isDirectPerplexityBaseUrl(baseUrl)
|
||||
? "chat_completions"
|
||||
: "search_api";
|
||||
}
|
||||
@@ -25,24 +25,24 @@ import {
|
||||
setProviderWebSearchPluginConfigValue,
|
||||
throwWebSearchApiError,
|
||||
type SearchConfigRecord,
|
||||
type WebSearchCredentialResolutionSource,
|
||||
type WebSearchProviderPlugin,
|
||||
type WebSearchProviderToolDefinition,
|
||||
withTrustedWebSearchEndpoint,
|
||||
wrapWebContent,
|
||||
writeCachedSearchPayload,
|
||||
} from "openclaw/plugin-sdk/provider-web-search";
|
||||
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
|
||||
import {
|
||||
normalizeLowercaseStringOrEmpty,
|
||||
normalizeOptionalString,
|
||||
} from "openclaw/plugin-sdk/text-runtime";
|
||||
DEFAULT_PERPLEXITY_BASE_URL,
|
||||
inferPerplexityBaseUrlFromApiKey,
|
||||
isDirectPerplexityBaseUrl,
|
||||
PERPLEXITY_DIRECT_BASE_URL,
|
||||
resolvePerplexityRuntimeTransport,
|
||||
type PerplexityTransport,
|
||||
} from "./perplexity-web-search-provider.shared.js";
|
||||
|
||||
const DEFAULT_PERPLEXITY_BASE_URL = "https://openrouter.ai/api/v1";
|
||||
const PERPLEXITY_DIRECT_BASE_URL = "https://api.perplexity.ai";
|
||||
const PERPLEXITY_SEARCH_ENDPOINT = "https://api.perplexity.ai/search";
|
||||
const DEFAULT_PERPLEXITY_MODEL = "perplexity/sonar-pro";
|
||||
const PERPLEXITY_KEY_PREFIXES = ["pplx-"];
|
||||
const OPENROUTER_KEY_PREFIXES = ["sk-or-"];
|
||||
|
||||
type PerplexityConfig = {
|
||||
apiKey?: string;
|
||||
@@ -50,9 +50,6 @@ type PerplexityConfig = {
|
||||
model?: string;
|
||||
};
|
||||
|
||||
type PerplexityTransport = "search_api" | "chat_completions";
|
||||
type PerplexityBaseUrlHint = "direct" | "openrouter";
|
||||
|
||||
type PerplexitySearchResponse = {
|
||||
choices?: Array<{
|
||||
message?: {
|
||||
@@ -85,20 +82,6 @@ function resolvePerplexityConfig(searchConfig?: SearchConfigRecord): PerplexityC
|
||||
: {};
|
||||
}
|
||||
|
||||
function inferPerplexityBaseUrlFromApiKey(apiKey?: string): PerplexityBaseUrlHint | undefined {
|
||||
if (!apiKey) {
|
||||
return undefined;
|
||||
}
|
||||
const normalized = normalizeLowercaseStringOrEmpty(apiKey);
|
||||
if (PERPLEXITY_KEY_PREFIXES.some((prefix) => normalized.startsWith(prefix))) {
|
||||
return "direct";
|
||||
}
|
||||
if (OPENROUTER_KEY_PREFIXES.some((prefix) => normalized.startsWith(prefix))) {
|
||||
return "openrouter";
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function resolvePerplexityApiKey(perplexity?: PerplexityConfig): {
|
||||
apiKey?: string;
|
||||
source: "config" | "perplexity_env" | "openrouter_env" | "none";
|
||||
@@ -149,16 +132,6 @@ function resolvePerplexityModel(perplexity?: PerplexityConfig): string {
|
||||
return model || DEFAULT_PERPLEXITY_MODEL;
|
||||
}
|
||||
|
||||
function isDirectPerplexityBaseUrl(baseUrl: string): boolean {
|
||||
try {
|
||||
return (
|
||||
normalizeLowercaseStringOrEmpty(new URL(baseUrl.trim()).hostname) === "api.perplexity.ai"
|
||||
);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function resolvePerplexityRequestModel(baseUrl: string, model: string): string {
|
||||
if (!isDirectPerplexityBaseUrl(baseUrl)) {
|
||||
return model;
|
||||
@@ -336,43 +309,6 @@ async function runPerplexitySearch(params: {
|
||||
);
|
||||
}
|
||||
|
||||
function resolveRuntimeTransport(params: {
|
||||
searchConfig?: Record<string, unknown>;
|
||||
resolvedKey?: string;
|
||||
keySource: WebSearchCredentialResolutionSource;
|
||||
fallbackEnvVar?: string;
|
||||
}): PerplexityTransport | undefined {
|
||||
const perplexity = params.searchConfig?.perplexity;
|
||||
const scoped =
|
||||
perplexity && typeof perplexity === "object" && !Array.isArray(perplexity)
|
||||
? (perplexity as { baseUrl?: string; model?: string })
|
||||
: undefined;
|
||||
const configuredBaseUrl = normalizeOptionalString(scoped?.baseUrl) ?? "";
|
||||
const configuredModel = normalizeOptionalString(scoped?.model) ?? "";
|
||||
const baseUrl = (() => {
|
||||
if (configuredBaseUrl) {
|
||||
return configuredBaseUrl;
|
||||
}
|
||||
if (params.keySource === "env") {
|
||||
if (params.fallbackEnvVar === "PERPLEXITY_API_KEY") {
|
||||
return PERPLEXITY_DIRECT_BASE_URL;
|
||||
}
|
||||
if (params.fallbackEnvVar === "OPENROUTER_API_KEY") {
|
||||
return DEFAULT_PERPLEXITY_BASE_URL;
|
||||
}
|
||||
}
|
||||
if ((params.keySource === "config" || params.keySource === "secretRef") && params.resolvedKey) {
|
||||
return inferPerplexityBaseUrlFromApiKey(params.resolvedKey) === "openrouter"
|
||||
? DEFAULT_PERPLEXITY_BASE_URL
|
||||
: PERPLEXITY_DIRECT_BASE_URL;
|
||||
}
|
||||
return DEFAULT_PERPLEXITY_BASE_URL;
|
||||
})();
|
||||
return configuredBaseUrl || configuredModel || !isDirectPerplexityBaseUrl(baseUrl)
|
||||
? "chat_completions"
|
||||
: "search_api";
|
||||
}
|
||||
|
||||
function createPerplexitySchema(transport?: PerplexityTransport) {
|
||||
const querySchema = {
|
||||
query: Type.String({ description: "Search query string." }),
|
||||
@@ -697,7 +633,7 @@ export function createPerplexityWebSearchProvider(): WebSearchProviderPlugin {
|
||||
setProviderWebSearchPluginConfigValue(configTarget, "perplexity", "apiKey", value);
|
||||
},
|
||||
resolveRuntimeMetadata: (ctx) => ({
|
||||
perplexityTransport: resolveRuntimeTransport({
|
||||
perplexityTransport: resolvePerplexityRuntimeTransport({
|
||||
searchConfig: mergeScopedSearchConfig(
|
||||
ctx.searchConfig,
|
||||
"perplexity",
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import {
|
||||
createWebSearchProviderContractFields,
|
||||
mergeScopedSearchConfig,
|
||||
resolveProviderWebSearchPluginConfig,
|
||||
type WebSearchProviderPlugin,
|
||||
} from "openclaw/plugin-sdk/provider-web-search-config-contract";
|
||||
import { resolvePerplexityRuntimeTransport } from "./src/perplexity-web-search-provider.shared.js";
|
||||
|
||||
export function createPerplexityWebSearchProvider(): WebSearchProviderPlugin {
|
||||
const credentialPath = "plugins.entries.perplexity.config.webSearch.apiKey";
|
||||
@@ -23,6 +26,18 @@ export function createPerplexityWebSearchProvider(): WebSearchProviderPlugin {
|
||||
searchCredential: { type: "scoped", scopeId: "perplexity" },
|
||||
configuredCredential: { pluginId: "perplexity" },
|
||||
}),
|
||||
resolveRuntimeMetadata: (ctx) => ({
|
||||
perplexityTransport: resolvePerplexityRuntimeTransport({
|
||||
searchConfig: mergeScopedSearchConfig(
|
||||
ctx.searchConfig,
|
||||
"perplexity",
|
||||
resolveProviderWebSearchPluginConfig(ctx.config, "perplexity"),
|
||||
),
|
||||
resolvedKey: ctx.resolvedCredential?.value,
|
||||
keySource: ctx.resolvedCredential?.source ?? "missing",
|
||||
fallbackEnvVar: ctx.resolvedCredential?.fallbackEnvVar,
|
||||
}),
|
||||
}),
|
||||
createTool: () => null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ vi.mock("./manifest-registry.js", async (importOriginal) => {
|
||||
};
|
||||
});
|
||||
|
||||
import { resolveBundledExplicitRuntimeWebSearchProvidersFromPublicArtifacts as resolveExplicitRuntimeWebSearchProviders } from "./web-provider-public-artifacts.explicit.js";
|
||||
import {
|
||||
resolveBundledWebFetchProvidersFromPublicArtifacts,
|
||||
resolveBundledWebSearchProvidersFromPublicArtifacts,
|
||||
@@ -35,6 +36,16 @@ describe("web provider public artifacts explicit fast path", () => {
|
||||
expect(loadPluginManifestRegistryMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("resolves bundled runtime web search providers by explicit plugin id", () => {
|
||||
const provider = resolveExplicitRuntimeWebSearchProviders({
|
||||
onlyPluginIds: ["google"],
|
||||
})?.[0];
|
||||
|
||||
expect(provider?.pluginId).toBe("google");
|
||||
expect(provider?.createTool({ config: {} as never })).not.toBeNull();
|
||||
expect(loadPluginManifestRegistryMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("resolves bundled web fetch providers by explicit plugin id without manifest scans", () => {
|
||||
const provider = resolveBundledWebFetchProvidersFromPublicArtifacts({
|
||||
bundledAllowlistCompat: true,
|
||||
|
||||
@@ -14,6 +14,7 @@ const WEB_SEARCH_ARTIFACT_CANDIDATES = [
|
||||
"web-search-provider.js",
|
||||
"web-search.js",
|
||||
] as const;
|
||||
const WEB_SEARCH_RUNTIME_ARTIFACT_CANDIDATES = ["web-search-provider.js", "web-search.js"] as const;
|
||||
const WEB_FETCH_ARTIFACT_CANDIDATES = [
|
||||
"web-fetch-contract-api.js",
|
||||
"web-fetch-provider.js",
|
||||
@@ -128,6 +129,28 @@ export function loadBundledWebSearchProviderEntriesFromDir(params: {
|
||||
return providers.map((provider) => ({ ...provider, pluginId: params.pluginId }));
|
||||
}
|
||||
|
||||
export function loadBundledRuntimeWebSearchProviderEntriesFromDir(params: {
|
||||
dirName: string;
|
||||
pluginId: string;
|
||||
}): PluginWebSearchProviderEntry[] | null {
|
||||
const mod = tryLoadBundledPublicArtifactModule({
|
||||
dirName: params.dirName,
|
||||
artifactCandidates: WEB_SEARCH_RUNTIME_ARTIFACT_CANDIDATES,
|
||||
});
|
||||
if (!mod) {
|
||||
return null;
|
||||
}
|
||||
const providers = collectProviderFactories({
|
||||
mod,
|
||||
suffix: "WebSearchProvider",
|
||||
isProvider: isWebSearchProviderPlugin,
|
||||
});
|
||||
if (providers.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return providers.map((provider) => ({ ...provider, pluginId: params.pluginId }));
|
||||
}
|
||||
|
||||
export function loadBundledWebFetchProviderEntriesFromDir(params: {
|
||||
dirName: string;
|
||||
pluginId: string;
|
||||
@@ -167,6 +190,23 @@ export function resolveBundledExplicitWebSearchProvidersFromPublicArtifacts(para
|
||||
return providers;
|
||||
}
|
||||
|
||||
export function resolveBundledExplicitRuntimeWebSearchProvidersFromPublicArtifacts(params: {
|
||||
onlyPluginIds: readonly string[];
|
||||
}): PluginWebSearchProviderEntry[] | null {
|
||||
const providers: PluginWebSearchProviderEntry[] = [];
|
||||
for (const pluginId of normalizeExplicitBundledPluginIds(params.onlyPluginIds)) {
|
||||
const loadedProviders = loadBundledRuntimeWebSearchProviderEntriesFromDir({
|
||||
dirName: pluginId,
|
||||
pluginId,
|
||||
});
|
||||
if (!loadedProviders) {
|
||||
return null;
|
||||
}
|
||||
providers.push(...loadedProviders);
|
||||
}
|
||||
return providers;
|
||||
}
|
||||
|
||||
export function resolveBundledExplicitWebFetchProvidersFromPublicArtifacts(params: {
|
||||
onlyPluginIds: readonly string[];
|
||||
}): PluginWebFetchProviderEntry[] | null {
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { OpenClawConfig } from "../../../src/config/config.js";
|
||||
import { resolveManifestContractOwnerPluginId } from "../../../src/plugins/manifest-registry.js";
|
||||
import { resolveBundledExplicitWebSearchProvidersFromPublicArtifacts } from "../../../src/plugins/web-provider-public-artifacts.explicit.js";
|
||||
import { resolvePluginWebSearchProviders } from "../../../src/plugins/web-search-providers.runtime.js";
|
||||
import {
|
||||
resolveBundledExplicitRuntimeWebSearchProvidersFromPublicArtifacts,
|
||||
resolveBundledExplicitWebSearchProvidersFromPublicArtifacts,
|
||||
} from "../../../src/plugins/web-provider-public-artifacts.explicit.js";
|
||||
|
||||
type ComparableProvider = {
|
||||
pluginId: string;
|
||||
@@ -94,19 +96,16 @@ export function describeBundledWebSearchFastPathContract(pluginId: string) {
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps fast-path provider metadata aligned with bundled public artifacts", async () => {
|
||||
const fastPathProviders = resolvePluginWebSearchProviders({
|
||||
origin: "bundled",
|
||||
onlyPluginIds: [pluginId],
|
||||
mode: "setup",
|
||||
}).filter((provider) => provider.pluginId === pluginId);
|
||||
const bundledProviderEntries =
|
||||
it("keeps fast-path provider metadata aligned with the bundled runtime artifact", async () => {
|
||||
const fastPathProviders =
|
||||
resolveBundledExplicitWebSearchProvidersFromPublicArtifacts({
|
||||
onlyPluginIds: [pluginId],
|
||||
})?.filter((provider) => provider.pluginId === pluginId) ?? [];
|
||||
const bundledProviderEntries =
|
||||
resolveBundledExplicitRuntimeWebSearchProvidersFromPublicArtifacts({
|
||||
onlyPluginIds: [pluginId],
|
||||
})?.filter((entry) => entry.pluginId === pluginId) ?? [];
|
||||
|
||||
expect(bundledProviderEntries.length).toBeGreaterThan(0);
|
||||
|
||||
expect(
|
||||
sortComparableEntries(
|
||||
fastPathProviders.map((provider) =>
|
||||
|
||||
@@ -41,6 +41,19 @@ describe("projects vitest config", () => {
|
||||
expect(normalizeConfigPath(config.test.runner)).toBe("test/non-isolated-runner.ts");
|
||||
});
|
||||
|
||||
it("narrows the contracts lane to targeted contract files", () => {
|
||||
const config = createContractsVitestConfig({}, [
|
||||
"node",
|
||||
"vitest",
|
||||
"run",
|
||||
"src/plugins/contracts/bundled-web-search.google.contract.test.ts",
|
||||
]);
|
||||
|
||||
expect(config.test.include).toEqual([
|
||||
"src/plugins/contracts/bundled-web-search.google.contract.test.ts",
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps the root ui lane aligned with the isolated jsdom setup", () => {
|
||||
const config = createUiVitestConfig();
|
||||
expect(config.test.environment).toBe("jsdom");
|
||||
|
||||
@@ -1,10 +1,25 @@
|
||||
import { defineConfig } from "vitest/config";
|
||||
import { loadPatternListFromEnv, narrowIncludePatternsForCli } from "./vitest.pattern-file.ts";
|
||||
import { nonIsolatedRunnerPath, sharedVitestConfig } from "./vitest.shared.config.ts";
|
||||
|
||||
const base = sharedVitestConfig as Record<string, unknown>;
|
||||
const baseTest = sharedVitestConfig.test ?? {};
|
||||
const contractIncludePatterns = [
|
||||
"src/channels/plugins/contracts/**/*.test.ts",
|
||||
"src/plugins/contracts/**/*.test.ts",
|
||||
];
|
||||
|
||||
export function createContractsVitestConfig() {
|
||||
export function loadContractsIncludePatternsFromEnv(
|
||||
env: Record<string, string | undefined> = process.env,
|
||||
): string[] | null {
|
||||
return loadPatternListFromEnv("OPENCLAW_VITEST_INCLUDE_FILE", env);
|
||||
}
|
||||
|
||||
export function createContractsVitestConfig(
|
||||
env: Record<string, string | undefined> = process.env,
|
||||
argv: string[] = process.argv,
|
||||
) {
|
||||
const cliIncludePatterns = narrowIncludePatternsForCli(contractIncludePatterns, argv);
|
||||
return defineConfig({
|
||||
...base,
|
||||
test: {
|
||||
@@ -16,10 +31,8 @@ export function createContractsVitestConfig() {
|
||||
pool: "forks",
|
||||
runner: nonIsolatedRunnerPath,
|
||||
setupFiles: baseTest.setupFiles ?? [],
|
||||
include: [
|
||||
"src/channels/plugins/contracts/**/*.test.ts",
|
||||
"src/plugins/contracts/**/*.test.ts",
|
||||
],
|
||||
include:
|
||||
loadContractsIncludePatternsFromEnv(env) ?? cliIncludePatterns ?? contractIncludePatterns,
|
||||
passWithNoTests: true,
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user