test(plugins): trim contract helper runtime boot

This commit is contained in:
Vincent Koc
2026-04-17 14:15:41 -07:00
parent 48c4a026dd
commit 8b5030447a
6 changed files with 109 additions and 44 deletions

View File

@@ -1,28 +1 @@
import {
createWebSearchProviderContractFields,
type WebSearchProviderPlugin,
} from "openclaw/plugin-sdk/provider-web-search-config-contract";
export function createGeminiWebSearchProvider(): WebSearchProviderPlugin {
const credentialPath = "plugins.entries.google.config.webSearch.apiKey";
return {
id: "gemini",
label: "Gemini (Google Search)",
hint: "Requires Google Gemini API key · Google Search grounding",
onboardingScopes: ["text-inference"],
credentialLabel: "Google Gemini API key",
envVars: ["GEMINI_API_KEY"],
placeholder: "AIza...",
signupUrl: "https://aistudio.google.com/apikey",
docsUrl: "https://docs.openclaw.ai/tools/web",
autoDetectOrder: 20,
credentialPath,
...createWebSearchProviderContractFields({
credentialPath,
searchCredential: { type: "scoped", scopeId: "gemini" },
configuredCredential: { pluginId: "google" },
}),
createTool: () => null,
};
}
export { createGeminiWebSearchProvider } from "./src/gemini-web-search-provider.js";

View File

