diff --git a/src/gateway/server-startup-model-runtime.event-loop.test.ts b/src/gateway/server-startup-model-runtime.event-loop.test.ts index 4cbca8150232..e50d6bddd485 100644 --- a/src/gateway/server-startup-model-runtime.event-loop.test.ts +++ b/src/gateway/server-startup-model-runtime.event-loop.test.ts @@ -2,7 +2,6 @@ import { mkdtemp, rm } from "node:fs/promises"; import http from "node:http"; import os from "node:os"; import path from "node:path"; -import { performance } from "node:perf_hooks"; import { afterEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import type { ProviderPlugin } from "../plugins/types.js"; @@ -41,8 +40,8 @@ vi.mock("../plugins/provider-discovery.js", async (importOriginal) => { pluginId: "openai", label: "OpenAI", auth: [], - catalog: { order: "simple", run: async () => null }, - staticCatalog: { order: "simple", run: async () => null }, + catalog: { order: "simple", run: providerMocks.liveCatalog }, + staticCatalog: { order: "simple", run: providerMocks.staticCatalog }, }; return { ...actual, @@ -56,6 +55,11 @@ const { resetPreparedModelRuntimeSnapshotsForTest } = await import("../agents/prepared-model-runtime.test-support.js"); const { writePersistedAuthProfileStoreRaw } = await import("../agents/auth-profiles/sqlite.js"); const { resolveAgentDir } = await import("../agents/agent-scope.js"); +const { createPluginMetadataSnapshot, makeRegistry } = + await import("../config/plugin-auto-enable.test-helpers.js"); +const { setCurrentPluginMetadataSnapshot } = + await import("../plugins/current-plugin-metadata-snapshot.js"); +const pluginDiscovery = await import("../plugins/discovery.js"); const { startGatewaySidecars } = await import("./server-startup-post-attach.js"); async function listenHealthz( @@ -108,9 +112,55 @@ describe("Gateway prepared model runtime startup", () => { }, }, gateway: { mode: "local", bind: "loopback", auth: { mode: "none" } }, - plugins: { enabled: false }, } satisfies OpenClawConfig; const env = { ...process.env, OPENCLAW_STATE_DIR: stateDir }; + const manifestRegistry = makeRegistry([ + { id: "openai", channels: [], providers: ["openai"], origin: "bundled" }, + ]); + const providerManifest = manifestRegistry.plugins[0]; + if (!providerManifest) { + throw new Error("expected bundled OpenAI provider manifest"); + } + providerManifest.enabledByDefault = true; + providerManifest.modelCatalog = { ...providerConfig, discovery: { openai: "runtime" } }; + const metadataSnapshot = createPluginMetadataSnapshot({ + config: cfg, + manifestRegistry, + workspaceDir, + }); + const startupMetadataSnapshot = { + ...metadataSnapshot, + index: { + ...metadataSnapshot.index, + plugins: [ + { + pluginId: providerManifest.id, + manifestPath: providerManifest.manifestPath, + manifestHash: "openai-test-manifest", + rootDir: providerManifest.rootDir, + origin: providerManifest.origin, + enabled: true, + enabledByDefault: true, + startup: { + sidecar: false, + memory: false, + deferConfiguredChannelFullLoadUntilAfterListen: false, + agentHarnesses: [], + }, + compat: [], + }, + ], + }, + owners: { + ...metadataSnapshot.owners, + providers: new Map([["openai", ["openai"]]]), + modelCatalogProviders: new Map([["openai", ["openai"]]]), + }, + metrics: { ...metadataSnapshot.metrics, indexPluginCount: 1 }, + startup: { channelPluginIds: [], configuredDeferredChannelPluginIds: [], pluginIds: [] }, + }; + setCurrentPluginMetadataSnapshot(startupMetadataSnapshot, { config: cfg, env, workspaceDir }); + const discoverPlugins = vi.spyOn(pluginDiscovery, "discoverOpenClawPlugins"); const agentDir = resolveAgentDir(cfg, "main", env); writePersistedAuthProfileStoreRaw( { @@ -126,15 +176,6 @@ describe("Gateway prepared model runtime startup", () => { }, agentDir, ); - const blockEventLoop = async () => { - const stopAt = performance.now() + 1_500; - while (performance.now() < stopAt) { - // Deliberately model synchronous provider/plugin catalog work that starves timers. - } - return providerConfig; - }; - providerMocks.staticCatalog.mockImplementation(blockEventLoop); - providerMocks.liveCatalog.mockImplementation(blockEventLoop); const startupEvents: string[] = []; const healthServer = await listenHealthz(() => { startupEvents.push("health-served"); @@ -198,6 +239,7 @@ describe("Gateway prepared model runtime startup", () => { "startup-complete", ]); console.info(`[gateway-startup-proof] ${startupEvents.join(" -> ")}`); + expect(discoverPlugins).not.toHaveBeenCalled(); expect(providerMocks.staticCatalog).not.toHaveBeenCalled(); expect(providerMocks.liveCatalog).not.toHaveBeenCalled(); }, diff --git a/src/plugins/provider-runtime.ts b/src/plugins/provider-runtime.ts index ee67378345e0..5bafd0b0956a 100644 --- a/src/plugins/provider-runtime.ts +++ b/src/plugins/provider-runtime.ts @@ -17,6 +17,7 @@ import type { ProviderSystemPromptContribution } from "../agents/system-prompt-c import type { ModelProviderConfig } from "../config/types.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import type { UsageProviderId } from "../infra/provider-usage.types.js"; +import { getCurrentPluginMetadataSnapshot } from "./current-plugin-metadata-snapshot.js"; import { normalizeProviderModelIdWithManifest } from "./manifest-model-id-normalization.js"; import type { PluginManifestRecord } from "./manifest-registry.js"; import { resolvePluginMetadataSnapshot } from "./plugin-metadata-snapshot.js"; @@ -998,11 +999,22 @@ export function resolveExternalAuthProfilesWithPlugins(params: { }): ProviderExternalAuthProfile[] { const workspaceDir = params.workspaceDir ?? getActivePluginRegistryWorkspaceDirFromState(); const env = params.env ?? process.env; - const { manifestRegistry } = resolvePluginMetadataSnapshot({ - config: params.config ?? {}, - workspaceDir, + const config = params.config ?? {}; + const currentMetadataSnapshot = getCurrentPluginMetadataSnapshot({ + config, env, + ...(workspaceDir === undefined ? { allowWorkspaceScopedSnapshot: true } : { workspaceDir }), }); + const { manifestRegistry } = + currentMetadataSnapshot ?? resolvePluginMetadataSnapshot({ config, workspaceDir, env }); + // A lifecycle-owned manifest is authoritative: no external-auth contracts means + // no provider registry discovery or runtime activation is needed for this overlay. + if ( + currentMetadataSnapshot && + !manifestRegistry.plugins.some((plugin) => plugin.contracts?.externalAuthProviders?.length) + ) { + return []; + } const externalAuthPluginIds = resolveExternalAuthProfileProviderPluginIds({ config: params.config, workspaceDir,