Files
openclaw/src/plugins/registry.provider-like.test.ts
2026-05-11 05:04:44 +01:00

200 lines
6.3 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { createPluginRecord } from "./loader-records.js";
import { createPluginRegistry } from "./registry.js";
import type { PluginRuntime } from "./runtime/types.js";
function createTestRegistry() {
return createPluginRegistry({
logger: {
info() {},
warn() {},
error() {},
debug() {},
},
runtime: {} as PluginRuntime,
activateGlobalSideEffects: false,
});
}
function createCatalogModel(id: string, name: string) {
return {
id,
name,
reasoning: false,
input: ["text" as const],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 8192,
maxTokens: 2048,
};
}
describe("plugin registry provider-like registrations", () => {
it("captures unified model catalog provider registrations", () => {
const pluginRegistry = createTestRegistry();
const record = createPluginRecord({
id: "catalog-owner",
name: "Catalog Owner",
source: "/tmp/catalog-owner/index.js",
origin: "global",
enabled: true,
configSchema: false,
});
pluginRegistry.registerModelCatalogProvider(record, {
provider: "catalog-provider",
kinds: ["text", "video_generation"],
staticCatalog: () => [
{
kind: "text",
provider: "catalog-provider",
model: "catalog-model",
source: "static",
},
],
});
expect(pluginRegistry.registry.modelCatalogProviders).toHaveLength(1);
const catalogRegistration = pluginRegistry.registry.modelCatalogProviders[0];
expect(catalogRegistration?.pluginId).toBe("catalog-owner");
expect(catalogRegistration?.provider.provider).toBe("catalog-provider");
expect(catalogRegistration?.provider.kinds).toEqual(["text", "video_generation"]);
});
it("publishes text catalog rows for registered provider catalog hooks", async () => {
const pluginRegistry = createTestRegistry();
const record = createPluginRecord({
id: "text-owner",
name: "Text Owner",
source: "/tmp/text-owner/index.js",
origin: "global",
enabled: true,
configSchema: false,
});
pluginRegistry.registerProvider(record, {
id: "text-provider",
label: "Text Provider",
auth: [],
catalog: {
run: async () => ({
provider: {
baseUrl: "https://text.example/v1",
models: [createCatalogModel("text-live", "Text Live")],
},
}),
},
staticCatalog: {
run: async () => ({
provider: {
baseUrl: "https://text.example/v1",
models: [createCatalogModel("text-static", "Text Static")],
},
}),
},
});
expect(pluginRegistry.registry.providers).toHaveLength(1);
expect(pluginRegistry.registry.modelCatalogProviders).toHaveLength(1);
const catalogProvider = pluginRegistry.registry.modelCatalogProviders[0]?.provider;
expect(catalogProvider?.provider).toBe("text-provider");
expect(catalogProvider?.kinds).toEqual(["text"]);
await expect(catalogProvider?.staticCatalog?.({} as never)).resolves.toEqual([
{
kind: "text",
provider: "text-provider",
model: "text-static",
label: "Text Static",
source: "static",
},
]);
await expect(catalogProvider?.liveCatalog?.({} as never)).resolves.toEqual([
{
kind: "text",
provider: "text-provider",
model: "text-live",
label: "Text Live",
source: "live",
},
]);
});
it("publishes synthesized media-generation catalog rows during provider registration", async () => {
const pluginRegistry = createTestRegistry();
const record = createPluginRecord({
id: "media-owner",
name: "Media Owner",
source: "/tmp/media-owner/index.js",
origin: "global",
enabled: true,
configSchema: false,
});
pluginRegistry.registerVideoGenerationProvider(record, {
id: "video-provider",
label: "Video Provider",
defaultModel: "video-default",
models: ["video-default", "video-pro"],
capabilities: {
generate: {
supportedDurationSeconds: [4, 8],
},
},
generateVideo: async () => ({
videos: [{ buffer: Buffer.alloc(0), mimeType: "video/mp4" }],
}),
});
expect(pluginRegistry.registry.videoGenerationProviders).toHaveLength(1);
expect(pluginRegistry.registry.modelCatalogProviders).toHaveLength(1);
const catalogProvider = pluginRegistry.registry.modelCatalogProviders[0]?.provider;
expect(catalogProvider?.provider).toBe("video-provider");
expect(catalogProvider?.kinds).toEqual(["video_generation"]);
const staticRows = await catalogProvider?.staticCatalog?.({} as never);
expect(staticRows).toHaveLength(2);
expect(staticRows?.[0]?.kind).toBe("video_generation");
expect(staticRows?.[0]?.provider).toBe("video-provider");
expect(staticRows?.[0]?.model).toBe("video-default");
expect(staticRows?.[0]?.source).toBe("static");
expect(staticRows?.[0]?.default).toBe(true);
expect(staticRows?.[0]?.capabilities).toEqual({
generate: {
supportedDurationSeconds: [4, 8],
},
});
expect(staticRows?.[1]?.kind).toBe("video_generation");
expect(staticRows?.[1]?.provider).toBe("video-provider");
expect(staticRows?.[1]?.model).toBe("video-pro");
expect(staticRows?.[1]?.source).toBe("static");
});
it("does not duplicate manifest-declared capability provider ids during runtime registration", () => {
const pluginRegistry = createTestRegistry();
const record = createPluginRecord({
id: "kitchen-sink",
name: "Kitchen Sink",
source: "/tmp/kitchen-sink/index.js",
origin: "global",
enabled: true,
contracts: {
speechProviders: ["kitchen-sink-speech-provider"],
},
configSchema: false,
});
pluginRegistry.registerSpeechProvider(record, {
id: "kitchen-sink-speech-provider",
label: "Kitchen Sink Speech",
isConfigured: () => true,
synthesize: async () => ({
audioBuffer: Buffer.alloc(0),
fileExtension: "mp3",
outputFormat: "audio/mpeg",
voiceCompatible: true,
}),
});
expect(record.speechProviderIds).toEqual(["kitchen-sink-speech-provider"]);
expect(pluginRegistry.registry.speechProviders).toHaveLength(1);
});
});