@@ -228,6 +228,42 @@ describe("plugin contract registry scoped retries", () => {
expect(loadBundledCapabilityRuntimeRegistry).not.toHaveBeenCalled();
});
it("uses web search public artifacts before falling back to the bundled runtime registry", async () => {
const loadBundledCapabilityRuntimeRegistry = vi.fn(() => {
throw new Error(
"web search contract vitest fast path should not hit bundled runtime registry",
);
});
const loadVitestWebSearchProviderContractRegistry = vi.fn(() => [
{
pluginId: "google",
provider: {
id: "gemini",
label: "Gemini",
credentialPath: "plugins.entries.google.config.webSearch.apiKey",
} as WebSearchProviderPlugin,
credentialValue: "AIzaSyDUMMY",
},
]);
vi.doMock("../bundled-capability-runtime.js", () => ({
loadBundledCapabilityRuntimeRegistry,
}));
vi.doMock("./web-provider-vitest-registry.js", () => ({
loadVitestWebSearchProviderContractRegistry,
}));
const { resolveWebSearchProviderContractEntriesForPluginId } = await import("./registry.js");
expect(
resolveWebSearchProviderContractEntriesForPluginId("google").map(
(entry) => entry.provider.id,
),
).toEqual(["gemini"]);
expect(loadVitestWebSearchProviderContractRegistry).toHaveBeenCalledTimes(1);
expect(loadBundledCapabilityRuntimeRegistry).not.toHaveBeenCalled();
});
it("retries web fetch provider loads after a transient plugin-scoped runtime error", async () => {
const loadBundledCapabilityRuntimeRegistry = vi
.fn()

View File

@@ -28,6 +28,7 @@ import {
loadVitestSpeechProviderContractRegistry,
loadVitestVideoGenerationProviderContractRegistry,
} from "./speech-vitest-registry.js";
import { loadVitestWebSearchProviderContractRegistry } from "./web-provider-vitest-registry.js";
type BundledCapabilityRuntimeRegistry = ReturnType<typeof loadBundledCapabilityRuntimeRegistry>;
type CapabilityContractEntry<T> = {
@@ -474,15 +475,23 @@ export function resolveWebFetchProviderContractEntriesForPluginId(
function loadWebSearchProviderContractRegistry(): WebSearchProviderContractEntry[] {
if (!webSearchProviderContractRegistryCache) {
const registry = loadBundledCapabilityRuntimeRegistry({
pluginIds: resolveBundledManifestContractPluginIds("webSearchProviders"),
pluginSdkResolution: "dist",
});
webSearchProviderContractRegistryCache = registry.webSearchProviders.map((entry) => ({
pluginId: entry.pluginId,
provider: entry.provider,
credentialValue: resolveWebSearchCredentialValue(entry.provider),
}));
const vitestEntries = process.env.VITEST ? loadVitestWebSearchProviderContractRegistry() : [];
const coveredPluginIds = new Set(vitestEntries.map((entry) => entry.pluginId));
const remainingPluginIds = resolveBundledManifestContractPluginIds("webSearchProviders").filter(
(pluginId) => !coveredPluginIds.has(pluginId),
);
const runtimeEntries =
remainingPluginIds.length > 0
? loadBundledCapabilityRuntimeRegistry({
pluginIds: remainingPluginIds,
pluginSdkResolution: "dist",
}).webSearchProviders.map((entry) => ({
pluginId: entry.pluginId,
provider: entry.provider,
credentialValue: resolveWebSearchCredentialValue(entry.provider),
}))
: [];
webSearchProviderContractRegistryCache = [...vitestEntries, ...runtimeEntries];
}
return webSearchProviderContractRegistryCache;
}
@@ -503,6 +512,16 @@ export function resolveWebSearchProviderContractEntriesForPluginId(
return cached;
}
if (process.env.VITEST) {
const vitestEntries = loadVitestWebSearchProviderContractRegistry().filter(
(entry) => entry.pluginId === pluginId,
);
if (vitestEntries.length > 0) {
cache.set(pluginId, vitestEntries);
return vitestEntries;
}
}
const entries = loadScopedCapabilityRuntimeRegistryEntries({
pluginId,
capabilityLabel: "web search provider",

View File

@@ -0,0 +1,21 @@
import { createGeminiWebSearchProvider } from "../../../extensions/google/web-search-contract-api.js";
import type { WebSearchProviderPlugin } from "../types.js";
export type WebSearchProviderContractEntry = {
pluginId: string;
provider: WebSearchProviderPlugin;
credentialValue: unknown;
};
let webSearchProviderContractRegistryCache: WebSearchProviderContractEntry[] | null = null;
export function loadVitestWebSearchProviderContractRegistry(): WebSearchProviderContractEntry[] {
webSearchProviderContractRegistryCache ??= [
{
pluginId: "google",
provider: createGeminiWebSearchProvider(),
credentialValue: "AIzaSyDUMMY",
},
];
return webSearchProviderContractRegistryCache;
}

View File

@@ -150,3 +150,18 @@ export function resolveRelativeBundledPluginPublicModuleId(params: {
.replaceAll(path.sep, "/");
return relativePath.startsWith(".") ? relativePath : `./${relativePath}`;
}
export function resolveRelativeExtensionPublicModuleId(params: {
fromModuleUrl: string;
dirName: string;
artifactBasename: string;
}): string {
const fromFilePath = fileURLToPath(params.fromModuleUrl);
const targetPath = resolveVitestSourceModulePath(
path.resolve(OPENCLAW_PACKAGE_ROOT, "extensions", params.dirName, params.artifactBasename),
);
const relativePath = path
.relative(path.dirname(fromFilePath), targetPath)
.replaceAll(path.sep, "/");
return relativePath.startsWith(".") ? relativePath : `./${relativePath}`;
}

View File

@@ -1,17 +1,23 @@
import type { AssistantMessage } from "@mariozechner/pi-ai";
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../../../src/config/config.js";
import { loadBundledPluginPublicSurfaceModuleSync } from "../../../src/plugin-sdk/facade-loader.js";
import { __testing as pluginLoaderTesting } from "../../../src/plugins/loader.js";
import { createEmptyPluginRegistry } from "../../../src/plugins/registry-empty.js";
import { setActivePluginRegistry } from "../../../src/plugins/runtime.js";
import type { SpeechProviderPlugin } from "../../../src/plugins/types.js";
import { resolveRelativeExtensionPublicModuleId } from "../../../src/test-utils/bundled-plugin-public-surface.js";
import { withEnv } from "../../../src/test-utils/env.js";
import { summarizeText as summarizeTextCore } from "../../../src/tts/tts-core.js";
import type { ResolvedTtsConfig } from "../../../src/tts/tts-types.js";
type TtsRuntimeModule = typeof import("../../../src/tts/tts.js");
const speechCoreRuntimeApiModuleId = resolveRelativeExtensionPublicModuleId({
fromModuleUrl: import.meta.url,
dirName: "speech-core",
artifactBasename: "runtime-api.js",
});
let ttsRuntime: TtsRuntimeModule;
let ttsRuntimePromise: Promise<TtsRuntimeModule> | null = null;
let ttsRuntimeInitialized = false;
@@ -389,12 +395,7 @@ function buildTestGoogleSpeechProvider(): SpeechProviderPlugin {
}
async function loadTtsRuntime(): Promise<TtsRuntimeModule> {
ttsRuntimePromise ??= Promise.resolve(
loadBundledPluginPublicSurfaceModuleSync<TtsRuntimeModule>({
dirName: "speech-core",
artifactBasename: "runtime-api.js",
}),
);
ttsRuntimePromise ??= import(speechCoreRuntimeApiModuleId) as Promise<TtsRuntimeModule>;
return await ttsRuntimePromise;
}