From d0e0150129febaa83f8f5ca6a401b0908bb79dcc Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Sat, 28 Mar 2026 19:53:21 -0400 Subject: [PATCH] Tests: retry scoped contract registry loads --- src/plugins/contracts/registry.retry.test.ts | 147 +++++++++++++++++++ src/plugins/contracts/registry.ts | 105 +++++++++++-- 2 files changed, 240 insertions(+), 12 deletions(-) create mode 100644 src/plugins/contracts/registry.retry.test.ts diff --git a/src/plugins/contracts/registry.retry.test.ts b/src/plugins/contracts/registry.retry.test.ts new file mode 100644 index 00000000000..044e4979484 --- /dev/null +++ b/src/plugins/contracts/registry.retry.test.ts @@ -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); + }); +}); diff --git a/src/plugins/contracts/registry.ts b/src/plugins/contracts/registry.ts index 09851888761..7922b49972b 100644 --- a/src/plugins/contracts/registry.ts +++ b/src/plugins/contracts/registry.ts @@ -20,6 +20,7 @@ import { loadVitestSpeechProviderContractRegistry, } from "./speech-vitest-registry.js"; +type BundledCapabilityRuntimeRegistry = ReturnType; type CapabilityContractEntry = { 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(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; }