Tests: avoid memory-search cold plugin loads

This commit is contained in:
Gustavo Madeira Santana
2026-04-17 11:18:02 -04:00
parent 2535331e94
commit d6c90b5af1
9 changed files with 326 additions and 110 deletions

View File

@@ -0,0 +1,121 @@
import type { MusicGenerationProvider } from "openclaw/plugin-sdk/music-generation";
import { isProviderApiKeyConfigured } from "openclaw/plugin-sdk/provider-auth";
import type {
VideoGenerationProvider,
VideoGenerationProviderConfiguredContext,
} from "openclaw/plugin-sdk/video-generation";
export const DEFAULT_GOOGLE_MUSIC_MODEL = "lyria-3-clip-preview";
export const GOOGLE_PRO_MUSIC_MODEL = "lyria-3-pro-preview";
export const GOOGLE_MAX_INPUT_IMAGES = 10;
export const DEFAULT_GOOGLE_VIDEO_MODEL = "veo-3.1-fast-generate-preview";
export const GOOGLE_VIDEO_ALLOWED_DURATION_SECONDS = [4, 6, 8] as const;
export const GOOGLE_VIDEO_MIN_DURATION_SECONDS = GOOGLE_VIDEO_ALLOWED_DURATION_SECONDS[0];
export const GOOGLE_VIDEO_MAX_DURATION_SECONDS =
GOOGLE_VIDEO_ALLOWED_DURATION_SECONDS[GOOGLE_VIDEO_ALLOWED_DURATION_SECONDS.length - 1];
function isGoogleProviderConfigured(
ctx: { agentDir?: string } | VideoGenerationProviderConfiguredContext,
): boolean {
return isProviderApiKeyConfigured({
provider: "google",
agentDir: ctx.agentDir,
});
}
export function createGoogleMusicGenerationProviderMetadata(): Omit<
MusicGenerationProvider,
"generateMusic"
> {
return {
id: "google",
label: "Google",
defaultModel: DEFAULT_GOOGLE_MUSIC_MODEL,
models: [DEFAULT_GOOGLE_MUSIC_MODEL, GOOGLE_PRO_MUSIC_MODEL],
isConfigured: isGoogleProviderConfigured,
capabilities: {
generate: {
maxTracks: 1,
supportsLyrics: true,
supportsInstrumental: true,
supportsFormat: true,
supportedFormatsByModel: {
[DEFAULT_GOOGLE_MUSIC_MODEL]: ["mp3"],
[GOOGLE_PRO_MUSIC_MODEL]: ["mp3", "wav"],
},
},
edit: {
enabled: true,
maxTracks: 1,
maxInputImages: GOOGLE_MAX_INPUT_IMAGES,
supportsLyrics: true,
supportsInstrumental: true,
supportsFormat: true,
supportedFormatsByModel: {
[DEFAULT_GOOGLE_MUSIC_MODEL]: ["mp3"],
[GOOGLE_PRO_MUSIC_MODEL]: ["mp3", "wav"],
},
},
},
};
}
export function createGoogleVideoGenerationProviderMetadata(): Omit<
VideoGenerationProvider,
"generateVideo"
> {
return {
id: "google",
label: "Google",
defaultModel: DEFAULT_GOOGLE_VIDEO_MODEL,
models: [
DEFAULT_GOOGLE_VIDEO_MODEL,
"veo-3.1-generate-preview",
"veo-3.1-lite-generate-preview",
"veo-3.0-fast-generate-001",
"veo-3.0-generate-001",
"veo-2.0-generate-001",
],
isConfigured: isGoogleProviderConfigured,
capabilities: {
generate: {
maxVideos: 1,
maxDurationSeconds: GOOGLE_VIDEO_MAX_DURATION_SECONDS,
supportedDurationSeconds: [...GOOGLE_VIDEO_ALLOWED_DURATION_SECONDS],
aspectRatios: ["16:9", "9:16"],
resolutions: ["720P", "1080P"],
supportsAspectRatio: true,
supportsResolution: true,
supportsSize: true,
supportsAudio: true,
},
imageToVideo: {
enabled: true,
maxVideos: 1,
maxInputImages: 1,
maxDurationSeconds: GOOGLE_VIDEO_MAX_DURATION_SECONDS,
supportedDurationSeconds: [...GOOGLE_VIDEO_ALLOWED_DURATION_SECONDS],
aspectRatios: ["16:9", "9:16"],
resolutions: ["720P", "1080P"],
supportsAspectRatio: true,
supportsResolution: true,
supportsSize: true,
supportsAudio: true,
},
videoToVideo: {
enabled: true,
maxVideos: 1,
maxInputVideos: 1,
maxDurationSeconds: GOOGLE_VIDEO_MAX_DURATION_SECONDS,
supportedDurationSeconds: [...GOOGLE_VIDEO_ALLOWED_DURATION_SECONDS],
aspectRatios: ["16:9", "9:16"],
resolutions: ["720P", "1080P"],
supportsAspectRatio: true,
supportsResolution: true,
supportsSize: true,
supportsAudio: true,
},
},
};
}

