mirror of https://github.com/openclaw/openclaw.git
Tests: retry scoped contract registry loads
This commit is contained in:
parent
d3673fd53e
commit
d0e0150129
|
|
@ -0,0 +1,147 @@
|
|||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import type { ProviderPlugin, WebSearchProviderPlugin } from "../types.js";
|
||||
|
||||
type MockPluginRecord = {
|
||||
id: string;
|
||||
status: "loaded" | "error";
|
||||
error?: string;
|
||||
providerIds: string[];
|
||||
webSearchProviderIds: string[];
|
||||
};
|
||||
|
||||
type MockRuntimeRegistry = {
|
||||
plugins: MockPluginRecord[];
|
||||
diagnostics: Array<{ pluginId?: string; message: string }>;
|
||||
providers: Array<{ pluginId: string; provider: ProviderPlugin }>;
|
||||
webSearchProviders: Array<{ pluginId: string; provider: WebSearchProviderPlugin }>;
|
||||
};
|
||||
|
||||
function createMockRuntimeRegistry(params: {
|
||||
plugin: MockPluginRecord;
|
||||
providers?: Array<{ pluginId: string; provider: ProviderPlugin }>;
|
||||
webSearchProviders?: Array<{ pluginId: string; provider: WebSearchProviderPlugin }>;
|
||||
diagnostics?: Array<{ pluginId?: string; message: string }>;
|
||||
}): MockRuntimeRegistry {
|
||||
return {
|
||||
plugins: [params.plugin],
|
||||
diagnostics: params.diagnostics ?? [],
|
||||
providers: params.providers ?? [],
|
||||
webSearchProviders: params.webSearchProviders ?? [],
|
||||
};
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
vi.resetModules();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe("plugin contract registry scoped retries", () => {
|
||||
it("retries provider loads after a transient plugin-scoped runtime error", async () => {
|
||||
const loadBundledCapabilityRuntimeRegistry = vi
|
||||
.fn()
|
||||
.mockReturnValueOnce(
|
||||
createMockRuntimeRegistry({
|
||||
plugin: {
|
||||
id: "xai",
|
||||
status: "error",
|
||||
error: "transient xai load failure",
|
||||
providerIds: [],
|
||||
webSearchProviderIds: [],
|
||||
},
|
||||
diagnostics: [{ pluginId: "xai", message: "transient xai load failure" }],
|
||||
}),
|
||||
)
|
||||
.mockReturnValueOnce(
|
||||
createMockRuntimeRegistry({
|
||||
plugin: {
|
||||
id: "xai",
|
||||
status: "loaded",
|
||||
providerIds: ["xai"],
|
||||
webSearchProviderIds: ["grok"],
|
||||
},
|
||||
providers: [
|
||||
{
|
||||
pluginId: "xai",
|
||||
provider: {
|
||||
id: "xai",
|
||||
label: "xAI",
|
||||
docsPath: "/providers/xai",
|
||||
auth: [],
|
||||
} as ProviderPlugin,
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
vi.doMock("../bundled-capability-runtime.js", () => ({
|
||||
loadBundledCapabilityRuntimeRegistry,
|
||||
}));
|
||||
|
||||
const { resolveProviderContractProvidersForPluginIds } = await import("./registry.js");
|
||||
|
||||
expect(
|
||||
resolveProviderContractProvidersForPluginIds(["xai"]).map((provider) => provider.id),
|
||||
).toEqual(["xai"]);
|
||||
expect(loadBundledCapabilityRuntimeRegistry).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("retries web search provider loads after a transient plugin-scoped runtime error", async () => {
|
||||
const loadBundledCapabilityRuntimeRegistry = vi
|
||||
.fn()
|
||||
.mockReturnValueOnce(
|
||||
createMockRuntimeRegistry({
|
||||
plugin: {
|
||||
id: "xai",
|
||||
status: "error",
|
||||
error: "transient grok load failure",
|
||||
providerIds: [],
|
||||
webSearchProviderIds: [],
|
||||
},
|
||||
diagnostics: [{ pluginId: "xai", message: "transient grok load failure" }],
|
||||
}),
|
||||
)
|
||||
.mockReturnValueOnce(
|
||||
createMockRuntimeRegistry({
|
||||
plugin: {
|
||||
id: "xai",
|
||||
status: "loaded",
|
||||
providerIds: ["xai"],
|
||||
webSearchProviderIds: ["grok"],
|
||||
},
|
||||
webSearchProviders: [
|
||||
{
|
||||
pluginId: "xai",
|
||||
provider: {
|
||||
id: "grok",
|
||||
label: "Grok Search",
|
||||
hint: "Search the web with Grok",
|
||||
envVars: ["XAI_API_KEY"],
|
||||
placeholder: "XAI_API_KEY",
|
||||
signupUrl: "https://x.ai",
|
||||
credentialPath: "plugins.entries.xai.config.webSearch.apiKey",
|
||||
requiresCredential: true,
|
||||
getCredentialValue: () => undefined,
|
||||
setCredentialValue() {},
|
||||
createTool: () => ({
|
||||
description: "search",
|
||||
parameters: {},
|
||||
execute: async () => ({}),
|
||||
}),
|
||||
} as WebSearchProviderPlugin,
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
vi.doMock("../bundled-capability-runtime.js", () => ({
|
||||
loadBundledCapabilityRuntimeRegistry,
|
||||
}));
|
||||
|
||||
const { resolveWebSearchProviderContractEntriesForPluginId } = await import("./registry.js");
|
||||
|
||||
expect(
|
||||
resolveWebSearchProviderContractEntriesForPluginId("xai").map((entry) => entry.provider.id),
|
||||
).toEqual(["grok"]);
|
||||
expect(loadBundledCapabilityRuntimeRegistry).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
|
@ -20,6 +20,7 @@ import {
|
|||
loadVitestSpeechProviderContractRegistry,
|
||||
} from "./speech-vitest-registry.js";
|
||||
|
||||
type BundledCapabilityRuntimeRegistry = ReturnType<typeof loadBundledCapabilityRuntimeRegistry>;
|
||||
type CapabilityContractEntry<T> = {
|
||||
pluginId: string;
|
||||
provider: T;
|
||||
|
|
@ -91,6 +92,73 @@ const providerContractPluginIdsByProviderId = createProviderContractPluginIdsByP
|
|||
|
||||
export let providerContractLoadError: Error | undefined;
|
||||
|
||||
function formatBundledCapabilityPluginLoadError(params: {
|
||||
pluginId: string;
|
||||
capabilityLabel: string;
|
||||
registry: BundledCapabilityRuntimeRegistry;
|
||||
}): Error {
|
||||
const plugin = params.registry.plugins.find((entry) => entry.id === params.pluginId);
|
||||
const diagnostics = params.registry.diagnostics
|
||||
.filter((entry) => entry.pluginId === params.pluginId)
|
||||
.map((entry) => entry.message);
|
||||
const detailParts = plugin
|
||||
? [
|
||||
`status=${plugin.status}`,
|
||||
...(plugin.error ? [`error=${plugin.error}`] : []),
|
||||
`providerIds=[${plugin.providerIds.join(", ")}]`,
|
||||
`webSearchProviderIds=[${plugin.webSearchProviderIds.join(", ")}]`,
|
||||
]
|
||||
: ["plugin record missing"];
|
||||
if (diagnostics.length > 0) {
|
||||
detailParts.push(`diagnostics=${diagnostics.join(" | ")}`);
|
||||
}
|
||||
return new Error(
|
||||
`bundled ${params.capabilityLabel} contract load failed for ${params.pluginId}: ${detailParts.join("; ")}`,
|
||||
);
|
||||
}
|
||||
|
||||
function loadScopedCapabilityRuntimeRegistryEntries<T>(params: {
|
||||
pluginId: string;
|
||||
capabilityLabel: string;
|
||||
loadEntries: (registry: BundledCapabilityRuntimeRegistry) => T[];
|
||||
loadDeclaredIds: (
|
||||
plugin: BundledCapabilityRuntimeRegistry["plugins"][number],
|
||||
) => readonly string[];
|
||||
}): T[] {
|
||||
let lastFailure: Error | undefined;
|
||||
|
||||
for (let attempt = 0; attempt < 2; attempt += 1) {
|
||||
const registry = loadBundledCapabilityRuntimeRegistry({
|
||||
pluginIds: [params.pluginId],
|
||||
pluginSdkResolution: "dist",
|
||||
});
|
||||
const entries = params.loadEntries(registry);
|
||||
if (entries.length > 0) {
|
||||
return entries;
|
||||
}
|
||||
|
||||
const plugin = registry.plugins.find((entry) => entry.id === params.pluginId);
|
||||
lastFailure = formatBundledCapabilityPluginLoadError({
|
||||
pluginId: params.pluginId,
|
||||
capabilityLabel: params.capabilityLabel,
|
||||
registry,
|
||||
});
|
||||
const shouldRetry =
|
||||
attempt === 0 &&
|
||||
(!plugin || plugin.status !== "loaded" || params.loadDeclaredIds(plugin).length === 0);
|
||||
if (!shouldRetry) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
throw (
|
||||
lastFailure ??
|
||||
new Error(
|
||||
`bundled ${params.capabilityLabel} contract load failed for ${params.pluginId}: no entries`,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function loadProviderContractEntriesForPluginIds(
|
||||
pluginIds: readonly string[],
|
||||
): ProviderContractEntry[] {
|
||||
|
|
@ -112,10 +180,18 @@ function loadProviderContractEntriesForPluginId(pluginId: string): ProviderContr
|
|||
|
||||
try {
|
||||
providerContractLoadError = undefined;
|
||||
const entries = loadBundledCapabilityRuntimeRegistry({
|
||||
pluginIds: [pluginId],
|
||||
pluginSdkResolution: "dist",
|
||||
}).providers.map((entry) => ({
|
||||
const entries = loadScopedCapabilityRuntimeRegistryEntries({
|
||||
pluginId,
|
||||
capabilityLabel: "provider",
|
||||
loadEntries: (registry) =>
|
||||
registry.providers
|
||||
.filter((entry) => entry.pluginId === pluginId)
|
||||
.map((entry) => ({
|
||||
pluginId: entry.pluginId,
|
||||
provider: entry.provider,
|
||||
})),
|
||||
loadDeclaredIds: (plugin) => plugin.providerIds,
|
||||
}).map((entry) => ({
|
||||
pluginId: entry.pluginId,
|
||||
provider: entry.provider,
|
||||
}));
|
||||
|
|
@ -208,14 +284,19 @@ export function resolveWebSearchProviderContractEntriesForPluginId(
|
|||
return cached;
|
||||
}
|
||||
|
||||
const entries = loadBundledCapabilityRuntimeRegistry({
|
||||
pluginIds: [pluginId],
|
||||
pluginSdkResolution: "dist",
|
||||
}).webSearchProviders.map((entry) => ({
|
||||
pluginId: entry.pluginId,
|
||||
provider: entry.provider,
|
||||
credentialValue: resolveWebSearchCredentialValue(entry.provider),
|
||||
}));
|
||||
const entries = loadScopedCapabilityRuntimeRegistryEntries({
|
||||
pluginId,
|
||||
capabilityLabel: "web search provider",
|
||||
loadEntries: (registry) =>
|
||||
registry.webSearchProviders
|
||||
.filter((entry) => entry.pluginId === pluginId)
|
||||
.map((entry) => ({
|
||||
pluginId: entry.pluginId,
|
||||
provider: entry.provider,
|
||||
credentialValue: resolveWebSearchCredentialValue(entry.provider),
|
||||
})),
|
||||
loadDeclaredIds: (plugin) => plugin.webSearchProviderIds,
|
||||
});
|
||||
cache.set(pluginId, entries);
|
||||
return entries;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue