diff --git a/extensions/amazon-bedrock/register.sync.runtime.ts b/extensions/amazon-bedrock/register.sync.runtime.ts index dde1444f7023..863be16de4a0 100644 --- a/extensions/amazon-bedrock/register.sync.runtime.ts +++ b/extensions/amazon-bedrock/register.sync.runtime.ts @@ -11,7 +11,7 @@ import type { ProviderNormalizeResolvedModelContext, } from "openclaw/plugin-sdk/plugin-entry"; import { - ANTHROPIC_BY_MODEL_REPLAY_HOOKS, + buildProviderReplayFamilyHooks, normalizeProviderId, resolveClaudeFable5ModelIdentity, resolveClaudeModelIdentity, @@ -364,7 +364,9 @@ export function registerAmazonBedrockPlugin(api: OpenClawPluginApi): void { ] as const; const deprecatedTemperatureValidationRe = /ValidationException[\s\S]*(?:invalid_request_error[\s\S]*)?temperature[\s\S]*deprecated|ValidationException[\s\S]*deprecated[\s\S]*temperature/i; - const anthropicByModelReplayHooks = ANTHROPIC_BY_MODEL_REPLAY_HOOKS; + const anthropicByModelReplayHooks = buildProviderReplayFamilyHooks({ + family: "anthropic-by-model", + }); const startupPluginConfig = (api.pluginConfig ?? {}) as AmazonBedrockPluginConfig; registerApiProvider( diff --git a/extensions/anthropic-vertex/index.ts b/extensions/anthropic-vertex/index.ts index a7b3a6a4762e..391d12ef1965 100644 --- a/extensions/anthropic-vertex/index.ts +++ b/extensions/anthropic-vertex/index.ts @@ -5,7 +5,7 @@ import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry"; import { readConfiguredProviderCatalogEntries } from "openclaw/plugin-sdk/provider-catalog-shared"; import { - NATIVE_ANTHROPIC_REPLAY_HOOKS, + buildProviderReplayFamilyHooks, resolveClaudeThinkingProfile, } from "openclaw/plugin-sdk/provider-model-shared"; import { @@ -48,7 +48,7 @@ export default definePluginEntry({ }, }, resolveConfigApiKey: ({ env }) => resolveAnthropicVertexConfigApiKey(env), - ...NATIVE_ANTHROPIC_REPLAY_HOOKS, + ...buildProviderReplayFamilyHooks({ family: "native-anthropic-by-model" }), normalizeResolvedModel: ({ modelId, model }) => normalizeAnthropicVertexResolvedModel(modelId, model), resolveThinkingProfile: ({ modelId, params }) => diff --git a/extensions/anthropic/register.runtime.ts b/extensions/anthropic/register.runtime.ts index 98a32359e4ea..90455d84a7e6 100644 --- a/extensions/anthropic/register.runtime.ts +++ b/extensions/anthropic/register.runtime.ts @@ -25,9 +25,9 @@ import { validateAnthropicSetupToken, } from "openclaw/plugin-sdk/provider-auth"; import { + buildProviderReplayFamilyHooks, cloneFirstTemplateModel, modelCostsEqual, - NATIVE_ANTHROPIC_REPLAY_HOOKS, type ProviderPlugin, resolveClaudeFable5ModelIdentity, resolveClaudeModelIdentity, @@ -896,7 +896,7 @@ export function buildAnthropicProvider(): ProviderPlugin { : undefined, // Publish Claude CLI rows through the provider catalog hook. augmentModelCatalog: () => buildClaudeCliCatalogEntries(), - ...NATIVE_ANTHROPIC_REPLAY_HOOKS, + ...buildProviderReplayFamilyHooks({ family: "native-anthropic-by-model" }), isModernModelRef: ({ provider, modelId }) => matchesAnthropicModernModel(modelId) && (!isAnthropicMandatoryClaude5Model(modelId) || diff --git a/extensions/arcee/index.ts b/extensions/arcee/index.ts index ed9946356d1e..6a1442429f1d 100644 --- a/extensions/arcee/index.ts +++ b/extensions/arcee/index.ts @@ -8,7 +8,7 @@ import { readConfiguredProviderCatalogEntries, type ProviderCatalogContext, } from "openclaw/plugin-sdk/provider-catalog-shared"; -import { OPENAI_COMPATIBLE_REPLAY_HOOKS } from "openclaw/plugin-sdk/provider-model-shared"; +import { buildProviderReplayFamilyHooks } from "openclaw/plugin-sdk/provider-model-shared"; import { applyArceeConfig, applyArceeOpenRouterConfig, @@ -141,7 +141,7 @@ export default definePluginEntry({ } : undefined; }, - ...OPENAI_COMPATIBLE_REPLAY_HOOKS, + ...buildProviderReplayFamilyHooks({ family: "openai-compatible" }), }); }, }); diff --git a/extensions/deepinfra/index.ts b/extensions/deepinfra/index.ts index 074bad919355..c1b0385a28c4 100644 --- a/extensions/deepinfra/index.ts +++ b/extensions/deepinfra/index.ts @@ -5,7 +5,7 @@ import { readConfiguredProviderCatalogEntries, } from "openclaw/plugin-sdk/provider-catalog-shared"; import { defineSingleProviderPluginEntry } from "openclaw/plugin-sdk/provider-entry"; -import { PASSTHROUGH_GEMINI_REPLAY_HOOKS } from "openclaw/plugin-sdk/provider-model-shared"; +import { buildProviderReplayFamilyHooks } from "openclaw/plugin-sdk/provider-model-shared"; import { createOpenRouterWrapper, isProxyReasoningUnsupported, @@ -104,7 +104,7 @@ export default defineSingleProviderPluginEntry({ normalizeConfig: ({ providerConfig }) => providerConfig, normalizeTransport: ({ api, baseUrl }) => baseUrl === "https://api.deepinfra.com/v1/openai" ? { api, baseUrl } : undefined, - ...PASSTHROUGH_GEMINI_REPLAY_HOOKS, + ...buildProviderReplayFamilyHooks({ family: "passthrough-gemini" }), wrapStreamFn: (ctx) => { const thinkingLevel = isProxyReasoningUnsupported(ctx.modelId) ? undefined diff --git a/extensions/feishu/src/outbound-delivery.test.ts b/extensions/feishu/src/outbound-delivery.test.ts index c8fd459c723b..3dd5a4c84580 100644 --- a/extensions/feishu/src/outbound-delivery.test.ts +++ b/extensions/feishu/src/outbound-delivery.test.ts @@ -1,8 +1,8 @@ // Feishu tests cover the shared outbound delivery path. +import { sendDurableMessageBatch } from "openclaw/plugin-sdk/channel-outbound"; import { createOutboundTestPlugin, createTestRegistry, - deliverOutboundPayloads, releasePinnedPluginChannelRegistry, resetGlobalHookRunner, setActivePluginRegistry, @@ -60,7 +60,7 @@ describe("Feishu outbound shared delivery", () => { }); it("routes oversized presentation media through one media send and chunked fallback text", async () => { - await deliverOutboundPayloads({ + await sendDurableMessageBatch({ cfg: {}, channel: "feishu", to: "chat_1", diff --git a/extensions/fireworks/index.ts b/extensions/fireworks/index.ts index a1ea6bf34532..644f4685a4c7 100644 --- a/extensions/fireworks/index.ts +++ b/extensions/fireworks/index.ts @@ -2,10 +2,10 @@ import type { ProviderResolveDynamicModelContext } from "openclaw/plugin-sdk/plugin-entry"; import { defineSingleProviderPluginEntry } from "openclaw/plugin-sdk/provider-entry"; import { + buildProviderReplayFamilyHooks, cloneFirstTemplateModel, DEFAULT_CONTEXT_TOKENS, normalizeModelCompat, - OPENAI_COMPATIBLE_REPLAY_HOOKS, } from "openclaw/plugin-sdk/provider-model-shared"; import { isFireworksKimiModelId } from "./model-id.js"; import { applyFireworksConfig, FIREWORKS_DEFAULT_MODEL_REF } from "./onboard.js"; @@ -97,7 +97,7 @@ export default defineSingleProviderPluginEntry({ buildProvider: buildFireworksProvider, allowExplicitBaseUrl: true, }, - ...OPENAI_COMPATIBLE_REPLAY_HOOKS, + ...buildProviderReplayFamilyHooks({ family: "openai-compatible" }), wrapStreamFn: wrapFireworksProviderStream, resolveThinkingProfile: ({ modelId }) => resolveFireworksThinkingProfile(modelId), resolveDynamicModel: (ctx) => resolveFireworksDynamicModel(ctx), diff --git a/extensions/kilocode/index.ts b/extensions/kilocode/index.ts index f5bfb3ddf07e..fac6ee34b2fc 100644 --- a/extensions/kilocode/index.ts +++ b/extensions/kilocode/index.ts @@ -1,7 +1,7 @@ // Kilocode plugin entrypoint registers its OpenClaw integration. import { readConfiguredProviderCatalogEntries } from "openclaw/plugin-sdk/provider-catalog-shared"; import { defineSingleProviderPluginEntry } from "openclaw/plugin-sdk/provider-entry"; -import { PASSTHROUGH_GEMINI_REPLAY_HOOKS } from "openclaw/plugin-sdk/provider-model-shared"; +import { buildProviderReplayFamilyHooks } from "openclaw/plugin-sdk/provider-model-shared"; import { applyKilocodeConfig, KILOCODE_DEFAULT_MODEL_REF } from "./onboard.js"; import { buildKilocodeProvider, buildKilocodeProviderWithDiscovery } from "./provider-catalog.js"; import { wrapKilocodeProviderStream } from "./stream.js"; @@ -37,7 +37,7 @@ export default defineSingleProviderPluginEntry({ config, providerId: PROVIDER_ID, }), - ...PASSTHROUGH_GEMINI_REPLAY_HOOKS, + ...buildProviderReplayFamilyHooks({ family: "passthrough-gemini" }), wrapStreamFn: wrapKilocodeProviderStream, isCacheTtlEligible: (ctx) => ctx.modelId.startsWith("anthropic/"), }, diff --git a/extensions/matrix/runtime-api.ts b/extensions/matrix/runtime-api.ts index 721b53b650cc..68ee4130125b 100644 --- a/extensions/matrix/runtime-api.ts +++ b/extensions/matrix/runtime-api.ts @@ -34,7 +34,6 @@ export { createPinnedDispatcher, resolvePinnedHostnameWithPolicy, ssrfPolicyFromDangerouslyAllowPrivateNetwork, - ssrfPolicyFromAllowPrivateNetwork, type LookupFn, type SsrFPolicy, } from "openclaw/plugin-sdk/ssrf-runtime"; diff --git a/extensions/matrix/src/runtime-api.ts b/extensions/matrix/src/runtime-api.ts index 238c8f87aaf8..0db353856ee2 100644 --- a/extensions/matrix/src/runtime-api.ts +++ b/extensions/matrix/src/runtime-api.ts @@ -71,7 +71,6 @@ export { isPrivateOrLoopbackHost, resolvePinnedHostnameWithPolicy, ssrfPolicyFromDangerouslyAllowPrivateNetwork, - ssrfPolicyFromAllowPrivateNetwork, type LookupFn, type SsrFPolicy, } from "openclaw/plugin-sdk/ssrf-runtime"; diff --git a/extensions/mattermost/src/outbound-delivery.test.ts b/extensions/mattermost/src/outbound-delivery.test.ts index ae19382dcc05..e86a8856b2bd 100644 --- a/extensions/mattermost/src/outbound-delivery.test.ts +++ b/extensions/mattermost/src/outbound-delivery.test.ts @@ -1,7 +1,7 @@ // Mattermost tests cover the shared outbound delivery path. +import { sendDurableMessageBatch } from "openclaw/plugin-sdk/channel-outbound"; import { createTestRegistry, - deliverOutboundPayloads, releasePinnedPluginChannelRegistry, setActivePluginRegistry, } from "openclaw/plugin-sdk/channel-test-helpers"; @@ -43,7 +43,7 @@ describe("Mattermost outbound delivery", () => { expected: "The pipeline has 3 open deals.", }, ])("sends sanitized $name through the channel handler", async ({ text, expected }) => { - await deliverOutboundPayloads({ + await sendDurableMessageBatch({ cfg: {}, channel: "mattermost", to: "channel:team-1", diff --git a/extensions/meta/index.ts b/extensions/meta/index.ts index 97b723ca3ba7..f3854356902a 100644 --- a/extensions/meta/index.ts +++ b/extensions/meta/index.ts @@ -2,7 +2,7 @@ * Meta provider plugin entrypoint. */ import { defineSingleProviderPluginEntry } from "openclaw/plugin-sdk/provider-entry"; -import { OPENAI_COMPATIBLE_REPLAY_HOOKS } from "openclaw/plugin-sdk/provider-model-shared"; +import { buildProviderReplayFamilyHooks } from "openclaw/plugin-sdk/provider-model-shared"; import { applyMetaConfig, META_DEFAULT_MODEL_REF } from "./onboard.js"; import { buildMetaProvider } from "./provider-catalog.js"; import { wrapMetaProviderStream } from "./stream.js"; @@ -40,7 +40,7 @@ export default defineSingleProviderPluginEntry({ buildProvider: buildMetaProvider, buildStaticProvider: buildMetaProvider, }, - ...OPENAI_COMPATIBLE_REPLAY_HOOKS, + ...buildProviderReplayFamilyHooks({ family: "openai-compatible" }), wrapStreamFn: wrapMetaProviderStream, resolveThinkingProfile: ({ modelId }) => resolveMetaThinkingProfile(modelId), }, diff --git a/extensions/microsoft-foundry/provider.ts b/extensions/microsoft-foundry/provider.ts index 2bf1e071b9c7..cfdf67e3e4f5 100644 --- a/extensions/microsoft-foundry/provider.ts +++ b/extensions/microsoft-foundry/provider.ts @@ -6,7 +6,7 @@ import { type ModelProviderConfig, type ProviderPlugin, } from "openclaw/plugin-sdk/provider-model-shared"; -import { OPENAI_RESPONSES_STREAM_HOOKS } from "openclaw/plugin-sdk/provider-stream-family"; +import { buildProviderStreamFamilyHooks } from "openclaw/plugin-sdk/provider-stream-family"; import { apiKeyAuthMethod, entraIdAuthMethod } from "./auth.js"; import { prepareFoundryRuntimeAuth } from "./runtime.js"; import { @@ -25,7 +25,8 @@ import { type FoundryProviderHooks = Pick; -const wrapOpenAIResponsesStreamFn = OPENAI_RESPONSES_STREAM_HOOKS.wrapStreamFn; +const openAIResponsesStreamHooks = buildProviderStreamFamilyHooks("openai-responses-defaults"); +const wrapOpenAIResponsesStreamFn = openAIResponsesStreamHooks.wrapStreamFn; const wrapMicrosoftFoundryStreamFn: NonNullable = (ctx) => { if (ctx.model?.api !== "openai-responses") { diff --git a/extensions/minimax/provider-registration.ts b/extensions/minimax/provider-registration.ts index 47f5cd73b2ed..72bc9105a785 100644 --- a/extensions/minimax/provider-registration.ts +++ b/extensions/minimax/provider-registration.ts @@ -21,7 +21,7 @@ import { buildProviderReplayFamilyHooks, normalizeModelCompat, } from "openclaw/plugin-sdk/provider-model-shared"; -import { MINIMAX_FAST_MODE_STREAM_HOOKS } from "openclaw/plugin-sdk/provider-stream-family"; +import { buildProviderStreamFamilyHooks } from "openclaw/plugin-sdk/provider-stream-family"; import { fetchMinimaxUsage } from "openclaw/plugin-sdk/provider-usage"; import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime"; import { @@ -63,7 +63,7 @@ const HYBRID_ANTHROPIC_OPENAI_REPLAY_HOOKS = buildProviderReplayFamilyHooks({ }); const MINIMAX_PROVIDER_HOOKS = { ...HYBRID_ANTHROPIC_OPENAI_REPLAY_HOOKS, - ...MINIMAX_FAST_MODE_STREAM_HOOKS, + ...buildProviderStreamFamilyHooks("minimax-fast-mode"), resolveReasoningOutputMode: () => "native" as const, resolveThinkingProfile: ({ modelId }: { modelId: string }) => resolveMinimaxThinkingProfile(modelId), diff --git a/extensions/moonshot/index.ts b/extensions/moonshot/index.ts index 582043b78458..ac64856d1c1d 100644 --- a/extensions/moonshot/index.ts +++ b/extensions/moonshot/index.ts @@ -1,7 +1,7 @@ // Moonshot plugin entrypoint registers its OpenClaw integration. import { defineSingleProviderPluginEntry } from "openclaw/plugin-sdk/provider-entry"; import { buildOpenAICompatibleReplayPolicy } from "openclaw/plugin-sdk/provider-model-shared"; -import { MOONSHOT_THINKING_STREAM_HOOKS } from "openclaw/plugin-sdk/provider-stream-family"; +import { buildProviderStreamFamilyHooks } from "openclaw/plugin-sdk/provider-stream-family"; import { applyMoonshotNativeStreamingUsageCompat } from "./api.js"; import { moonshotMediaUnderstandingProvider } from "./media-understanding-provider.js"; import { @@ -14,7 +14,7 @@ import { KIMI_K2_7_CODE_MODEL_ID, resolveThinkingProfile } from "./provider-poli import { createKimiWebSearchProvider } from "./src/kimi-web-search-provider.js"; const PROVIDER_ID = "moonshot"; -const moonshotThinkingStreamHooks = MOONSHOT_THINKING_STREAM_HOOKS; +const moonshotThinkingStreamHooks = buildProviderStreamFamilyHooks("moonshot-thinking"); export default defineSingleProviderPluginEntry({ id: PROVIDER_ID, diff --git a/extensions/ollama/index.ts b/extensions/ollama/index.ts index 7865b5334529..73aee5425b61 100644 --- a/extensions/ollama/index.ts +++ b/extensions/ollama/index.ts @@ -25,7 +25,7 @@ import type { } from "openclaw/plugin-sdk/provider-model-shared"; import { buildOpenAICompatibleReplayPolicy, - OPENAI_COMPATIBLE_REPLAY_HOOKS, + buildProviderReplayFamilyHooks, } from "openclaw/plugin-sdk/provider-model-shared"; import { buildOllamaModelDefinition, @@ -540,7 +540,7 @@ export default definePluginEntry({ ) ?? OLLAMA_CLOUD_BASE_URL, }); }, - ...OPENAI_COMPATIBLE_REPLAY_HOOKS, + ...buildProviderReplayFamilyHooks({ family: "openai-compatible" }), buildReplayPolicy: (ctx) => ctx.modelApi === "ollama" ? buildNativeOllamaReplayPolicy() @@ -671,7 +671,7 @@ export default definePluginEntry({ ), }); }, - ...OPENAI_COMPATIBLE_REPLAY_HOOKS, + ...buildProviderReplayFamilyHooks({ family: "openai-compatible" }), buildReplayPolicy: (ctx) => ctx.modelApi === "ollama" ? buildNativeOllamaReplayPolicy() diff --git a/extensions/openai/openai-chatgpt-oauth-types.runtime.ts b/extensions/openai/openai-chatgpt-oauth-types.runtime.ts index 1847eb1ec603..827ac12e8055 100644 --- a/extensions/openai/openai-chatgpt-oauth-types.runtime.ts +++ b/extensions/openai/openai-chatgpt-oauth-types.runtime.ts @@ -4,9 +4,7 @@ export type { OAuthCredentials, OAuthLoginCallbacks, OAuthPrompt, - OAuthProvider, OAuthProviderId, - OAuthProviderInfo, OAuthProviderInterface, OAuthSelectOption, OAuthSelectPrompt, diff --git a/extensions/openai/shared.ts b/extensions/openai/shared.ts index d912935cd769..7e658b21509d 100644 --- a/extensions/openai/shared.ts +++ b/extensions/openai/shared.ts @@ -6,7 +6,7 @@ import { matchesExactOrPrefix, type ProviderPlugin, } from "openclaw/plugin-sdk/provider-model-shared"; -import { OPENAI_RESPONSES_STREAM_HOOKS } from "openclaw/plugin-sdk/provider-stream-family"; +import { buildProviderStreamFamilyHooks } from "openclaw/plugin-sdk/provider-stream-family"; import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime"; import { createOpenAINativeWebSearchWrapper } from "./native-web-search.js"; import { buildOpenAIReplayPolicy } from "./replay-policy.js"; @@ -78,7 +78,8 @@ const resolveOpenAIResponsesWebSocketSessionPolicy: NonNullable< OpenAIResponsesProviderHooks["resolveWebSocketSessionPolicy"] > = (ctx) => resolveOpenAIWebSocketSessionPolicy(ctx); -const wrapOpenAIResponsesStreamFn = OPENAI_RESPONSES_STREAM_HOOKS.wrapStreamFn; +const openAIResponsesStreamHooks = buildProviderStreamFamilyHooks("openai-responses-defaults"); +const wrapOpenAIResponsesStreamFn = openAIResponsesStreamHooks.wrapStreamFn; const wrapOpenAIResponsesProviderStreamFn: NonNullable< OpenAIResponsesProviderHooks["wrapStreamFn"] > = (ctx) => @@ -94,7 +95,7 @@ export function buildOpenAIResponsesProviderHooks(options?: { return { buildReplayPolicy: buildOpenAIReplayPolicy, prepareExtraParams: (ctx) => defaultOpenAIResponsesExtraParams(ctx.extraParams, options), - ...OPENAI_RESPONSES_STREAM_HOOKS, + ...openAIResponsesStreamHooks, wrapStreamFn: wrapOpenAIResponsesProviderStreamFn, resolveTransportTurnState: resolveOpenAIResponsesTransportTurnState, resolveWebSocketSessionPolicy: resolveOpenAIResponsesWebSocketSessionPolicy, diff --git a/extensions/opencode-go/index.ts b/extensions/opencode-go/index.ts index fd157ae714e7..dbd3bd20f869 100644 --- a/extensions/opencode-go/index.ts +++ b/extensions/opencode-go/index.ts @@ -1,7 +1,7 @@ // Opencode Go plugin entrypoint registers its OpenClaw integration. import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry"; import { createProviderApiKeyAuthMethod } from "openclaw/plugin-sdk/provider-auth-api-key"; -import { PASSTHROUGH_GEMINI_REPLAY_HOOKS } from "openclaw/plugin-sdk/provider-model-shared"; +import { buildProviderReplayFamilyHooks } from "openclaw/plugin-sdk/provider-model-shared"; import { applyOpencodeGoConfig, OPENCODE_GO_DEFAULT_MODEL_REF } from "./api.js"; import { opencodeGoMediaUnderstandingProvider } from "./media-understanding-provider.js"; import { @@ -135,7 +135,7 @@ export default definePluginEntry({ }, }, augmentModelCatalog: () => listOpencodeGoModelCatalogEntries(), - ...PASSTHROUGH_GEMINI_REPLAY_HOOKS, + ...buildProviderReplayFamilyHooks({ family: "passthrough-gemini" }), wrapStreamFn: (ctx) => createOpencodeGoWrapper(ctx.streamFn, ctx.thinkingLevel), isModernModelRef: () => true, }); diff --git a/extensions/opencode/index.ts b/extensions/opencode/index.ts index 3ac57e38f527..a4e360426706 100644 --- a/extensions/opencode/index.ts +++ b/extensions/opencode/index.ts @@ -2,8 +2,8 @@ import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry"; import { createProviderApiKeyAuthMethod } from "openclaw/plugin-sdk/provider-auth-api-key"; import { + buildProviderReplayFamilyHooks, matchesExactOrPrefix, - PASSTHROUGH_GEMINI_REPLAY_HOOKS, resolveClaudeThinkingProfile, } from "openclaw/plugin-sdk/provider-model-shared"; import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime"; @@ -149,7 +149,7 @@ export default definePluginEntry({ }, }, augmentModelCatalog: () => listOpencodeZenModelCatalogEntries(), - ...PASSTHROUGH_GEMINI_REPLAY_HOOKS, + ...buildProviderReplayFamilyHooks({ family: "passthrough-gemini" }), isModernModelRef: ({ modelId }) => isModernOpencodeModel(modelId), resolveThinkingProfile: ({ modelId }) => resolveClaudeThinkingProfile(modelId), }); diff --git a/extensions/openrouter/index.ts b/extensions/openrouter/index.ts index 3df22033a3ec..7a49eb7f73cc 100644 --- a/extensions/openrouter/index.ts +++ b/extensions/openrouter/index.ts @@ -8,8 +8,8 @@ import { } from "openclaw/plugin-sdk/plugin-entry"; import { createProviderApiKeyAuthMethod } from "openclaw/plugin-sdk/provider-auth-api-key"; import { + buildProviderReplayFamilyHooks, DEFAULT_CONTEXT_TOKENS, - PASSTHROUGH_GEMINI_REPLAY_HOOKS, } from "openclaw/plugin-sdk/provider-model-shared"; import { getOpenRouterModelCapabilities, @@ -263,7 +263,10 @@ export default definePluginEntry({ return OPENROUTER_CACHE_TTL_MODEL_PREFIXES.some((prefix) => modelId.startsWith(prefix)); } - const passthroughReplayHook = PASSTHROUGH_GEMINI_REPLAY_HOOKS.buildReplayPolicy; + const passthroughGeminiReplayHooks = buildProviderReplayFamilyHooks({ + family: "passthrough-gemini", + }); + const passthroughReplayHook = passthroughGeminiReplayHooks.buildReplayPolicy; function buildOpenRouterReplayPolicy(ctx: ProviderReplayPolicyContext): ProviderReplayPolicy { const base = passthroughReplayHook?.(ctx) ?? {}; // OpenRouter proxies Mistral, which uses non-base62 tool_call_ids and @@ -352,7 +355,7 @@ export default definePluginEntry({ } : undefined; }, - ...PASSTHROUGH_GEMINI_REPLAY_HOOKS, + ...passthroughGeminiReplayHooks, buildReplayPolicy: buildOpenRouterReplayPolicy, resolveReasoningOutputMode: () => "native", supportsXHighThinking: ({ modelId }) => supportsOpenRouterXHighThinking(modelId), diff --git a/extensions/openrouter/stream.ts b/extensions/openrouter/stream.ts index b08e0b57b96b..ccc47853bd53 100644 --- a/extensions/openrouter/stream.ts +++ b/extensions/openrouter/stream.ts @@ -1,7 +1,7 @@ // Openrouter plugin module implements stream behavior. import type { StreamFn } from "openclaw/plugin-sdk/agent-core"; import type { ProviderWrapStreamFnContext } from "openclaw/plugin-sdk/plugin-entry"; -import { OPENROUTER_THINKING_STREAM_HOOKS } from "openclaw/plugin-sdk/provider-stream-family"; +import { buildProviderStreamFamilyHooks } from "openclaw/plugin-sdk/provider-stream-family"; import { createPayloadPatchStreamWrapper } from "openclaw/plugin-sdk/provider-stream-shared"; import { createSubsystemLogger } from "openclaw/plugin-sdk/runtime-env"; import { isOpenRouterDeepSeekV4ModelId } from "./models.js"; @@ -12,6 +12,7 @@ import { } from "./provider-catalog.js"; const log = createSubsystemLogger("openrouter-stream"); +const openRouterThinkingStreamHooks = buildProviderStreamFamilyHooks("openrouter-thinking"); function readString(value: unknown): string | undefined { return typeof value === "string" ? value.trim() : undefined; @@ -312,7 +313,7 @@ export function wrapOpenRouterProviderStream( const routedStreamFn = providerRouting ? injectOpenRouterRouting(ctx.streamFn, providerRouting) : ctx.streamFn; - const wrapStreamFn = OPENROUTER_THINKING_STREAM_HOOKS.wrapStreamFn ?? undefined; + const wrapStreamFn = openRouterThinkingStreamHooks.wrapStreamFn ?? undefined; if (!wrapStreamFn) { return createOpenRouterAnthropicPrefillWrapper( createOpenRouterAuthHeaderWrapper( diff --git a/extensions/slack/src/outbound-delivery.test.ts b/extensions/slack/src/outbound-delivery.test.ts index a816fbf0bb5b..81ca39e7b879 100644 --- a/extensions/slack/src/outbound-delivery.test.ts +++ b/extensions/slack/src/outbound-delivery.test.ts @@ -1,10 +1,10 @@ // Slack tests cover outbound delivery plugin behavior. +import { sendDurableMessageBatch } from "openclaw/plugin-sdk/channel-outbound"; import { addTestHook, createEmptyPluginRegistry, createOutboundTestPlugin, createTestRegistry, - deliverOutboundPayloads, initializeGlobalHookRunner, releasePinnedPluginChannelRegistry, resetGlobalHookRunner, @@ -68,7 +68,7 @@ describe("slack outbound shared hook wiring", () => { }); initializeGlobalHookRunner(hookRegistry); - await deliverOutboundPayloads({ + await sendDurableMessageBatch({ cfg, channel: "slack", to: "C123", @@ -99,7 +99,7 @@ describe("slack outbound shared hook wiring", () => { }); it("passes replyToId as Slack threadTs for threaded outbound delivery", async () => { - await deliverOutboundPayloads({ + await sendDurableMessageBatch({ cfg, channel: "slack", to: "C123", @@ -131,7 +131,7 @@ describe("slack outbound shared hook wiring", () => { }); initializeGlobalHookRunner(hookRegistry); - const result = await deliverOutboundPayloads({ + const result = await sendDurableMessageBatch({ cfg, channel: "slack", to: "C123", @@ -142,6 +142,6 @@ describe("slack outbound shared hook wiring", () => { expect(handler).toHaveBeenCalledTimes(1); expect(sendMessageSlackMock).not.toHaveBeenCalled(); - expect(result).toStrictEqual([]); + expect(result).toMatchObject({ status: "suppressed", results: [] }); }); }); diff --git a/extensions/tlon/api.ts b/extensions/tlon/api.ts index 8e946ba11395..5bba54aba045 100644 --- a/extensions/tlon/api.ts +++ b/extensions/tlon/api.ts @@ -10,7 +10,6 @@ export { type RuntimeEnv, SsrFBlockedError, type SsrFPolicy, - ssrfPolicyFromAllowPrivateNetwork, ssrfPolicyFromDangerouslyAllowPrivateNetwork, } from "./runtime-api.js"; export { tlonPlugin } from "./src/channel.js"; diff --git a/extensions/tlon/runtime-api.ts b/extensions/tlon/runtime-api.ts index 7301a4b62b68..c4fa76f479dd 100644 --- a/extensions/tlon/runtime-api.ts +++ b/extensions/tlon/runtime-api.ts @@ -9,7 +9,6 @@ export { createLoggerBackedRuntime } from "./src/logger-runtime.js"; export { fetchWithSsrFGuard, isBlockedHostnameOrIp, - ssrfPolicyFromAllowPrivateNetwork, ssrfPolicyFromDangerouslyAllowPrivateNetwork, type LookupFn, type SsrFPolicy, diff --git a/extensions/tlon/src/outbound-tool-trace-sanitize.test.ts b/extensions/tlon/src/outbound-tool-trace-sanitize.test.ts index fbdcbe13ba3c..0a6f379f6d62 100644 --- a/extensions/tlon/src/outbound-tool-trace-sanitize.test.ts +++ b/extensions/tlon/src/outbound-tool-trace-sanitize.test.ts @@ -3,9 +3,9 @@ // cannot drift apart unnoticed. import http from "node:http"; import { expectDefined } from "@openclaw/normalization-core"; +import { sendDurableMessageBatch } from "openclaw/plugin-sdk/channel-outbound"; import { createTestRegistry, - deliverOutboundPayloads, releasePinnedPluginChannelRegistry, setActivePluginRegistry, } from "openclaw/plugin-sdk/channel-test-helpers"; @@ -102,7 +102,7 @@ describe("tlon outbound assistant-visible sanitization", () => { }, } as OpenClawConfig; - await deliverOutboundPayloads({ + await sendDurableMessageBatch({ cfg, channel: "tlon", to: "~sampel-palnet", @@ -121,7 +121,7 @@ describe("tlon outbound assistant-visible sanitization", () => { skipQueue: true, }); - await deliverOutboundPayloads({ + await sendDurableMessageBatch({ cfg, channel: "tlon", to: "chat/~host-ship/general", @@ -133,7 +133,7 @@ describe("tlon outbound assistant-visible sanitization", () => { skipQueue: true, }); - await deliverOutboundPayloads({ + await sendDurableMessageBatch({ cfg, channel: "tlon", to: "~sampel-palnet", diff --git a/extensions/whatsapp/src/send.delivery-recovery.test.ts b/extensions/whatsapp/src/send.delivery-recovery.test.ts index db48c248c44e..4269b1b84490 100644 --- a/extensions/whatsapp/src/send.delivery-recovery.test.ts +++ b/extensions/whatsapp/src/send.delivery-recovery.test.ts @@ -1,9 +1,9 @@ // Whatsapp tests cover the durable outbound handoff across startup recovery. +import { sendDurableMessageBatch } from "openclaw/plugin-sdk/channel-outbound"; import { createEmptyPluginRegistry, createOutboundTestPlugin, createTestRegistry, - deliverOutboundPayloads, releasePinnedPluginChannelRegistry, setActivePluginRegistry, } from "openclaw/plugin-sdk/channel-test-helpers"; @@ -76,15 +76,18 @@ describe("WhatsApp delivery recovery", () => { it("keeps pre-connect recovery replayable, then sends exactly once after connect", async () => { await withStateDirEnv("openclaw-whatsapp-delivery-recovery-", async ({ stateDir }) => { - const initialError = await deliverOutboundPayloads({ + const initialResult = await sendDurableMessageBatch({ cfg, channel: "whatsapp", to: "+1555", payloads: [{ text: "queued before listener startup" }], - queuePolicy: "required", - }).catch((err: unknown) => err); - expect(initialError).toMatchObject({ - cause: expect.any(PlatformMessageNotDispatchedError), + durability: "required", + }); + expect(initialResult).toMatchObject({ + status: "failed", + error: { + cause: expect.any(PlatformMessageNotDispatchedError), + }, }); const preConnectLog = await drainDefaultWhatsAppDeliveries(stateDir); diff --git a/extensions/xai/index.ts b/extensions/xai/index.ts index 3e24fb51ed16..2a139d1b5af3 100644 --- a/extensions/xai/index.ts +++ b/extensions/xai/index.ts @@ -2,7 +2,7 @@ import { createLazyRuntimeModule } from "openclaw/plugin-sdk/lazy-runtime"; // Xai plugin entrypoint registers its OpenClaw integration. import type { OpenClawPluginToolContext } from "openclaw/plugin-sdk/plugin-entry"; import { defineSingleProviderPluginEntry } from "openclaw/plugin-sdk/provider-entry"; -import { OPENAI_COMPATIBLE_REPLAY_HOOKS } from "openclaw/plugin-sdk/provider-model-shared"; +import { buildProviderReplayFamilyHooks } from "openclaw/plugin-sdk/provider-model-shared"; import { defaultToolStreamExtraParams } from "openclaw/plugin-sdk/provider-stream-shared"; import { jsonResult } from "openclaw/plugin-sdk/provider-web-search"; import { @@ -249,7 +249,7 @@ export default defineSingleProviderPluginEntry({ provider: buildXaiProvider(), }), }, - ...OPENAI_COMPATIBLE_REPLAY_HOOKS, + ...buildProviderReplayFamilyHooks({ family: "openai-compatible" }), prepareExtraParams: (ctx) => defaultToolStreamExtraParams(ctx.extraParams), wrapStreamFn: wrapXaiProviderStream, // Provider-specific fallback auth stays owned by the xAI plugin so core diff --git a/scripts/check-changed.d.mts b/scripts/check-changed.d.mts index 31d526ef6f39..ff3fba212433 100644 --- a/scripts/check-changed.d.mts +++ b/scripts/check-changed.d.mts @@ -46,6 +46,7 @@ export function shouldRunRuntimeSidecarBaselineCheck(paths: string[]): boolean; export function shouldRunSqliteSessionSchemaBaselineCheck(paths: string[]): boolean; export function shouldRunPluginSdkApiBaselineCheck(paths: string[]): boolean; export function shouldRunPluginSdkSurfaceChecks(paths: string[]): boolean; +export function shouldRunDeprecationHygieneChecks(paths: string[]): boolean; export function shouldRunCanvasA2uiNativeResourceCheck(paths: string[]): boolean; export function shouldRunAppcastOwnerTest(paths: string[]): boolean; export function shouldRunTestTempCreationReport(paths: string[]): boolean; diff --git a/scripts/check-changed.mjs b/scripts/check-changed.mjs index 2f4ae3a6a704..c084624f8560 100644 --- a/scripts/check-changed.mjs +++ b/scripts/check-changed.mjs @@ -43,6 +43,8 @@ const PLUGIN_SDK_API_BASELINE_PATH_RE = /^(?:src\/|packages\/|extensions\/|pnpm-lock\.yaml$|tsconfig\.json$|scripts\/(?:generate-plugin-sdk-api-baseline\.ts|lib\/plugin-sdk-(?:doc-metadata\.ts|entries\.mjs|entrypoints\.json|private-local-only-subpaths\.json))|docs\/\.generated\/plugin-sdk-api-baseline\.sha256$)/u; const PLUGIN_SDK_SURFACE_PATH_RE = /^(?:package\.json$|src\/plugin-sdk\/|scripts\/(?:plugin-sdk-surface-report\.mjs|sync-plugin-sdk-exports\.mjs|lib\/plugin-sdk-(?:declaration-budget\.mjs|deprecated-barrel-subpaths\.json|deprecated-public-subpaths\.json|entries\.mjs|entrypoints\.json|private-local-only-subpaths\.json)))/u; +const DEPRECATION_HYGIENE_PATH_RE = + /^(?:package\.json$|src\/|extensions\/|packages\/|scripts\/(?:check-deprecated-api-usage\.mjs$|plugin-boundary-report\.ts$|lib\/plugin-sdk))/u; const CANVAS_A2UI_NATIVE_RESOURCE_PATH_RE = /^(?:pnpm-lock\.yaml$|apps\/shared\/OpenClawKit\/Sources\/OpenClawKit\/Resources\/CanvasA2UI\/|extensions\/canvas\/(?:package\.json$|scripts\/bundle-a2ui\.mjs$|src\/host\/a2ui(?:\/(?:index\.html|a2ui\.bundle\.js|\.bundle\.hash)$|-app\/))|scripts\/(?:bundle-a2ui|sync-native-a2ui)\.mjs$)/u; const CONTROL_UI_I18N_VERIFY_PATH_RE = @@ -311,6 +313,13 @@ export function shouldRunPluginSdkSurfaceChecks(paths) { ); } +/** Returns whether changed files can alter deprecated API or plugin-boundary results. */ +export function shouldRunDeprecationHygieneChecks(paths) { + return paths.some((changedPath) => + DEPRECATION_HYGIENE_PATH_RE.test(normalizeChangedPath(changedPath)), + ); +} + export function shouldRunCanvasA2uiNativeResourceCheck(paths) { return paths.some((changedPath) => CANVAS_A2UI_NATIVE_RESOURCE_PATH_RE.test(normalizeChangedPath(changedPath)), @@ -462,6 +471,12 @@ export function createChangedCheckPlan(result, options = {}) { add("Plugin SDK package exports", ["plugin-sdk:check-exports"]); add("Plugin SDK surface budget", ["plugin-sdk:surface:check"]); } + if (result.lanes.all || shouldRunDeprecationHygieneChecks(result.paths)) { + add("deprecated API usage", ["check:deprecated-api-usage"]); + // After 2026-07-24, lapsed compatibility windows intentionally fail this gate + // until their scheduled deletion PRs land. + add("plugin boundaries", ["plugins:boundary-report:ci"]); + } if (shouldRunCanvasA2uiNativeResourceCheck(result.paths)) { addCommand( "Canvas A2UI native resource sync", diff --git a/scripts/check-deprecated-api-usage.mjs b/scripts/check-deprecated-api-usage.mjs index a2d6caffe81c..3a92e074b637 100644 --- a/scripts/check-deprecated-api-usage.mjs +++ b/scripts/check-deprecated-api-usage.mjs @@ -213,10 +213,8 @@ const rules = [ "src/infra/outbound/deliver.ts", "src/plugin-sdk/channel-message-runtime.ts", "src/plugin-sdk/channel-message.ts", - "src/plugin-sdk/channel-test-helpers.ts", "src/plugin-sdk/inbound-reply-dispatch.ts", "src/plugin-sdk/outbound-runtime.ts", - "src/plugin-sdk/test-helpers/outbound-delivery.ts", ], message: "use sendDurableMessageBatch or deliverInboundReplyWithMessageSendContext", }, diff --git a/scripts/lib/plugin-sdk-deprecated-public-subpaths.json b/scripts/lib/plugin-sdk-deprecated-public-subpaths.json index 99b74902ba52..235e6ff47616 100644 --- a/scripts/lib/plugin-sdk-deprecated-public-subpaths.json +++ b/scripts/lib/plugin-sdk-deprecated-public-subpaths.json @@ -1,8 +1,6 @@ [ "agent-config-primitives", - "agent-runtime-test-contracts", "channel-config-schema-legacy", - "channel-contract-testing", "channel-envelope", "channel-inbound-roots", "channel-lifecycle", @@ -16,8 +14,6 @@ "channel-runtime", "channel-secret-runtime", "channel-streaming", - "channel-target-testing", - "channel-test-helpers", "command-auth", "compat", "config-runtime", @@ -43,11 +39,7 @@ "music-generation-core", "outbound-runtime", "outbound-send-deps", - "plugin-test-api", - "plugin-test-contracts", "provider-auth-login", - "provider-http-test-mocks", - "provider-test-contracts", "provider-zai-endpoint", "reply-dedupe", "runtime-logger", @@ -56,9 +48,6 @@ "setup-adapter-runtime", "telegram-account", "telegram-command-config", - "test-env", - "test-fixtures", - "test-node-mocks", "text-runtime", "webhook-path", "zalouser", diff --git a/src/agents/auth-profiles/oauth.ts b/src/agents/auth-profiles/oauth.ts index 7c5f78f51995..baf51bc96a74 100644 --- a/src/agents/auth-profiles/oauth.ts +++ b/src/agents/auth-profiles/oauth.ts @@ -12,7 +12,7 @@ import { getOAuthApiKey, getOAuthProviders, type OAuthCredentials, - type OAuthProvider, + type OAuthProviderId, } from "../../llm/oauth.js"; import { formatProviderAuthProfileApiKeyWithPlugin, @@ -67,10 +67,10 @@ function listOAuthProviderIds(): string[] { const OAUTH_PROVIDER_IDS = new Set(listOAuthProviderIds()); -const isOAuthProvider = (provider: string): provider is OAuthProvider => +const isOAuthProvider = (provider: string): provider is OAuthProviderId => OAUTH_PROVIDER_IDS.has(provider); -const resolveOAuthProvider = (provider: string): OAuthProvider | null => +const resolveOAuthProvider = (provider: string): OAuthProviderId | null => isOAuthProvider(provider) ? provider : null; /** Bearer-token auth modes that are interchangeable (oauth tokens and raw tokens). */ diff --git a/src/llm/utils/oauth/types.ts b/src/llm/utils/oauth/types.ts index ba7de2632e12..70dd8de69b5a 100644 --- a/src/llm/utils/oauth/types.ts +++ b/src/llm/utils/oauth/types.ts @@ -3,7 +3,6 @@ export type { OAuthCredentials, OAuthLoginCallbacks, OAuthPrompt, - OAuthProvider, OAuthProviderId, OAuthProviderInterface, } from "../../../plugin-sdk/provider-oauth-runtime.js"; diff --git a/src/plugin-sdk/channel-test-helpers.ts b/src/plugin-sdk/channel-test-helpers.ts index ad966c50ca89..b8f3362436ab 100644 --- a/src/plugin-sdk/channel-test-helpers.ts +++ b/src/plugin-sdk/channel-test-helpers.ts @@ -19,8 +19,6 @@ export { setActivePluginRegistry, type PluginHookRegistration, } from "./test-helpers/outbound-delivery.js"; -/** @deprecated Direct outbound delivery is runtime substrate; use channel message runtime helpers. */ -export { deliverOutboundPayloads } from "./test-helpers/outbound-delivery.js"; export { createPluginRuntimeMediaMock, createPluginRuntimeMock, diff --git a/src/plugin-sdk/test-helpers/outbound-delivery.ts b/src/plugin-sdk/test-helpers/outbound-delivery.ts index f218f4839d36..cfaf3f8d1989 100644 --- a/src/plugin-sdk/test-helpers/outbound-delivery.ts +++ b/src/plugin-sdk/test-helpers/outbound-delivery.ts @@ -11,5 +11,3 @@ export { setActivePluginRegistry, } from "../../plugins/runtime.js"; export { createOutboundTestPlugin, createTestRegistry } from "../../test-utils/channel-plugins.js"; -/** @deprecated Direct outbound delivery is runtime substrate; use channel message runtime helpers. */ -export { deliverOutboundPayloads } from "../../infra/outbound/deliver.js"; diff --git a/src/plugins/contracts/plugin-sdk-runtime-api-guardrails.test.ts b/src/plugins/contracts/plugin-sdk-runtime-api-guardrails.test.ts index f63566eadf80..1038a17cee07 100644 --- a/src/plugins/contracts/plugin-sdk-runtime-api-guardrails.test.ts +++ b/src/plugins/contracts/plugin-sdk-runtime-api-guardrails.test.ts @@ -149,7 +149,7 @@ const RUNTIME_API_EXPORT_GUARDS: Record = { 'export { getMatrixScopedEnvVarNames, listMatrixEnvAccountIds, resolveMatrixEnvAccountToken } from "./src/env-vars.js";', 'export { hashMatrixAccessToken, resolveMatrixAccountStorageRoot, resolveMatrixCredentialsDir, resolveMatrixCredentialsFilename, resolveMatrixCredentialsPath, resolveMatrixHomeserverKey, sanitizeMatrixPathSegment } from "./src/storage-paths.js";', 'export { ensureMatrixSdkInstalled, isMatrixSdkAvailable } from "./src/matrix/deps.js";', - 'export { assertHttpUrlTargetsPrivateNetwork, closeDispatcher, createPinnedDispatcher, resolvePinnedHostnameWithPolicy, ssrfPolicyFromDangerouslyAllowPrivateNetwork, ssrfPolicyFromAllowPrivateNetwork, type LookupFn, type SsrFPolicy } from "openclaw/plugin-sdk/ssrf-runtime";', + 'export { assertHttpUrlTargetsPrivateNetwork, closeDispatcher, createPinnedDispatcher, resolvePinnedHostnameWithPolicy, ssrfPolicyFromDangerouslyAllowPrivateNetwork, type LookupFn, type SsrFPolicy } from "openclaw/plugin-sdk/ssrf-runtime";', 'export { setMatrixThreadBindingIdleTimeoutBySessionKey, setMatrixThreadBindingMaxAgeBySessionKey } from "./src/matrix/thread-bindings-shared.js";', 'export { setMatrixRuntime } from "./src/runtime.js";', 'export { writeJsonFileAtomically } from "openclaw/plugin-sdk/json-store";', diff --git a/src/plugins/contracts/provider-family-plugin-tests.test.ts b/src/plugins/contracts/provider-family-plugin-tests.test.ts index 4ffd1ecf99be..74bef0587f55 100644 --- a/src/plugins/contracts/provider-family-plugin-tests.test.ts +++ b/src/plugins/contracts/provider-family-plugin-tests.test.ts @@ -32,6 +32,7 @@ const SHARED_FAMILY_HOOK_PATTERNS: ReadonlyArray<{ const PROVIDER_BOUNDARY_TEST_SIGNALS = [ /\bregister(?:Single)?ProviderPlugin\s*\(/u, /\bcreateTestPluginApi\s*\(/u, + /\bcapturePluginRegistration\s*\(/u, /\bexpectPassthroughReplayPolicy\s*\(/u, ] as const; const EXPECTED_SENTINEL_SHARED_FAMILY_ASSIGNMENTS: Record = { @@ -41,8 +42,10 @@ const EXPECTED_SENTINEL_SHARED_FAMILY_ASSIGNMENTS: Record { "duplicate scan target coverage", "dependency pin guard", "format changed files", + "deprecated API usage", + "plugin boundaries", "package patch guard", "test temp creation report (warning-only)", "typecheck core tests", @@ -1721,6 +1724,8 @@ describe("scripts/changed-lanes", () => { "deps:pins:check", "format:check", "scripts/generate-npm-shrinkwrap.mjs", + "check:deprecated-api-usage", + "plugins:boundary-report:ci", "deps:patches:check", "release-metadata:check", "android:version:check", @@ -1926,6 +1931,37 @@ describe("scripts/changed-lanes", () => { ); }); + it("runs deprecation hygiene checks for outcome-changing paths and all lanes", () => { + expect( + shouldRunDeprecationHygieneChecks([ + "src/plugin-sdk/core.ts", + "extensions/slack/index.ts", + "packages/gateway-protocol/src/index.ts", + "scripts/lib/plugin-sdk-entries.mjs", + "scripts/check-deprecated-api-usage.mjs", + "scripts/plugin-boundary-report.ts", + "src/plugins/compat/registry.ts", + "package.json", + ]), + ).toBe(true); + expect(shouldRunDeprecationHygieneChecks(["docs/plugins/sdk-migration.md"])).toBe(false); + + for (const result of [ + detectChangedLanes(["extensions/slack/index.ts"]), + detectChangedLanes(["unknown-surface.foo"]), + ]) { + const plan = createChangedCheckPlan(result); + expect(plan.commands).toContainEqual({ + name: "deprecated API usage", + args: ["check:deprecated-api-usage"], + }); + expect(plan.commands).toContainEqual({ + name: "plugin boundaries", + args: ["plugins:boundary-report:ci"], + }); + } + }); + it("guards release metadata package changes to the top-level version field", () => { const dir = makeTempRepoRoot(tempDirs, "openclaw-release-metadata-"); git(dir, ["init", "-q", "--initial-branch=main"]);