View File

@@ -1,17 +1,23 @@
import type { ImageGenerationProvider } from "openclaw/plugin-sdk/image-generation";
import type { MediaUnderstandingProvider } from "openclaw/plugin-sdk/media-understanding";
import type { MusicGenerationProvider } from "openclaw/plugin-sdk/music-generation";
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
import type { VideoGenerationProvider } from "openclaw/plugin-sdk/video-generation";
import { buildGoogleGeminiCliBackend } from "./cli-backend.js";
import { registerGoogleGeminiCliProvider } from "./gemini-cli-provider.js";
import {
createGoogleMusicGenerationProviderMetadata,
createGoogleVideoGenerationProviderMetadata,
} from "./generation-provider-metadata.js";
import { geminiMemoryEmbeddingProviderAdapter } from "./memory-embedding-adapter.js";
import { buildGoogleMusicGenerationProvider } from "./music-generation-provider.js";
import { registerGoogleProvider } from "./provider-registration.js";
import { buildGoogleSpeechProvider } from "./speech-provider.js";
import { createGeminiWebSearchProvider } from "./src/gemini-web-search-provider.js";
import { buildGoogleVideoGenerationProvider } from "./video-generation-provider.js";
let googleImageGenerationProviderPromise: Promise<ImageGenerationProvider> | null = null;
let googleMediaUnderstandingProviderPromise: Promise<MediaUnderstandingProvider> | null = null;
let googleMusicGenerationProviderPromise: Promise<MusicGenerationProvider> | null = null;
let googleVideoGenerationProviderPromise: Promise<VideoGenerationProvider> | null = null;
type GoogleMediaUnderstandingProvider = Required<
Pick<
@@ -38,6 +44,24 @@ async function loadGoogleMediaUnderstandingProvider(): Promise<MediaUnderstandin
return await googleMediaUnderstandingProviderPromise;
}
async function loadGoogleMusicGenerationProvider(): Promise<MusicGenerationProvider> {
if (!googleMusicGenerationProviderPromise) {
googleMusicGenerationProviderPromise = import("./music-generation-provider.js").then((mod) =>
mod.buildGoogleMusicGenerationProvider(),
);
}
return await googleMusicGenerationProviderPromise;
}
async function loadGoogleVideoGenerationProvider(): Promise<VideoGenerationProvider> {
if (!googleVideoGenerationProviderPromise) {
googleVideoGenerationProviderPromise = import("./video-generation-provider.js").then((mod) =>
mod.buildGoogleVideoGenerationProvider(),
);
}
return await googleVideoGenerationProviderPromise;
}
async function loadGoogleRequiredMediaUnderstandingProvider(): Promise<GoogleMediaUnderstandingProvider> {
const provider = await loadGoogleMediaUnderstandingProvider();
if (
@@ -104,6 +128,22 @@ function createLazyGoogleMediaUnderstandingProvider(): MediaUnderstandingProvide
};
}
function createLazyGoogleMusicGenerationProvider(): MusicGenerationProvider {
return {
...createGoogleMusicGenerationProviderMetadata(),
generateMusic: async (...args) =>
await (await loadGoogleMusicGenerationProvider()).generateMusic(...args),
};
}
function createLazyGoogleVideoGenerationProvider(): VideoGenerationProvider {
return {
...createGoogleVideoGenerationProviderMetadata(),
generateVideo: async (...args) =>
await (await loadGoogleVideoGenerationProvider()).generateVideo(...args),
};
}
export default definePluginEntry({
id: "google",
name: "Google Plugin",
@@ -115,9 +155,9 @@ export default definePluginEntry({
api.registerMemoryEmbeddingProvider(geminiMemoryEmbeddingProviderAdapter);
api.registerImageGenerationProvider(createLazyGoogleImageGenerationProvider());
api.registerMediaUnderstandingProvider(createLazyGoogleMediaUnderstandingProvider());
api.registerMusicGenerationProvider(buildGoogleMusicGenerationProvider());
api.registerMusicGenerationProvider(createLazyGoogleMusicGenerationProvider());
api.registerSpeechProvider(buildGoogleSpeechProvider());
api.registerVideoGenerationProvider(buildGoogleVideoGenerationProvider());
api.registerVideoGenerationProvider(createLazyGoogleVideoGenerationProvider());
api.registerWebSearchProvider(createGeminiWebSearchProvider());
},
});

View File

@@ -5,15 +5,17 @@ import type {
MusicGenerationProvider,
MusicGenerationRequest,
} from "openclaw/plugin-sdk/music-generation";
import { isProviderApiKeyConfigured } from "openclaw/plugin-sdk/provider-auth";
import { resolveApiKeyForProvider } from "openclaw/plugin-sdk/provider-auth-runtime";
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
import { normalizeGoogleApiBaseUrl } from "./api.js";
import {
createGoogleMusicGenerationProviderMetadata,
DEFAULT_GOOGLE_MUSIC_MODEL,
GOOGLE_MAX_INPUT_IMAGES,
GOOGLE_PRO_MUSIC_MODEL,
} from "./generation-provider-metadata.js";
const DEFAULT_GOOGLE_MUSIC_MODEL = "lyria-3-clip-preview";
const GOOGLE_PRO_MUSIC_MODEL = "lyria-3-pro-preview";
const DEFAULT_TIMEOUT_MS = 180_000;
const GOOGLE_MAX_INPUT_IMAGES = 10;
type GoogleInlineDataPart = {
mimeType?: string;
@@ -99,39 +101,7 @@ function extractTracks(params: { payload: GoogleGenerateMusicResponse; model: st
export function buildGoogleMusicGenerationProvider(): MusicGenerationProvider {
return {
id: "google",
label: "Google",
defaultModel: DEFAULT_GOOGLE_MUSIC_MODEL,
models: [DEFAULT_GOOGLE_MUSIC_MODEL, GOOGLE_PRO_MUSIC_MODEL],
isConfigured: ({ agentDir }) =>
isProviderApiKeyConfigured({
provider: "google",
agentDir,
}),
capabilities: {
generate: {
maxTracks: 1,
supportsLyrics: true,
supportsInstrumental: true,
supportsFormat: true,
supportedFormatsByModel: {
[DEFAULT_GOOGLE_MUSIC_MODEL]: ["mp3"],
[GOOGLE_PRO_MUSIC_MODEL]: ["mp3", "wav"],
},
},
edit: {
enabled: true,
maxTracks: 1,
maxInputImages: GOOGLE_MAX_INPUT_IMAGES,
supportsLyrics: true,
supportsInstrumental: true,
supportsFormat: true,
supportedFormatsByModel: {
[DEFAULT_GOOGLE_MUSIC_MODEL]: ["mp3"],
[GOOGLE_PRO_MUSIC_MODEL]: ["mp3", "wav"],
},
},
},
...createGoogleMusicGenerationProviderMetadata(),
async generateMusic(req) {
if ((req.inputImages?.length ?? 0) > GOOGLE_MAX_INPUT_IMAGES) {
throw new Error(

View File

@@ -1,7 +1,6 @@
import { mkdtemp, readFile, rm } from "node:fs/promises";
import path from "node:path";
import { GoogleGenAI } from "@google/genai";
import { isProviderApiKeyConfigured } from "openclaw/plugin-sdk/provider-auth";
import { resolveApiKeyForProvider } from "openclaw/plugin-sdk/provider-auth-runtime";
import {
createProviderOperationDeadline,
@@ -16,15 +15,17 @@ import type {
VideoGenerationRequest,
} from "openclaw/plugin-sdk/video-generation";
import { normalizeGoogleApiBaseUrl } from "./api.js";
import {
createGoogleVideoGenerationProviderMetadata,
DEFAULT_GOOGLE_VIDEO_MODEL,
GOOGLE_VIDEO_ALLOWED_DURATION_SECONDS,
GOOGLE_VIDEO_MAX_DURATION_SECONDS,
GOOGLE_VIDEO_MIN_DURATION_SECONDS,
} from "./generation-provider-metadata.js";
const DEFAULT_GOOGLE_VIDEO_MODEL = "veo-3.1-fast-generate-preview";
const DEFAULT_TIMEOUT_MS = 180_000;
const POLL_INTERVAL_MS = 10_000;
const MAX_POLL_ATTEMPTS = 90;
const GOOGLE_VIDEO_ALLOWED_DURATION_SECONDS = [4, 6, 8] as const;
const GOOGLE_VIDEO_MIN_DURATION_SECONDS = GOOGLE_VIDEO_ALLOWED_DURATION_SECONDS[0];
const GOOGLE_VIDEO_MAX_DURATION_SECONDS =
GOOGLE_VIDEO_ALLOWED_DURATION_SECONDS[GOOGLE_VIDEO_ALLOWED_DURATION_SECONDS.length - 1];
function resolveConfiguredGoogleVideoBaseUrl(req: VideoGenerationRequest): string | undefined {
const configured = normalizeOptionalString(req.cfg?.models?.providers?.google?.baseUrl);
@@ -151,61 +152,7 @@ async function downloadGeneratedVideo(params: {
export function buildGoogleVideoGenerationProvider(): VideoGenerationProvider {
return {
id: "google",
label: "Google",
defaultModel: DEFAULT_GOOGLE_VIDEO_MODEL,
models: [
DEFAULT_GOOGLE_VIDEO_MODEL,
"veo-3.1-generate-preview",
"veo-3.1-lite-generate-preview",
"veo-3.0-fast-generate-001",
"veo-3.0-generate-001",
"veo-2.0-generate-001",
],
isConfigured: ({ agentDir }) =>
isProviderApiKeyConfigured({
provider: "google",
agentDir,
}),
capabilities: {
generate: {
maxVideos: 1,
maxDurationSeconds: GOOGLE_VIDEO_MAX_DURATION_SECONDS,
supportedDurationSeconds: GOOGLE_VIDEO_ALLOWED_DURATION_SECONDS,
aspectRatios: ["16:9", "9:16"],
resolutions: ["720P", "1080P"],
supportsAspectRatio: true,
supportsResolution: true,
supportsSize: true,
supportsAudio: true,
},
imageToVideo: {
enabled: true,
maxVideos: 1,
maxInputImages: 1,
maxDurationSeconds: GOOGLE_VIDEO_MAX_DURATION_SECONDS,
supportedDurationSeconds: GOOGLE_VIDEO_ALLOWED_DURATION_SECONDS,
aspectRatios: ["16:9", "9:16"],
resolutions: ["720P", "1080P"],
supportsAspectRatio: true,
supportsResolution: true,
supportsSize: true,
supportsAudio: true,
},
videoToVideo: {
enabled: true,
maxVideos: 1,
maxInputVideos: 1,
maxDurationSeconds: GOOGLE_VIDEO_MAX_DURATION_SECONDS,
supportedDurationSeconds: GOOGLE_VIDEO_ALLOWED_DURATION_SECONDS,
aspectRatios: ["16:9", "9:16"],
resolutions: ["720P", "1080P"],
supportsAspectRatio: true,
supportsResolution: true,
supportsSize: true,
supportsAudio: true,
},
},
...createGoogleVideoGenerationProviderMetadata(),
async generateVideo(req) {
if ((req.inputImages?.length ?? 0) > 1) {
throw new Error("Google video generation supports at most one input image.");

View File

@@ -8,7 +8,7 @@ import {
normalizeMemoryMultimodalSettings,
type MemoryMultimodalSettings,
} from "../memory-host-sdk/multimodal.js";
import { getMemoryEmbeddingProvider } from "../plugins/memory-embedding-provider-runtime.js";
import { getMemoryEmbeddingProvider } from "../plugins/memory-embedding-providers.js";
import { clampInt, clampNumber, resolveUserPath } from "../utils.js";
import { resolveAgentConfig } from "./agent-scope.js";
@@ -388,9 +388,12 @@ export function resolveMemorySearchConfig(
const multimodalActive = isMemoryMultimodalEnabled(resolved.multimodal);
const multimodalProvider =
resolved.provider === "auto" ? undefined : getMemoryEmbeddingProvider(resolved.provider);
// Config resolution is a startup/doctor hot path; only validate adapters
// already registered by the active runtime instead of cold-loading plugins.
if (
multimodalActive &&
!(multimodalProvider?.supportsMultimodalEmbeddings?.({ model: resolved.model }) ?? false)
multimodalProvider &&
!(multimodalProvider.supportsMultimodalEmbeddings?.({ model: resolved.model }) ?? false)
) {
throw new Error(
"agents.*.memorySearch.multimodal requires a provider adapter that supports multimodal embeddings for the configured model.",

View File

@@ -47,6 +47,7 @@ vi.mock("./bundled-compat.js", () => ({
}));
let resolvePluginCapabilityProviders: typeof import("./capability-provider-runtime.js").resolvePluginCapabilityProviders;
let resolvePluginCapabilityProvider: typeof import("./capability-provider-runtime.js").resolvePluginCapabilityProvider;
function expectResolvedCapabilityProviderIds(providers: Array<{ id: string }>, expected: string[]) {
expect(providers.map((provider) => provider.id)).toEqual(expected);
@@ -149,7 +150,8 @@ function expectCompatChainApplied(params: {
describe("resolvePluginCapabilityProviders", () => {
beforeAll(async () => {
({ resolvePluginCapabilityProviders } = await import("./capability-provider-runtime.js"));
({ resolvePluginCapabilityProvider, resolvePluginCapabilityProviders } =
await import("./capability-provider-runtime.js"));
});
beforeEach(() => {
@@ -310,4 +312,68 @@ describe("resolvePluginCapabilityProviders", () => {
});
expect(mocks.resolveRuntimePluginRegistry).toHaveBeenCalledWith({ config: compatConfig });
});
it("loads only the bundled owner plugin for a targeted provider lookup", () => {
const cfg = { plugins: { allow: ["custom-plugin"] } } as OpenClawConfig;
const allowlistCompat = {
plugins: {
allow: ["custom-plugin", "google"],
},
} as OpenClawConfig;
const enablementCompat = {
plugins: {
allow: ["custom-plugin", "google"],
entries: { google: { enabled: true } },
},
};
const loaded = createEmptyPluginRegistry();
loaded.memoryEmbeddingProviders.push({
pluginId: "google",
pluginName: "google",
source: "test",
provider: {
id: "gemini",
create: async () => ({ provider: null }),
},
} as never);
mocks.loadPluginManifestRegistry.mockReturnValue({
plugins: [
{
id: "google",
origin: "bundled",
contracts: { memoryEmbeddingProviders: ["gemini"] },
},
{
id: "openai",
origin: "bundled",
contracts: { memoryEmbeddingProviders: ["openai"] },
},
] as never,
diagnostics: [],
});
mocks.withBundledPluginEnablementCompat.mockReturnValue(enablementCompat);
mocks.withBundledPluginVitestCompat.mockReturnValue(enablementCompat);
mocks.resolveRuntimePluginRegistry.mockImplementation((params?: unknown) =>
params === undefined ? undefined : loaded,
);
const provider = resolvePluginCapabilityProvider({
key: "memoryEmbeddingProviders",
providerId: "gemini",
cfg,
});
expect(provider?.id).toBe("gemini");
expect(mocks.withBundledPluginAllowlistCompat).toHaveBeenCalledWith({
config: cfg,
pluginIds: ["google"],
});
expect(mocks.withBundledPluginEnablementCompat).toHaveBeenCalledWith({
config: allowlistCompat,
pluginIds: ["google"],
});
expect(mocks.resolveRuntimePluginRegistry).toHaveBeenCalledWith({
config: enablementCompat,
});
});
});

View File

@@ -45,6 +45,7 @@ const CAPABILITY_CONTRACT_KEY: Record<CapabilityProviderRegistryKey, CapabilityC
function resolveBundledCapabilityCompatPluginIds(params: {
key: CapabilityProviderRegistryKey;
cfg?: OpenClawConfig;
providerId?: string;
}): string[] {
const contractKey = CAPABILITY_CONTRACT_KEY[params.key];
return loadPluginManifestRegistry({
@@ -52,7 +53,10 @@ function resolveBundledCapabilityCompatPluginIds(params: {
env: process.env,
})
.plugins.filter(
(plugin) => plugin.origin === "bundled" && (plugin.contracts?.[contractKey]?.length ?? 0) > 0,
(plugin) =>
plugin.origin === "bundled" &&
(plugin.contracts?.[contractKey]?.length ?? 0) > 0 &&
(!params.providerId || (plugin.contracts?.[contractKey] ?? []).includes(params.providerId)),
)
.map((plugin) => plugin.id)
.toSorted((left, right) => left.localeCompare(right));
@@ -61,8 +65,9 @@ function resolveBundledCapabilityCompatPluginIds(params: {
function resolveCapabilityProviderConfig(params: {
key: CapabilityProviderRegistryKey;
cfg?: OpenClawConfig;
pluginIds?: string[];
}) {
const pluginIds = resolveBundledCapabilityCompatPluginIds(params);
const pluginIds = params.pluginIds ?? resolveBundledCapabilityCompatPluginIds(params);
const allowlistCompat = withBundledPluginAllowlistCompat({
config: params.cfg,
pluginIds,
@@ -78,6 +83,51 @@ function resolveCapabilityProviderConfig(params: {
});
}
function findProviderById<K extends CapabilityProviderRegistryKey>(
entries: PluginRegistry[K],
providerId: string,
): CapabilityProviderForKey<K> | undefined {
const providerEntries = entries as unknown as Array<{
provider: CapabilityProviderForKey<K> & { id?: unknown };
}>;
for (const entry of providerEntries) {
if (entry.provider.id === providerId) {
return entry.provider;
}
}
return undefined;
}
export function resolvePluginCapabilityProvider<K extends CapabilityProviderRegistryKey>(params: {
key: K;
providerId: string;
cfg?: OpenClawConfig;
}): CapabilityProviderForKey<K> | undefined {
const activeRegistry = resolveRuntimePluginRegistry();
const activeProvider = findProviderById(activeRegistry?.[params.key] ?? [], params.providerId);
if (activeProvider) {
return activeProvider;
}
const pluginIds = resolveBundledCapabilityCompatPluginIds({
key: params.key,
cfg: params.cfg,
providerId: params.providerId,
});
if (pluginIds.length === 0) {
return undefined;
}
const compatConfig = resolveCapabilityProviderConfig({
key: params.key,
cfg: params.cfg,
pluginIds,
});
const loadOptions = compatConfig === undefined ? undefined : { config: compatConfig };
const registry = resolveRuntimePluginRegistry(loadOptions);
return findProviderById(registry?.[params.key] ?? [], params.providerId);
}
export function resolvePluginCapabilityProviders<K extends CapabilityProviderRegistryKey>(params: {
key: K;
cfg?: OpenClawConfig;

View File

@@ -9,9 +9,13 @@ const mocks = vi.hoisted(() => ({
resolvePluginCapabilityProviders: vi.fn<
typeof import("./capability-provider-runtime.js").resolvePluginCapabilityProviders
>(() => []),
resolvePluginCapabilityProvider: vi.fn<
typeof import("./capability-provider-runtime.js").resolvePluginCapabilityProvider
>(() => undefined),
}));
vi.mock("./capability-provider-runtime.js", () => ({
resolvePluginCapabilityProvider: mocks.resolvePluginCapabilityProvider,
resolvePluginCapabilityProviders: mocks.resolvePluginCapabilityProviders,
}));
@@ -28,6 +32,8 @@ beforeEach(async () => {
clearMemoryEmbeddingProviders();
mocks.resolvePluginCapabilityProviders.mockReset();
mocks.resolvePluginCapabilityProviders.mockReturnValue([]);
mocks.resolvePluginCapabilityProvider.mockReset();
mocks.resolvePluginCapabilityProvider.mockReturnValue(undefined);
runtimeModule = await import("./memory-embedding-provider-runtime.js");
});
@@ -53,12 +59,18 @@ describe("memory embedding provider runtime resolution", () => {
it("falls back to declared capability adapters when the registry is cold", () => {
mocks.resolvePluginCapabilityProviders.mockReturnValue([createCapabilityAdapter("ollama")]);
mocks.resolvePluginCapabilityProvider.mockReturnValue(createCapabilityAdapter("ollama"));
expect(runtimeModule.listMemoryEmbeddingProviders().map((adapter) => adapter.id)).toEqual([
"ollama",
]);
expect(runtimeModule.getMemoryEmbeddingProvider("ollama")?.id).toBe("ollama");
expect(mocks.resolvePluginCapabilityProviders).toHaveBeenCalledTimes(2);
expect(mocks.resolvePluginCapabilityProviders).toHaveBeenCalledTimes(1);
expect(mocks.resolvePluginCapabilityProvider).toHaveBeenCalledWith({
key: "memoryEmbeddingProviders",
providerId: "ollama",
cfg: undefined,
});
});
it("prefers registered adapters over declared capability fallback adapters with the same id", () => {

View File

@@ -1,5 +1,8 @@
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { resolvePluginCapabilityProviders } from "./capability-provider-runtime.js";
import {
resolvePluginCapabilityProvider,
resolvePluginCapabilityProviders,
} from "./capability-provider-runtime.js";
import {
getRegisteredMemoryEmbeddingProvider,
listRegisteredMemoryEmbeddingProviders,
@@ -35,5 +38,9 @@ export function getMemoryEmbeddingProvider(
if (registered) {
return registered.adapter;
}
return listMemoryEmbeddingProviders(cfg).find((adapter) => adapter.id === id);
return resolvePluginCapabilityProvider({
key: "memoryEmbeddingProviders",
providerId: id,
cfg,
});
}