From 8599fdda4aa380fd6a55910df251209d3863f329 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 27 Apr 2026 22:55:01 +0100 Subject: [PATCH] test: keep extension mocks on sdk seams --- docs/plugins/sdk-subpaths.md | 1 + .../bluebubbles/src/channel.status.test.ts | 5 -- .../src/cli/browser-cli-inspect.test.ts | 12 +++- .../browser-request.profile-from-body.test.ts | 14 +++-- .../codex/src/app-server/auth-bridge.test.ts | 59 ++++++++++++++++--- extensions/discord/src/monitor.test.ts | 12 +++- .../native-command.think-autocomplete.test.ts | 44 ++++++++++++-- extensions/feishu/src/channel.test.ts | 5 -- extensions/feishu/src/client.test.ts | 5 -- .../feishu/src/lifecycle.test-support.ts | 7 --- extensions/feishu/src/media.test.ts | 5 -- .../feishu/src/send.reply-fallback.test.ts | 5 -- extensions/google/oauth.test.ts | 48 +++++++++------ .../nextcloud-talk/src/channel.core.test.ts | 5 -- .../provider-catalog.contract-test-support.ts | 52 ++++++++++++---- .../event-handler.inbound-context.test.ts | 14 +++-- .../message-handler.app-mention-race.test.ts | 48 ++++++++------- extensions/slack/src/send.upload.test.ts | 19 ++++-- extensions/webhooks/src/http.test.ts | 20 ------- extensions/whatsapp/src/inbound.media.test.ts | 18 +++++- .../src/outbound-adapter.poll.test.ts | 12 +++- package.json | 4 ++ .../check-no-extension-test-core-imports.ts | 3 + scripts/lib/plugin-sdk-entrypoints.json | 1 + src/plugin-sdk/provider-catalog-runtime.ts | 15 +++++ test/helpers/plugins/provider-catalog.ts | 4 +- 26 files changed, 292 insertions(+), 145 deletions(-) create mode 100644 src/plugin-sdk/provider-catalog-runtime.ts diff --git a/docs/plugins/sdk-subpaths.md b/docs/plugins/sdk-subpaths.md index 63ee84e7cd6..4976e76fa3d 100644 --- a/docs/plugins/sdk-subpaths.md +++ b/docs/plugins/sdk-subpaths.md @@ -99,6 +99,7 @@ For the plugin authoring guide, see [Plugin SDK overview](/plugins/sdk-overview) | `plugin-sdk/provider-env-vars` | Provider auth env-var lookup helpers | | `plugin-sdk/provider-auth` | `createProviderApiKeyAuthMethod`, `ensureApiKeyFromOptionEnvOrPrompt`, `upsertAuthProfile`, `upsertApiKeyProfile`, `writeOAuthCredentials` | | `plugin-sdk/provider-model-shared` | `ProviderReplayFamily`, `buildProviderReplayFamilyHooks`, `normalizeModelCompat`, shared replay-policy builders, provider-endpoint helpers, and model-id normalization helpers such as `normalizeNativeXaiModelId` | + | `plugin-sdk/provider-catalog-runtime` | Provider catalog runtime hook and plugin-provider registry seams for contract tests | | `plugin-sdk/provider-catalog-shared` | `findCatalogTemplate`, `buildSingleProviderApiKeyCatalog`, `supportsNativeStreamingUsageCompat`, `applyProviderNativeStreamingUsageCompat` | | `plugin-sdk/provider-http` | Generic provider HTTP/endpoint capability helpers, provider HTTP errors, and audio transcription multipart form helpers | | `plugin-sdk/provider-web-fetch-contract` | Narrow web-fetch config/selection contract helpers such as `enablePluginInConfig` and `WebFetchProviderPlugin` | diff --git a/extensions/bluebubbles/src/channel.status.test.ts b/extensions/bluebubbles/src/channel.status.test.ts index ad469ba4198..92d80ae8710 100644 --- a/extensions/bluebubbles/src/channel.status.test.ts +++ b/extensions/bluebubbles/src/channel.status.test.ts @@ -10,11 +10,6 @@ vi.mock("./channel.runtime.js", () => ({ }, })); -vi.mock("../../../src/channels/plugins/bundled.js", () => ({ - bundledChannelPlugins: [], - bundledChannelSetupPlugins: [], -})); - let bluebubblesPlugin: typeof import("./channel.js").bluebubblesPlugin; describe("bluebubblesPlugin.status.probeAccount", () => { diff --git a/extensions/browser/src/cli/browser-cli-inspect.test.ts b/extensions/browser/src/cli/browser-cli-inspect.test.ts index af076e15717..e428c60eab4 100644 --- a/extensions/browser/src/cli/browser-cli-inspect.test.ts +++ b/extensions/browser/src/cli/browser-cli-inspect.test.ts @@ -16,9 +16,15 @@ const gatewayMocks = vi.hoisted(() => ({ })), })); -vi.mock("../../../../src/cli/gateway-rpc.js", () => ({ - callGatewayFromCli: gatewayMocks.callGatewayFromCli, -})); +vi.mock("openclaw/plugin-sdk/browser-node-runtime", async () => { + const actual = await vi.importActual( + "openclaw/plugin-sdk/browser-node-runtime", + ); + return { + ...actual, + callGatewayFromCli: gatewayMocks.callGatewayFromCli, + }; +}); const configMocks = vi.hoisted(() => { const loadConfig = vi.fn(() => ({ browser: {} })); diff --git a/extensions/browser/src/gateway/browser-request.profile-from-body.test.ts b/extensions/browser/src/gateway/browser-request.profile-from-body.test.ts index dd43bb917c3..4a2baa4205f 100644 --- a/extensions/browser/src/gateway/browser-request.profile-from-body.test.ts +++ b/extensions/browser/src/gateway/browser-request.profile-from-body.test.ts @@ -18,10 +18,16 @@ vi.mock("openclaw/plugin-sdk/runtime-config-snapshot", async () => { }; }); -vi.mock("../../../../src/gateway/node-command-policy.js", () => ({ - isNodeCommandAllowed: isNodeCommandAllowedMock, - resolveNodeCommandAllowlist: resolveNodeCommandAllowlistMock, -})); +vi.mock("openclaw/plugin-sdk/browser-node-runtime", async () => { + const actual = await vi.importActual( + "openclaw/plugin-sdk/browser-node-runtime", + ); + return { + ...actual, + isNodeCommandAllowed: isNodeCommandAllowedMock, + resolveNodeCommandAllowlist: resolveNodeCommandAllowlistMock, + }; +}); import { browserHandlers } from "./browser-request.js"; diff --git a/extensions/codex/src/app-server/auth-bridge.test.ts b/extensions/codex/src/app-server/auth-bridge.test.ts index 9b44c60ba50..33906ef8d1e 100644 --- a/extensions/codex/src/app-server/auth-bridge.test.ts +++ b/extensions/codex/src/app-server/auth-bridge.test.ts @@ -16,7 +16,7 @@ const oauthMocks = vi.hoisted(() => ({ const providerRuntimeMocks = vi.hoisted(() => ({ formatProviderAuthProfileApiKeyWithPlugin: vi.fn(), refreshProviderOAuthCredentialWithPlugin: vi.fn( - async (params: { context: { refresh: string } }) => { + async (params: { provider?: string; context: { refresh: string } }) => { const refreshed = await oauthMocks.refreshOpenAICodexToken(params.context.refresh); return refreshed ? { @@ -37,12 +37,57 @@ vi.mock("@mariozechner/pi-ai/oauth", () => ({ refreshOpenAICodexToken: oauthMocks.refreshOpenAICodexToken, })); -vi.mock("../../../../src/plugins/provider-runtime.runtime.js", () => ({ - formatProviderAuthProfileApiKeyWithPlugin: - providerRuntimeMocks.formatProviderAuthProfileApiKeyWithPlugin, - refreshProviderOAuthCredentialWithPlugin: - providerRuntimeMocks.refreshProviderOAuthCredentialWithPlugin, -})); +vi.mock("openclaw/plugin-sdk/agent-runtime", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + resolveApiKeyForProfile: async ( + params: Parameters[0], + ) => { + const credential = params.store.profiles[params.profileId]; + if (!credential) { + return null; + } + if (credential.type === "api_key") { + const apiKey = + credential.key?.trim() || + (credential.keyRef?.source === "env" ? process.env[credential.keyRef.id]?.trim() : ""); + return apiKey ? { apiKey, provider: credential.provider } : null; + } + if (credential.type === "token") { + const apiKey = + credential.token?.trim() || + (credential.tokenRef?.source === "env" + ? process.env[credential.tokenRef.id]?.trim() + : ""); + return apiKey ? { apiKey, provider: credential.provider, email: credential.email } : null; + } + let oauthCredential = credential; + if ((oauthCredential.expires ?? 0) <= Date.now()) { + const refreshed = await providerRuntimeMocks.refreshProviderOAuthCredentialWithPlugin({ + provider: oauthCredential.provider, + context: oauthCredential, + }); + if (refreshed?.access) { + oauthCredential = refreshed as typeof oauthCredential; + params.store.profiles[params.profileId] = oauthCredential; + if (params.agentDir) { + actual.saveAuthProfileStore(params.store, params.agentDir); + } + } + } + const formatted = await providerRuntimeMocks.formatProviderAuthProfileApiKeyWithPlugin({ + provider: oauthCredential.provider, + context: oauthCredential, + }); + const apiKey = + typeof formatted === "string" && formatted ? formatted : oauthCredential.access; + return apiKey + ? { apiKey, provider: oauthCredential.provider, email: oauthCredential.email } + : null; + }, + }; +}); afterEach(() => { vi.unstubAllEnvs(); diff --git a/extensions/discord/src/monitor.test.ts b/extensions/discord/src/monitor.test.ts index e2004f9fa36..f0e7eecb5e9 100644 --- a/extensions/discord/src/monitor.test.ts +++ b/extensions/discord/src/monitor.test.ts @@ -25,9 +25,15 @@ type DiscordReactionClient = Parameters< const readAllowFromStoreMock = vi.hoisted(() => vi.fn()); -vi.mock("../../../src/pairing/pairing-store.js", () => ({ - readChannelAllowFromStore: (...args: unknown[]) => readAllowFromStoreMock(...args), -})); +vi.mock("openclaw/plugin-sdk/conversation-runtime", async () => { + const actual = await vi.importActual( + "openclaw/plugin-sdk/conversation-runtime", + ); + return { + ...actual, + readChannelAllowFromStore: (...args: unknown[]) => readAllowFromStoreMock(...args), + }; +}); const fakeGuild = (id: string, name: string) => ({ id, name }) as Guild; diff --git a/extensions/discord/src/monitor/native-command.think-autocomplete.test.ts b/extensions/discord/src/monitor/native-command.think-autocomplete.test.ts index eb538cb2c7d..69f0e3ab2f7 100644 --- a/extensions/discord/src/monitor/native-command.think-autocomplete.test.ts +++ b/extensions/discord/src/monitor/native-command.think-autocomplete.test.ts @@ -4,6 +4,7 @@ import path from "node:path"; import { ChannelType, type AutocompleteInteraction } from "@buape/carbon"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types"; import { clearSessionStoreCacheForTest } from "openclaw/plugin-sdk/session-store-runtime"; +import { createEmptyPluginRegistry, setActivePluginRegistry } from "openclaw/plugin-sdk/testing"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { createNoopThreadBindingManager } from "./thread-bindings.js"; @@ -125,14 +126,44 @@ let findCommandByNativeName: typeof import("openclaw/plugin-sdk/command-auth").f let resolveCommandArgChoices: typeof import("openclaw/plugin-sdk/command-auth").resolveCommandArgChoices; let resolveDiscordNativeChoiceContext: typeof import("./native-command-ui.js").resolveDiscordNativeChoiceContext; +function installProviderThinkingRegistryForTest(): void { + const registry = createEmptyPluginRegistry(); + registry.providers.push({ + pluginId: "discord-test", + source: "test", + provider: { + id: "discord-test-thinking", + label: "Discord Test Thinking", + aliases: ["anthropic", "openai-codex"], + auth: [], + isBinaryThinking: (context) => + providerThinkingMocks.resolveProviderBinaryThinking({ + provider: context.provider, + context, + }), + supportsXHighThinking: (context) => + providerThinkingMocks.resolveProviderXHighThinking({ + provider: context.provider, + context, + }), + resolveThinkingProfile: (context) => + providerThinkingMocks.resolveProviderThinkingProfile({ + provider: context.provider, + context, + }), + resolveDefaultThinkingLevel: (context) => + providerThinkingMocks.resolveProviderDefaultThinkingLevel({ + provider: context.provider, + context, + }), + }, + }); + setActivePluginRegistry(registry); +} + async function loadDiscordThinkAutocompleteModulesForTest() { vi.resetModules(); - vi.doMock("../../../../src/plugins/provider-thinking.js", () => ({ - resolveProviderBinaryThinking: providerThinkingMocks.resolveProviderBinaryThinking, - resolveProviderDefaultThinkingLevel: providerThinkingMocks.resolveProviderDefaultThinkingLevel, - resolveProviderThinkingProfile: providerThinkingMocks.resolveProviderThinkingProfile, - resolveProviderXHighThinking: providerThinkingMocks.resolveProviderXHighThinking, - })); + installProviderThinkingRegistryForTest(); const commandAuth = await import("openclaw/plugin-sdk/command-auth"); const nativeCommandUi = await import("./native-command-ui.js"); return { @@ -183,6 +214,7 @@ describe("discord native /think autocomplete", () => { ? true : undefined, ); + installProviderThinkingRegistryForTest(); fs.mkdirSync(path.dirname(STORE_PATH), { recursive: true }); fs.writeFileSync( STORE_PATH, diff --git a/extensions/feishu/src/channel.test.ts b/extensions/feishu/src/channel.test.ts index a6e7af830ce..10c98c64bb6 100644 --- a/extensions/feishu/src/channel.test.ts +++ b/extensions/feishu/src/channel.test.ts @@ -55,11 +55,6 @@ vi.mock("./channel.runtime.js", () => ({ }, })); -vi.mock("../../../src/channels/plugins/bundled.js", () => ({ - bundledChannelPlugins: [], - bundledChannelSetupPlugins: [], -})); - function getDescribedActions(cfg: OpenClawConfig, accountId?: string): string[] { return [...(feishuPlugin.actions?.describeMessageTool?.({ cfg, accountId })?.actions ?? [])]; } diff --git a/extensions/feishu/src/client.test.ts b/extensions/feishu/src/client.test.ts index 7349177d372..80172b70744 100644 --- a/extensions/feishu/src/client.test.ts +++ b/extensions/feishu/src/client.test.ts @@ -91,11 +91,6 @@ vi.mock("./subagent-hooks.js", () => ({ registerFeishuSubagentHooks: registerFeishuSubagentHooksMock, })); -vi.mock("../../../src/channels/plugins/bundled.js", () => ({ - bundledChannelPlugins: [], - bundledChannelSetupPlugins: [], -})); - const baseAccount: ResolvedFeishuAccount = { accountId: "main", selectionSource: "explicit", diff --git a/extensions/feishu/src/lifecycle.test-support.ts b/extensions/feishu/src/lifecycle.test-support.ts index 9e42bd1acf2..cdce9bcfd1c 100644 --- a/extensions/feishu/src/lifecycle.test-support.ts +++ b/extensions/feishu/src/lifecycle.test-support.ts @@ -218,10 +218,3 @@ vi.mock("openclaw/plugin-sdk/conversation-runtime", async () => { }), }; }); - -vi.mock("../../../src/infra/outbound/session-binding-service.js", () => ({ - getSessionBindingService: () => ({ - resolveByConversation: resolveBoundConversationMock, - touch: touchBindingMock, - }), -})); diff --git a/extensions/feishu/src/media.test.ts b/extensions/feishu/src/media.test.ts index 0ed50ae6c8c..9f572b21076 100644 --- a/extensions/feishu/src/media.test.ts +++ b/extensions/feishu/src/media.test.ts @@ -51,11 +51,6 @@ vi.mock("openclaw/plugin-sdk/media-runtime", async (importOriginal) => { }; }); -vi.mock("../../../src/channels/plugins/bundled.js", () => ({ - bundledChannelPlugins: [], - bundledChannelSetupPlugins: [], -})); - let downloadImageFeishu: typeof import("./media.js").downloadImageFeishu; let downloadMessageResourceFeishu: typeof import("./media.js").downloadMessageResourceFeishu; let sanitizeFileNameForUpload: typeof import("./media.js").sanitizeFileNameForUpload; diff --git a/extensions/feishu/src/send.reply-fallback.test.ts b/extensions/feishu/src/send.reply-fallback.test.ts index 7148b98284c..2fb1bdc2798 100644 --- a/extensions/feishu/src/send.reply-fallback.test.ts +++ b/extensions/feishu/src/send.reply-fallback.test.ts @@ -20,11 +20,6 @@ vi.mock("./runtime.js", () => ({ }), })); -vi.mock("../../../src/channels/plugins/bundled.js", () => ({ - bundledChannelPlugins: [], - bundledChannelSetupPlugins: [], -})); - let sendCardFeishu: typeof import("./send.js").sendCardFeishu; let sendMessageFeishu: typeof import("./send.js").sendMessageFeishu; diff --git a/extensions/google/oauth.test.ts b/extensions/google/oauth.test.ts index 4220f7015d4..0b2b6a81907 100644 --- a/extensions/google/oauth.test.ts +++ b/extensions/google/oauth.test.ts @@ -1,25 +1,37 @@ import { join, parse } from "node:path"; import { describe, expect, it, vi, beforeAll, beforeEach, afterEach } from "vitest"; -vi.mock("../../src/infra/wsl.js", () => ({ - isWSL2Sync: () => false, -})); +vi.mock("openclaw/plugin-sdk/runtime-env", async () => { + const actual = await vi.importActual( + "openclaw/plugin-sdk/runtime-env", + ); + return { + ...actual, + isWSL2Sync: () => false, + }; +}); -vi.mock("../../src/infra/net/fetch-guard.js", () => ({ - fetchWithSsrFGuard: async (params: { - url: string; - init?: RequestInit; - fetchImpl?: (input: RequestInfo | URL, init?: RequestInit) => Promise; - }) => { - const fetchImpl = params.fetchImpl ?? globalThis.fetch; - const response = await fetchImpl(params.url, params.init); - return { - response, - finalUrl: params.url, - release: async () => {}, - }; - }, -})); +vi.mock("openclaw/plugin-sdk/ssrf-runtime", async () => { + const actual = await vi.importActual( + "openclaw/plugin-sdk/ssrf-runtime", + ); + return { + ...actual, + fetchWithSsrFGuard: async (params: { + url: string; + init?: RequestInit; + fetchImpl?: (input: RequestInfo | URL, init?: RequestInit) => Promise; + }) => { + const fetchImpl = params.fetchImpl ?? globalThis.fetch; + const response = await fetchImpl(params.url, params.init); + return { + response, + finalUrl: params.url, + release: async () => {}, + }; + }, + }; +}); const mockExistsSync = vi.fn(); const mockReadFileSync = vi.fn(); diff --git a/extensions/nextcloud-talk/src/channel.core.test.ts b/extensions/nextcloud-talk/src/channel.core.test.ts index 4094fde9b28..db0b5905153 100644 --- a/extensions/nextcloud-talk/src/channel.core.test.ts +++ b/extensions/nextcloud-talk/src/channel.core.test.ts @@ -12,11 +12,6 @@ vi.mock("../../../test/helpers/config/bundled-channel-config-runtime.js", () => getBundledChannelConfigSchemaMap: () => new Map(), })); -vi.mock("../../../src/channels/plugins/bundled.js", () => ({ - bundledChannelPlugins: [], - bundledChannelSetupPlugins: [], -})); - describe("nextcloud talk channel core", () => { it("accepts SecretRef botSecret and apiPassword at top-level", () => { const result = NextcloudTalkConfigSchema.safeParse({ diff --git a/extensions/openai/test-support/provider-catalog.contract-test-support.ts b/extensions/openai/test-support/provider-catalog.contract-test-support.ts index e384e768849..a854ce3a6f6 100644 --- a/extensions/openai/test-support/provider-catalog.contract-test-support.ts +++ b/extensions/openai/test-support/provider-catalog.contract-test-support.ts @@ -27,17 +27,47 @@ const resolveCatalogHookProviderPluginIdsMock = vi.hoisted(() => vi.fn((_) => [] as string[]), ); -vi.mock("../../../src/plugins/providers.js", () => ({ - resolveOwningPluginIdsForProvider: (params: unknown) => - resolveOwningPluginIdsForProviderMock(params as never), - resolveCatalogHookProviderPluginIds: (params: unknown) => - resolveCatalogHookProviderPluginIdsMock(params as never), -})); - -vi.mock("../../../src/plugins/providers.runtime.js", () => ({ - isPluginProvidersLoadInFlight: () => false, - resolvePluginProviders: (params: unknown) => resolvePluginProvidersMock(params as never), -})); +vi.mock("openclaw/plugin-sdk/provider-catalog-runtime", async () => { + const actual = await vi.importActual< + typeof import("openclaw/plugin-sdk/provider-catalog-runtime") + >("openclaw/plugin-sdk/provider-catalog-runtime"); + const resolveCatalogHookProviders = (params: unknown) => + resolvePluginProvidersMock({ + onlyPluginIds: resolveCatalogHookProviderPluginIdsMock(params), + }); + return { + ...actual, + augmentModelCatalogWithProviderPlugins: async (params: { + context: Parameters>[0]; + }) => { + const supplemental = []; + for (const provider of resolveCatalogHookProviders(params)) { + const entries = await provider.augmentModelCatalog?.(params.context); + if (entries?.length) { + supplemental.push(...entries); + } + } + return supplemental; + }, + resolveProviderBuiltInModelSuppression: (params: { + context: Parameters>[0]; + }) => { + for (const provider of resolveCatalogHookProviders(params)) { + const result = provider.suppressBuiltInModel?.(params.context); + if (result?.suppress) { + return result; + } + } + return undefined; + }, + resolveOwningPluginIdsForProvider: (params: unknown) => + resolveOwningPluginIdsForProviderMock(params as never), + resolveCatalogHookProviderPluginIds: (params: unknown) => + resolveCatalogHookProviderPluginIdsMock(params as never), + isPluginProvidersLoadInFlight: () => false, + resolvePluginProviders: (params: unknown) => resolvePluginProvidersMock(params as never), + }; +}); export function describeOpenAIProviderCatalogContract() { const contractDepsPromise = (async () => { diff --git a/extensions/signal/src/monitor/event-handler.inbound-context.test.ts b/extensions/signal/src/monitor/event-handler.inbound-context.test.ts index 465efc20001..ece9ae1521e 100644 --- a/extensions/signal/src/monitor/event-handler.inbound-context.test.ts +++ b/extensions/signal/src/monitor/event-handler.inbound-context.test.ts @@ -46,10 +46,16 @@ vi.mock("openclaw/plugin-sdk/reply-runtime", async () => { }; }); -vi.mock("../../../../src/pairing/pairing-store.js", () => ({ - readChannelAllowFromStore: vi.fn().mockResolvedValue([]), - upsertChannelPairingRequest: vi.fn(), -})); +vi.mock("openclaw/plugin-sdk/conversation-runtime", async () => { + const actual = await vi.importActual( + "openclaw/plugin-sdk/conversation-runtime", + ); + return { + ...actual, + readChannelAllowFromStore: vi.fn().mockResolvedValue([]), + upsertChannelPairingRequest: vi.fn(), + }; +}); describe("signal createSignalEventHandler inbound context", () => { beforeEach(() => { diff --git a/extensions/slack/src/monitor/message-handler.app-mention-race.test.ts b/extensions/slack/src/monitor/message-handler.app-mention-race.test.ts index 6b0e095caec..f83610bffcd 100644 --- a/extensions/slack/src/monitor/message-handler.app-mention-race.test.ts +++ b/extensions/slack/src/monitor/message-handler.app-mention-race.test.ts @@ -8,28 +8,34 @@ const prepareSlackMessageMock = >(); const dispatchPreparedSlackMessageMock = vi.fn<(prepared: unknown) => Promise>(); -vi.mock("../../../../src/channels/inbound-debounce-policy.js", () => ({ - shouldDebounceTextInbound: () => false, - createChannelInboundDebouncer: (params: { - onFlush: ( - entries: Array<{ - message: Record; - opts: { source: "message" | "app_mention"; wasMentioned?: boolean }; - }>, - ) => Promise; - }) => ({ - debounceMs: 0, - debouncer: { - enqueue: async (entry: { - message: Record; - opts: { source: "message" | "app_mention"; wasMentioned?: boolean }; - }) => { - await params.onFlush([entry]); +vi.mock("openclaw/plugin-sdk/channel-inbound", async () => { + const actual = await vi.importActual( + "openclaw/plugin-sdk/channel-inbound", + ); + return { + ...actual, + shouldDebounceTextInbound: () => false, + createChannelInboundDebouncer: (params: { + onFlush: ( + entries: Array<{ + message: Record; + opts: { source: "message" | "app_mention"; wasMentioned?: boolean }; + }>, + ) => Promise; + }) => ({ + debounceMs: 0, + debouncer: { + enqueue: async (entry: { + message: Record; + opts: { source: "message" | "app_mention"; wasMentioned?: boolean }; + }) => { + await params.onFlush([entry]); + }, + flushKey: async (_key: string) => {}, }, - flushKey: async (_key: string) => {}, - }, - }), -})); + }), + }; +}); vi.mock("./thread-resolution.js", () => ({ createSlackThreadTsResolver: () => ({ diff --git a/extensions/slack/src/send.upload.test.ts b/extensions/slack/src/send.upload.test.ts index 214a90a7320..2bbde8ef2ab 100644 --- a/extensions/slack/src/send.upload.test.ts +++ b/extensions/slack/src/send.upload.test.ts @@ -21,15 +21,24 @@ const fetchWithSsrFGuard = vi.fn( }) as const, ); -vi.mock("../../../src/infra/net/fetch-guard.js", () => ({ +vi.mock("openclaw/plugin-sdk/ssrf-runtime", () => ({ fetchWithSsrFGuard: (...args: unknown[]) => fetchWithSsrFGuard(...(args as [params: { url: string; init?: RequestInit }])), - withTrustedEnvProxyGuardedFetchMode: (params: Record) => ({ - ...params, - mode: "trusted_env_proxy", - }), })); +vi.mock("openclaw/plugin-sdk/fetch-runtime", async () => { + const actual = await vi.importActual( + "openclaw/plugin-sdk/fetch-runtime", + ); + return { + ...actual, + withTrustedEnvProxyGuardedFetchMode: (params: Record) => ({ + ...params, + mode: "trusted_env_proxy", + }), + }; +}); + vi.mock("./runtime-api.js", async () => { const actual = await vi.importActual("./runtime-api.js"); const mockedLoadOutboundMediaFromUrl = diff --git a/extensions/webhooks/src/http.test.ts b/extensions/webhooks/src/http.test.ts index 34ff6bd6308..4800a2d5f33 100644 --- a/extensions/webhooks/src/http.test.ts +++ b/extensions/webhooks/src/http.test.ts @@ -7,32 +7,12 @@ import type { OpenClawConfig } from "../runtime-api.js"; import { createTaskFlowWebhookRequestHandler, type TaskFlowWebhookTarget } from "./http.js"; const hoisted = vi.hoisted(() => { - const sendMessageMock = vi.fn(); - const cancelSessionMock = vi.fn(); - const killSubagentRunAdminMock = vi.fn(); const resolveConfiguredSecretInputStringMock = vi.fn(); return { - sendMessageMock, - cancelSessionMock, - killSubagentRunAdminMock, resolveConfiguredSecretInputStringMock, }; }); -vi.mock("../../../src/tasks/task-registry-delivery-runtime.js", () => ({ - sendMessage: hoisted.sendMessageMock, -})); - -vi.mock("../../../src/acp/control-plane/manager.js", () => ({ - getAcpSessionManager: () => ({ - cancelSession: hoisted.cancelSessionMock, - }), -})); - -vi.mock("../../../src/agents/subagent-control.js", () => ({ - killSubagentRunAdmin: (params: unknown) => hoisted.killSubagentRunAdminMock(params), -})); - vi.mock("../runtime-api.js", async (importOriginal) => { const actual = await importOriginal(); hoisted.resolveConfiguredSecretInputStringMock.mockImplementation( diff --git a/extensions/whatsapp/src/inbound.media.test.ts b/extensions/whatsapp/src/inbound.media.test.ts index eed90225935..89d03359358 100644 --- a/extensions/whatsapp/src/inbound.media.test.ts +++ b/extensions/whatsapp/src/inbound.media.test.ts @@ -49,8 +49,12 @@ vi.mock("openclaw/plugin-sdk/runtime-config-snapshot", async () => { }; }); -vi.mock("../../../src/pairing/pairing-store.js", () => { +vi.mock("openclaw/plugin-sdk/conversation-runtime", async () => { + const actual = await vi.importActual( + "openclaw/plugin-sdk/conversation-runtime", + ); return { + ...actual, readChannelAllowFromStore(...args: unknown[]) { return readAllowFromStoreMock(...args); }, @@ -60,6 +64,18 @@ vi.mock("../../../src/pairing/pairing-store.js", () => { }; }); +vi.mock("openclaw/plugin-sdk/channel-pairing", async () => { + const actual = await vi.importActual( + "openclaw/plugin-sdk/channel-pairing", + ); + return { + ...actual, + readChannelAllowFromStore(...args: unknown[]) { + return readAllowFromStoreMock(...args); + }, + }; +}); + vi.mock("openclaw/plugin-sdk/media-store", async () => { const actual = await vi.importActual( "openclaw/plugin-sdk/media-store", diff --git a/extensions/whatsapp/src/outbound-adapter.poll.test.ts b/extensions/whatsapp/src/outbound-adapter.poll.test.ts index 7626dcd5bb3..53c5a29e6dc 100644 --- a/extensions/whatsapp/src/outbound-adapter.poll.test.ts +++ b/extensions/whatsapp/src/outbound-adapter.poll.test.ts @@ -6,9 +6,15 @@ const hoisted = vi.hoisted(() => ({ sendReactionWhatsApp: vi.fn(async () => undefined), })); -vi.mock("../../../src/globals.js", () => ({ - shouldLogVerbose: () => false, -})); +vi.mock("openclaw/plugin-sdk/runtime-env", async () => { + const actual = await vi.importActual( + "openclaw/plugin-sdk/runtime-env", + ); + return { + ...actual, + shouldLogVerbose: () => false, + }; +}); vi.mock("./send.js", () => ({ sendPollWhatsApp: hoisted.sendPollWhatsApp, diff --git a/package.json b/package.json index 3c792daf730..9f7a4267142 100644 --- a/package.json +++ b/package.json @@ -1162,6 +1162,10 @@ "types": "./dist/plugin-sdk/plugin-entry.d.ts", "default": "./dist/plugin-sdk/plugin-entry.js" }, + "./plugin-sdk/provider-catalog-runtime": { + "types": "./dist/plugin-sdk/provider-catalog-runtime.d.ts", + "default": "./dist/plugin-sdk/provider-catalog-runtime.js" + }, "./plugin-sdk/provider-catalog-shared": { "types": "./dist/plugin-sdk/provider-catalog-shared.d.ts", "default": "./dist/plugin-sdk/provider-catalog-shared.js" diff --git a/scripts/check-no-extension-test-core-imports.ts b/scripts/check-no-extension-test-core-imports.ts index 63fe83f6672..2669a77c7ac 100644 --- a/scripts/check-no-extension-test-core-imports.ts +++ b/scripts/check-no-extension-test-core-imports.ts @@ -37,6 +37,8 @@ const FORBIDDEN_PATTERNS: Array<{ pattern: RegExp; hint: string }> = [ const STATIC_RELATIVE_MODULE_PATTERN = /\b(?:import|export)\b[\s\S]*?\bfrom\s*["']([^"']+)["']/g; const DYNAMIC_RELATIVE_MODULE_PATTERN = /\bimport\s*\(\s*["']([^"']+)["']\s*\)/g; +const MOCK_RELATIVE_MODULE_PATTERN = + /\bvi\.(?:mock|doMock|unmock|doUnmock)\s*\(\s*["']([^"']+)["']/g; const RELATIVE_CORE_HINT = "Use openclaw/plugin-sdk/testing or a focused plugin-sdk test/runtime subpath instead of core internals."; @@ -88,6 +90,7 @@ function collectRelativeCoreImportOffenders( const matches = [ ...content.matchAll(STATIC_RELATIVE_MODULE_PATTERN), ...(opts.includeDynamic ? [...content.matchAll(DYNAMIC_RELATIVE_MODULE_PATTERN)] : []), + ...content.matchAll(MOCK_RELATIVE_MODULE_PATTERN), ]; for (const match of matches) { const specifier = match[1]; diff --git a/scripts/lib/plugin-sdk-entrypoints.json b/scripts/lib/plugin-sdk-entrypoints.json index d362fa059d8..8b4ec34338d 100644 --- a/scripts/lib/plugin-sdk-entrypoints.json +++ b/scripts/lib/plugin-sdk-entrypoints.json @@ -274,6 +274,7 @@ "provider-auth-login", "provider-selection-runtime", "plugin-entry", + "provider-catalog-runtime", "provider-catalog-shared", "provider-entry", "provider-env-vars", diff --git a/src/plugin-sdk/provider-catalog-runtime.ts b/src/plugin-sdk/provider-catalog-runtime.ts new file mode 100644 index 00000000000..ff1a339f0b6 --- /dev/null +++ b/src/plugin-sdk/provider-catalog-runtime.ts @@ -0,0 +1,15 @@ +// Public provider-catalog runtime seams for provider plugin contract tests. + +export { + augmentModelCatalogWithProviderPlugins, + resetProviderRuntimeHookCacheForTest, + resolveProviderBuiltInModelSuppression, +} from "../plugins/provider-runtime.js"; +export { + resolveCatalogHookProviderPluginIds, + resolveOwningPluginIdsForProvider, +} from "../plugins/providers.js"; +export { + isPluginProvidersLoadInFlight, + resolvePluginProviders, +} from "../plugins/providers.runtime.js"; diff --git a/test/helpers/plugins/provider-catalog.ts b/test/helpers/plugins/provider-catalog.ts index 454a3bb669f..79569b4732b 100644 --- a/test/helpers/plugins/provider-catalog.ts +++ b/test/helpers/plugins/provider-catalog.ts @@ -11,7 +11,7 @@ export { } from "../../../src/test-utils/bundled-plugin-public-surface.js"; type ProviderRuntimeCatalogModule = Pick< - typeof import("../../../src/plugins/provider-runtime.js"), + typeof import("openclaw/plugin-sdk/provider-catalog-runtime"), | "augmentModelCatalogWithProviderPlugins" | "resetProviderRuntimeHookCacheForTest" | "resolveProviderBuiltInModelSuppression" @@ -22,7 +22,7 @@ export async function importProviderRuntimeCatalogModule(): Promise