From f6868b7e42db7dc6a8a1d22daf02880bafd5dc42 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 16 Mar 2026 23:51:41 -0700 Subject: [PATCH] refactor: dedupe channel entrypoints and test bridges --- extensions/amazon-bedrock/index.test.ts | 2 +- extensions/bluebubbles/index.ts | 16 ++-- extensions/bluebubbles/setup-entry.ts | 5 +- extensions/diffs/index.test.ts | 2 +- extensions/diffs/src/http.test.ts | 2 +- extensions/discord/index.ts | 21 ++--- extensions/discord/setup-entry.ts | 3 +- extensions/discord/src/api.test.ts | 2 +- extensions/discord/src/chunk.test.ts | 2 +- extensions/discord/src/monitor.test.ts | 2 +- .../discord/src/resolve-channels.test.ts | 2 +- extensions/discord/src/resolve-users.test.ts | 2 +- extensions/feishu/index.ts | 19 ++-- extensions/feishu/setup-entry.ts | 5 +- extensions/github-copilot/usage.test.ts | 5 +- extensions/googlechat/index.ts | 16 ++-- extensions/googlechat/setup-entry.ts | 5 +- .../src/monitor.webhook-routing.test.ts | 2 +- extensions/imessage/index.ts | 16 ++-- extensions/imessage/setup-entry.ts | 3 +- extensions/irc/index.ts | 17 ++-- extensions/irc/setup-entry.ts | 5 +- extensions/line/index.ts | 21 ++--- extensions/line/setup-entry.ts | 5 +- extensions/matrix/index.ts | 18 ++-- extensions/matrix/setup-entry.ts | 5 +- extensions/mattermost/index.ts | 27 ++---- extensions/mattermost/setup-entry.ts | 5 +- extensions/msteams/index.ts | 16 ++-- extensions/msteams/setup-entry.ts | 5 +- extensions/msteams/src/graph-upload.test.ts | 2 +- extensions/nextcloud-talk/index.ts | 16 ++-- extensions/nextcloud-talk/setup-entry.ts | 5 +- extensions/nostr/index.ts | 42 +++------ extensions/nostr/setup-entry.ts | 5 +- extensions/signal/index.ts | 16 ++-- extensions/signal/setup-entry.ts | 3 +- extensions/slack/index.ts | 16 ++-- extensions/slack/setup-entry.ts | 3 +- extensions/slack/src/monitor/media.test.ts | 2 +- extensions/synology-chat/index.ts | 16 ++-- extensions/synology-chat/setup-entry.ts | 5 +- extensions/talk-voice/index.test.ts | 2 +- extensions/telegram/index.ts | 17 ++-- extensions/telegram/setup-entry.ts | 3 +- .../telegram/src/account-inspect.test.ts | 2 +- extensions/telegram/src/accounts.test.ts | 2 +- .../src/bot.create-telegram-bot.test.ts | 4 +- extensions/telegram/src/bot.test.ts | 2 +- extensions/telegram/src/probe.test.ts | 2 +- extensions/test-utils/chunk-test-helpers.ts | 1 + extensions/test-utils/env.ts | 1 + extensions/test-utils/fetch-mock.ts | 1 + extensions/test-utils/frozen-time.ts | 1 + extensions/test-utils/mock-http-response.ts | 1 + extensions/test-utils/plugin-command.ts | 1 + extensions/test-utils/plugin-registration.ts | 1 + extensions/test-utils/provider-usage-fetch.ts | 4 + extensions/test-utils/temp-dir.ts | 1 + extensions/test-utils/typed-cases.ts | 1 + extensions/tlon/index.ts | 54 +++-------- extensions/tlon/setup-entry.ts | 5 +- extensions/twitch/index.ts | 19 ++-- extensions/whatsapp/index.ts | 16 ++-- extensions/whatsapp/setup-entry.ts | 3 +- .../src/accounts.whatsapp-auth.test.ts | 2 +- ...o-reply.connection-and-logging.e2e.test.ts | 2 +- .../auto-reply/web-auto-reply-utils.test.ts | 2 +- extensions/whatsapp/src/media.test.ts | 2 +- extensions/zalo/index.ts | 18 ++-- extensions/zalo/setup-entry.ts | 5 +- extensions/zalouser/index.ts | 21 ++--- extensions/zalouser/setup-entry.ts | 5 +- package.json | 3 +- .../check-no-extension-test-core-imports.ts | 90 +++++++++++++++++++ src/plugin-sdk/core.ts | 53 +++++++++++ src/plugin-sdk/subpaths.test.ts | 2 + 77 files changed, 360 insertions(+), 376 deletions(-) create mode 100644 extensions/test-utils/chunk-test-helpers.ts create mode 100644 extensions/test-utils/env.ts create mode 100644 extensions/test-utils/fetch-mock.ts create mode 100644 extensions/test-utils/frozen-time.ts create mode 100644 extensions/test-utils/mock-http-response.ts create mode 100644 extensions/test-utils/plugin-command.ts create mode 100644 extensions/test-utils/plugin-registration.ts create mode 100644 extensions/test-utils/provider-usage-fetch.ts create mode 100644 extensions/test-utils/temp-dir.ts create mode 100644 extensions/test-utils/typed-cases.ts create mode 100644 scripts/check-no-extension-test-core-imports.ts diff --git a/extensions/amazon-bedrock/index.test.ts b/extensions/amazon-bedrock/index.test.ts index 641173cd6ce..61b33a0bc68 100644 --- a/extensions/amazon-bedrock/index.test.ts +++ b/extensions/amazon-bedrock/index.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { registerSingleProviderPlugin } from "../../src/test-utils/plugin-registration.js"; +import { registerSingleProviderPlugin } from "../test-utils/plugin-registration.js"; import amazonBedrockPlugin from "./index.js"; describe("amazon-bedrock provider plugin", () => { diff --git a/extensions/bluebubbles/index.ts b/extensions/bluebubbles/index.ts index f04afb40959..778cbd8ae8f 100644 --- a/extensions/bluebubbles/index.ts +++ b/extensions/bluebubbles/index.ts @@ -1,17 +1,11 @@ -import type { OpenClawPluginApi } from "openclaw/plugin-sdk/bluebubbles"; -import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/bluebubbles"; +import { defineChannelPluginEntry } from "openclaw/plugin-sdk/core"; import { bluebubblesPlugin } from "./src/channel.js"; import { setBlueBubblesRuntime } from "./src/runtime.js"; -const plugin = { +export default defineChannelPluginEntry({ id: "bluebubbles", name: "BlueBubbles", description: "BlueBubbles channel plugin (macOS app)", - configSchema: emptyPluginConfigSchema(), - register(api: OpenClawPluginApi) { - setBlueBubblesRuntime(api.runtime); - api.registerChannel({ plugin: bluebubblesPlugin }); - }, -}; - -export default plugin; + plugin: bluebubblesPlugin, + setRuntime: setBlueBubblesRuntime, +}); diff --git a/extensions/bluebubbles/setup-entry.ts b/extensions/bluebubbles/setup-entry.ts index 5e05d9c8bb2..940837c87f6 100644 --- a/extensions/bluebubbles/setup-entry.ts +++ b/extensions/bluebubbles/setup-entry.ts @@ -1,5 +1,4 @@ +import { defineSetupPluginEntry } from "openclaw/plugin-sdk/core"; import { bluebubblesPlugin } from "./src/channel.js"; -export default { - plugin: bluebubblesPlugin, -}; +export default defineSetupPluginEntry(bluebubblesPlugin); diff --git a/extensions/diffs/index.test.ts b/extensions/diffs/index.test.ts index c38da12bfcd..b1ade0c6a09 100644 --- a/extensions/diffs/index.test.ts +++ b/extensions/diffs/index.test.ts @@ -1,7 +1,7 @@ import type { IncomingMessage } from "node:http"; import type { OpenClawPluginApi } from "openclaw/plugin-sdk/diffs"; import { describe, expect, it, vi } from "vitest"; -import { createMockServerResponse } from "../../src/test-utils/mock-http-response.js"; +import { createMockServerResponse } from "../test-utils/mock-http-response.js"; import { createTestPluginApi } from "../test-utils/plugin-api.js"; import plugin from "./index.js"; diff --git a/extensions/diffs/src/http.test.ts b/extensions/diffs/src/http.test.ts index a1caef018e4..eed9abd77d8 100644 --- a/extensions/diffs/src/http.test.ts +++ b/extensions/diffs/src/http.test.ts @@ -1,6 +1,6 @@ import type { IncomingMessage } from "node:http"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; -import { createMockServerResponse } from "../../../src/test-utils/mock-http-response.js"; +import { createMockServerResponse } from "../../test-utils/mock-http-response.js"; import { createDiffsHttpHandler } from "./http.js"; import { DiffArtifactStore } from "./store.js"; import { createDiffStoreHarness } from "./test-helpers.js"; diff --git a/extensions/discord/index.ts b/extensions/discord/index.ts index 13b32f08bb1..7c179623e23 100644 --- a/extensions/discord/index.ts +++ b/extensions/discord/index.ts @@ -1,22 +1,13 @@ -import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core"; -import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/core"; +import { defineChannelPluginEntry } from "openclaw/plugin-sdk/core"; import { discordPlugin } from "./src/channel.js"; import { setDiscordRuntime } from "./src/runtime.js"; import { registerDiscordSubagentHooks } from "./src/subagent-hooks.js"; -const plugin = { +export default defineChannelPluginEntry({ id: "discord", name: "Discord", description: "Discord channel plugin", - configSchema: emptyPluginConfigSchema(), - register(api: OpenClawPluginApi) { - setDiscordRuntime(api.runtime); - api.registerChannel({ plugin: discordPlugin }); - if (api.registrationMode !== "full") { - return; - } - registerDiscordSubagentHooks(api); - }, -}; - -export default plugin; + plugin: discordPlugin, + setRuntime: setDiscordRuntime, + registerFull: registerDiscordSubagentHooks, +}); diff --git a/extensions/discord/setup-entry.ts b/extensions/discord/setup-entry.ts index 329a9376c9f..e59c812ff4b 100644 --- a/extensions/discord/setup-entry.ts +++ b/extensions/discord/setup-entry.ts @@ -1,3 +1,4 @@ +import { defineSetupPluginEntry } from "openclaw/plugin-sdk/core"; import { discordSetupPlugin } from "./src/channel.setup.js"; -export default { plugin: discordSetupPlugin }; +export default defineSetupPluginEntry(discordSetupPlugin); diff --git a/extensions/discord/src/api.test.ts b/extensions/discord/src/api.test.ts index 5b0e648aa1d..09e0863e137 100644 --- a/extensions/discord/src/api.test.ts +++ b/extensions/discord/src/api.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { withFetchPreconnect } from "../../../src/test-utils/fetch-mock.js"; +import { withFetchPreconnect } from "../../test-utils/fetch-mock.js"; import { fetchDiscord } from "./api.js"; import { jsonResponse } from "./test-http-helpers.js"; diff --git a/extensions/discord/src/chunk.test.ts b/extensions/discord/src/chunk.test.ts index 3c667c0fc9f..69f5ec856ec 100644 --- a/extensions/discord/src/chunk.test.ts +++ b/extensions/discord/src/chunk.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { countLines, hasBalancedFences } from "../../../src/test-utils/chunk-test-helpers.js"; +import { countLines, hasBalancedFences } from "../../test-utils/chunk-test-helpers.js"; import { chunkDiscordText, chunkDiscordTextWithMode } from "./chunk.js"; describe("chunkDiscordText", () => { diff --git a/extensions/discord/src/monitor.test.ts b/extensions/discord/src/monitor.test.ts index 40f14a00551..b3af666c35f 100644 --- a/extensions/discord/src/monitor.test.ts +++ b/extensions/discord/src/monitor.test.ts @@ -1,6 +1,6 @@ import { ChannelType, type Guild } from "@buape/carbon"; import { beforeEach, describe, expect, it, vi } from "vitest"; -import { typedCases } from "../../../src/test-utils/typed-cases.js"; +import { typedCases } from "../../test-utils/typed-cases.js"; import { allowListMatches, buildDiscordMediaPayload, diff --git a/extensions/discord/src/resolve-channels.test.ts b/extensions/discord/src/resolve-channels.test.ts index fb46792aaaa..f053fb97888 100644 --- a/extensions/discord/src/resolve-channels.test.ts +++ b/extensions/discord/src/resolve-channels.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { withFetchPreconnect } from "../../../src/test-utils/fetch-mock.js"; +import { withFetchPreconnect } from "../../test-utils/fetch-mock.js"; import { resolveDiscordChannelAllowlist } from "./resolve-channels.js"; import { jsonResponse, urlToString } from "./test-http-helpers.js"; diff --git a/extensions/discord/src/resolve-users.test.ts b/extensions/discord/src/resolve-users.test.ts index d788b77ebe0..f67b7289a59 100644 --- a/extensions/discord/src/resolve-users.test.ts +++ b/extensions/discord/src/resolve-users.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { withFetchPreconnect } from "../../../src/test-utils/fetch-mock.js"; +import { withFetchPreconnect } from "../../test-utils/fetch-mock.js"; import { resolveDiscordUserAllowlist } from "./resolve-users.js"; import { jsonResponse, urlToString } from "./test-http-helpers.js"; diff --git a/extensions/feishu/index.ts b/extensions/feishu/index.ts index ba7ac26922b..27f90f66479 100644 --- a/extensions/feishu/index.ts +++ b/extensions/feishu/index.ts @@ -1,5 +1,4 @@ -import type { OpenClawPluginApi } from "openclaw/plugin-sdk/feishu"; -import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/feishu"; +import { defineChannelPluginEntry } from "openclaw/plugin-sdk/core"; import { registerFeishuBitableTools } from "./src/bitable.js"; import { feishuPlugin } from "./src/channel.js"; import { registerFeishuChatTools } from "./src/chat.js"; @@ -46,17 +45,13 @@ export { } from "./src/mention.js"; export { feishuPlugin } from "./src/channel.js"; -const plugin = { +export default defineChannelPluginEntry({ id: "feishu", name: "Feishu", description: "Feishu/Lark channel plugin", - configSchema: emptyPluginConfigSchema(), - register(api: OpenClawPluginApi) { - setFeishuRuntime(api.runtime); - api.registerChannel({ plugin: feishuPlugin }); - if (api.registrationMode !== "full") { - return; - } + plugin: feishuPlugin, + setRuntime: setFeishuRuntime, + registerFull(api) { registerFeishuSubagentHooks(api); registerFeishuDocTools(api); registerFeishuChatTools(api); @@ -65,6 +60,4 @@ const plugin = { registerFeishuPermTools(api); registerFeishuBitableTools(api); }, -}; - -export default plugin; +}); diff --git a/extensions/feishu/setup-entry.ts b/extensions/feishu/setup-entry.ts index 3e4df4faee8..1f16bde8bdd 100644 --- a/extensions/feishu/setup-entry.ts +++ b/extensions/feishu/setup-entry.ts @@ -1,5 +1,4 @@ +import { defineSetupPluginEntry } from "openclaw/plugin-sdk/core"; import { feishuPlugin } from "./src/channel.js"; -export default { - plugin: feishuPlugin, -}; +export default defineSetupPluginEntry(feishuPlugin); diff --git a/extensions/github-copilot/usage.test.ts b/extensions/github-copilot/usage.test.ts index b4044c7f5f9..0bc97974d70 100644 --- a/extensions/github-copilot/usage.test.ts +++ b/extensions/github-copilot/usage.test.ts @@ -1,8 +1,5 @@ import { describe, expect, it } from "vitest"; -import { - createProviderUsageFetch, - makeResponse, -} from "../../src/test-utils/provider-usage-fetch.js"; +import { createProviderUsageFetch, makeResponse } from "../test-utils/provider-usage-fetch.js"; import { fetchCopilotUsage } from "./usage.js"; describe("fetchCopilotUsage", () => { diff --git a/extensions/googlechat/index.ts b/extensions/googlechat/index.ts index 892694f93b4..414bfc9557b 100644 --- a/extensions/googlechat/index.ts +++ b/extensions/googlechat/index.ts @@ -1,17 +1,11 @@ -import type { OpenClawPluginApi } from "openclaw/plugin-sdk/googlechat"; -import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/googlechat"; +import { defineChannelPluginEntry } from "openclaw/plugin-sdk/core"; import { googlechatPlugin } from "./src/channel.js"; import { setGoogleChatRuntime } from "./src/runtime.js"; -const plugin = { +export default defineChannelPluginEntry({ id: "googlechat", name: "Google Chat", description: "OpenClaw Google Chat channel plugin", - configSchema: emptyPluginConfigSchema(), - register(api: OpenClawPluginApi) { - setGoogleChatRuntime(api.runtime); - api.registerChannel(googlechatPlugin); - }, -}; - -export default plugin; + plugin: googlechatPlugin, + setRuntime: setGoogleChatRuntime, +}); diff --git a/extensions/googlechat/setup-entry.ts b/extensions/googlechat/setup-entry.ts index be33127799f..44fd1f11fb3 100644 --- a/extensions/googlechat/setup-entry.ts +++ b/extensions/googlechat/setup-entry.ts @@ -1,5 +1,4 @@ +import { defineSetupPluginEntry } from "openclaw/plugin-sdk/core"; import { googlechatPlugin } from "./src/channel.js"; -export default { - plugin: googlechatPlugin, -}; +export default defineSetupPluginEntry(googlechatPlugin); diff --git a/extensions/googlechat/src/monitor.webhook-routing.test.ts b/extensions/googlechat/src/monitor.webhook-routing.test.ts index 9896efce645..2258d154449 100644 --- a/extensions/googlechat/src/monitor.webhook-routing.test.ts +++ b/extensions/googlechat/src/monitor.webhook-routing.test.ts @@ -4,7 +4,7 @@ import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk/googlech import { afterEach, describe, expect, it, vi } from "vitest"; import { createEmptyPluginRegistry } from "../../../src/plugins/registry.js"; import { setActivePluginRegistry } from "../../../src/plugins/runtime.js"; -import { createMockServerResponse } from "../../../src/test-utils/mock-http-response.js"; +import { createMockServerResponse } from "../../test-utils/mock-http-response.js"; import type { ResolvedGoogleChatAccount } from "./accounts.js"; import { verifyGoogleChatRequest } from "./auth.js"; import { handleGoogleChatWebhookRequest, registerGoogleChatWebhookTarget } from "./monitor.js"; diff --git a/extensions/imessage/index.ts b/extensions/imessage/index.ts index e87d421cf2e..aea014f06d4 100644 --- a/extensions/imessage/index.ts +++ b/extensions/imessage/index.ts @@ -1,17 +1,11 @@ -import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core"; -import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/core"; +import { defineChannelPluginEntry } from "openclaw/plugin-sdk/core"; import { imessagePlugin } from "./src/channel.js"; import { setIMessageRuntime } from "./src/runtime.js"; -const plugin = { +export default defineChannelPluginEntry({ id: "imessage", name: "iMessage", description: "iMessage channel plugin", - configSchema: emptyPluginConfigSchema(), - register(api: OpenClawPluginApi) { - setIMessageRuntime(api.runtime); - api.registerChannel({ plugin: imessagePlugin }); - }, -}; - -export default plugin; + plugin: imessagePlugin, + setRuntime: setIMessageRuntime, +}); diff --git a/extensions/imessage/setup-entry.ts b/extensions/imessage/setup-entry.ts index 6b4c642d0ae..ed6936ca387 100644 --- a/extensions/imessage/setup-entry.ts +++ b/extensions/imessage/setup-entry.ts @@ -1,3 +1,4 @@ +import { defineSetupPluginEntry } from "openclaw/plugin-sdk/core"; import { imessageSetupPlugin } from "./src/channel.setup.js"; -export default { plugin: imessageSetupPlugin }; +export default defineSetupPluginEntry(imessageSetupPlugin); diff --git a/extensions/irc/index.ts b/extensions/irc/index.ts index 40182558dcb..5ae8619812d 100644 --- a/extensions/irc/index.ts +++ b/extensions/irc/index.ts @@ -1,17 +1,12 @@ -import type { ChannelPlugin, OpenClawPluginApi } from "openclaw/plugin-sdk/irc"; -import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/irc"; +import type { ChannelPlugin } from "openclaw/plugin-sdk/core"; +import { defineChannelPluginEntry } from "openclaw/plugin-sdk/core"; import { ircPlugin } from "./src/channel.js"; import { setIrcRuntime } from "./src/runtime.js"; -const plugin = { +export default defineChannelPluginEntry({ id: "irc", name: "IRC", description: "IRC channel plugin", - configSchema: emptyPluginConfigSchema(), - register(api: OpenClawPluginApi) { - setIrcRuntime(api.runtime); - api.registerChannel({ plugin: ircPlugin as ChannelPlugin }); - }, -}; - -export default plugin; + plugin: ircPlugin as ChannelPlugin, + setRuntime: setIrcRuntime, +}); diff --git a/extensions/irc/setup-entry.ts b/extensions/irc/setup-entry.ts index fe8bea1814d..3d3d040990c 100644 --- a/extensions/irc/setup-entry.ts +++ b/extensions/irc/setup-entry.ts @@ -1,5 +1,4 @@ +import { defineSetupPluginEntry } from "openclaw/plugin-sdk/core"; import { ircPlugin } from "./src/channel.js"; -export default { - plugin: ircPlugin, -}; +export default defineSetupPluginEntry(ircPlugin); diff --git a/extensions/line/index.ts b/extensions/line/index.ts index 59b1d97920d..fabf1c9d5b7 100644 --- a/extensions/line/index.ts +++ b/extensions/line/index.ts @@ -1,22 +1,13 @@ -import type { OpenClawPluginApi } from "openclaw/plugin-sdk/line"; -import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/line"; +import { defineChannelPluginEntry } from "openclaw/plugin-sdk/core"; import { registerLineCardCommand } from "./src/card-command.js"; import { linePlugin } from "./src/channel.js"; import { setLineRuntime } from "./src/runtime.js"; -const plugin = { +export default defineChannelPluginEntry({ id: "line", name: "LINE", description: "LINE Messaging API channel plugin", - configSchema: emptyPluginConfigSchema(), - register(api: OpenClawPluginApi) { - setLineRuntime(api.runtime); - api.registerChannel({ plugin: linePlugin }); - if (api.registrationMode !== "full") { - return; - } - registerLineCardCommand(api); - }, -}; - -export default plugin; + plugin: linePlugin, + setRuntime: setLineRuntime, + registerFull: registerLineCardCommand, +}); diff --git a/extensions/line/setup-entry.ts b/extensions/line/setup-entry.ts index ca25d243155..97ed5fa30c6 100644 --- a/extensions/line/setup-entry.ts +++ b/extensions/line/setup-entry.ts @@ -1,5 +1,4 @@ +import { defineSetupPluginEntry } from "openclaw/plugin-sdk/core"; import { lineSetupPlugin } from "./src/channel.setup.js"; -export default { - plugin: lineSetupPlugin, -}; +export default defineSetupPluginEntry(lineSetupPlugin); diff --git a/extensions/matrix/index.ts b/extensions/matrix/index.ts index 46a4ba5864f..5400a9b94c6 100644 --- a/extensions/matrix/index.ts +++ b/extensions/matrix/index.ts @@ -1,17 +1,11 @@ -import type { OpenClawPluginApi } from "openclaw/plugin-sdk/matrix"; -import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/matrix"; +import { defineChannelPluginEntry } from "openclaw/plugin-sdk/core"; import { matrixPlugin } from "./src/channel.js"; import { setMatrixRuntime } from "./src/runtime.js"; -const plugin = { +export default defineChannelPluginEntry({ id: "matrix", name: "Matrix", - description: "Matrix channel plugin (matrix-js-sdk)", - configSchema: emptyPluginConfigSchema(), - register(api: OpenClawPluginApi) { - setMatrixRuntime(api.runtime); - api.registerChannel({ plugin: matrixPlugin }); - }, -}; - -export default plugin; + description: "Matrix channel plugin", + plugin: matrixPlugin, + setRuntime: setMatrixRuntime, +}); diff --git a/extensions/matrix/setup-entry.ts b/extensions/matrix/setup-entry.ts index 4cbabfe6333..045b3a58917 100644 --- a/extensions/matrix/setup-entry.ts +++ b/extensions/matrix/setup-entry.ts @@ -1,5 +1,4 @@ +import { defineSetupPluginEntry } from "openclaw/plugin-sdk/core"; import { matrixPlugin } from "./src/channel.js"; -export default { - plugin: matrixPlugin, -}; +export default defineSetupPluginEntry(matrixPlugin); diff --git a/extensions/mattermost/index.ts b/extensions/mattermost/index.ts index de6f4e1d8a0..f5086aba465 100644 --- a/extensions/mattermost/index.ts +++ b/extensions/mattermost/index.ts @@ -1,26 +1,17 @@ -import type { OpenClawPluginApi } from "openclaw/plugin-sdk/mattermost"; -import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/mattermost"; +import { defineChannelPluginEntry } from "openclaw/plugin-sdk/core"; import { mattermostPlugin } from "./src/channel.js"; -import { getSlashCommandState, registerSlashCommandRoute } from "./src/mattermost/slash-state.js"; +import { registerSlashCommandRoute } from "./src/mattermost/slash-state.js"; import { setMattermostRuntime } from "./src/runtime.js"; -const plugin = { +export default defineChannelPluginEntry({ id: "mattermost", name: "Mattermost", description: "Mattermost channel plugin", - configSchema: emptyPluginConfigSchema(), - register(api: OpenClawPluginApi) { - setMattermostRuntime(api.runtime); - api.registerChannel({ plugin: mattermostPlugin }); - if (api.registrationMode !== "full") { - return; - } - - // Register the HTTP route for slash command callbacks. - // The actual command registration with MM happens in the monitor - // after the bot connects and we know the team ID. + plugin: mattermostPlugin, + setRuntime: setMattermostRuntime, + registerFull(api) { + // Actual slash-command registration happens after the monitor connects and + // knows the team id; the route itself can be wired here. registerSlashCommandRoute(api); }, -}; - -export default plugin; +}); diff --git a/extensions/mattermost/setup-entry.ts b/extensions/mattermost/setup-entry.ts index 64c02fcbe9d..34ce40972e4 100644 --- a/extensions/mattermost/setup-entry.ts +++ b/extensions/mattermost/setup-entry.ts @@ -1,5 +1,4 @@ +import { defineSetupPluginEntry } from "openclaw/plugin-sdk/core"; import { mattermostPlugin } from "./src/channel.js"; -export default { - plugin: mattermostPlugin, -}; +export default defineSetupPluginEntry(mattermostPlugin); diff --git a/extensions/msteams/index.ts b/extensions/msteams/index.ts index 725ad40dfdf..c190ea49224 100644 --- a/extensions/msteams/index.ts +++ b/extensions/msteams/index.ts @@ -1,17 +1,11 @@ -import type { OpenClawPluginApi } from "openclaw/plugin-sdk/msteams"; -import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/msteams"; +import { defineChannelPluginEntry } from "openclaw/plugin-sdk/core"; import { msteamsPlugin } from "./src/channel.js"; import { setMSTeamsRuntime } from "./src/runtime.js"; -const plugin = { +export default defineChannelPluginEntry({ id: "msteams", name: "Microsoft Teams", description: "Microsoft Teams channel plugin (Bot Framework)", - configSchema: emptyPluginConfigSchema(), - register(api: OpenClawPluginApi) { - setMSTeamsRuntime(api.runtime); - api.registerChannel({ plugin: msteamsPlugin }); - }, -}; - -export default plugin; + plugin: msteamsPlugin, + setRuntime: setMSTeamsRuntime, +}); diff --git a/extensions/msteams/setup-entry.ts b/extensions/msteams/setup-entry.ts index fb850b60e18..6e29414c82e 100644 --- a/extensions/msteams/setup-entry.ts +++ b/extensions/msteams/setup-entry.ts @@ -1,5 +1,4 @@ +import { defineSetupPluginEntry } from "openclaw/plugin-sdk/core"; import { msteamsPlugin } from "./src/channel.js"; -export default { - plugin: msteamsPlugin, -}; +export default defineSetupPluginEntry(msteamsPlugin); diff --git a/extensions/msteams/src/graph-upload.test.ts b/extensions/msteams/src/graph-upload.test.ts index b79086f54ca..90a9da1d352 100644 --- a/extensions/msteams/src/graph-upload.test.ts +++ b/extensions/msteams/src/graph-upload.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, vi } from "vitest"; -import { withFetchPreconnect } from "../../../src/test-utils/fetch-mock.js"; +import { withFetchPreconnect } from "../../test-utils/fetch-mock.js"; import { uploadToOneDrive, uploadToSharePoint } from "./graph-upload.js"; describe("graph upload helpers", () => { diff --git a/extensions/nextcloud-talk/index.ts b/extensions/nextcloud-talk/index.ts index 697a810009f..2057bd435e8 100644 --- a/extensions/nextcloud-talk/index.ts +++ b/extensions/nextcloud-talk/index.ts @@ -1,17 +1,11 @@ -import type { OpenClawPluginApi } from "openclaw/plugin-sdk/nextcloud-talk"; -import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/nextcloud-talk"; +import { defineChannelPluginEntry } from "openclaw/plugin-sdk/core"; import { nextcloudTalkPlugin } from "./src/channel.js"; import { setNextcloudTalkRuntime } from "./src/runtime.js"; -const plugin = { +export default defineChannelPluginEntry({ id: "nextcloud-talk", name: "Nextcloud Talk", description: "Nextcloud Talk channel plugin", - configSchema: emptyPluginConfigSchema(), - register(api: OpenClawPluginApi) { - setNextcloudTalkRuntime(api.runtime); - api.registerChannel({ plugin: nextcloudTalkPlugin }); - }, -}; - -export default plugin; + plugin: nextcloudTalkPlugin, + setRuntime: setNextcloudTalkRuntime, +}); diff --git a/extensions/nextcloud-talk/setup-entry.ts b/extensions/nextcloud-talk/setup-entry.ts index f33df37c7dc..88aec7d47e9 100644 --- a/extensions/nextcloud-talk/setup-entry.ts +++ b/extensions/nextcloud-talk/setup-entry.ts @@ -1,5 +1,4 @@ +import { defineSetupPluginEntry } from "openclaw/plugin-sdk/core"; import { nextcloudTalkPlugin } from "./src/channel.js"; -export default { - plugin: nextcloudTalkPlugin, -}; +export default defineSetupPluginEntry(nextcloudTalkPlugin); diff --git a/extensions/nostr/index.ts b/extensions/nostr/index.ts index d8fdb203924..cdabf64c322 100644 --- a/extensions/nostr/index.ts +++ b/extensions/nostr/index.ts @@ -1,24 +1,17 @@ -import type { OpenClawPluginApi } from "openclaw/plugin-sdk/nostr"; -import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/nostr"; +import { defineChannelPluginEntry } from "openclaw/plugin-sdk/core"; import { nostrPlugin } from "./src/channel.js"; import type { NostrProfile } from "./src/config-schema.js"; import { createNostrProfileHttpHandler } from "./src/nostr-profile-http.js"; -import { setNostrRuntime, getNostrRuntime } from "./src/runtime.js"; +import { getNostrRuntime, setNostrRuntime } from "./src/runtime.js"; import { resolveNostrAccount } from "./src/types.js"; -const plugin = { +export default defineChannelPluginEntry({ id: "nostr", name: "Nostr", description: "Nostr DM channel plugin via NIP-04", - configSchema: emptyPluginConfigSchema(), - register(api: OpenClawPluginApi) { - setNostrRuntime(api.runtime); - api.registerChannel({ plugin: nostrPlugin }); - if (api.registrationMode !== "full") { - return; - } - - // Register HTTP handler for profile management + plugin: nostrPlugin, + setRuntime: setNostrRuntime, + registerFull(api) { const httpHandler = createNostrProfileHttpHandler({ getConfigProfile: (accountId: string) => { const runtime = getNostrRuntime(); @@ -30,23 +23,18 @@ const plugin = { const runtime = getNostrRuntime(); const cfg = runtime.config.loadConfig(); - // Build the config patch for channels.nostr.profile const channels = (cfg.channels ?? {}) as Record; const nostrConfig = (channels.nostr ?? {}) as Record; - const updatedNostrConfig = { - ...nostrConfig, - profile, - }; - - const updatedChannels = { - ...channels, - nostr: updatedNostrConfig, - }; - await runtime.config.writeConfigFile({ ...cfg, - channels: updatedChannels, + channels: { + ...channels, + nostr: { + ...nostrConfig, + profile, + }, + }, }); }, getAccountInfo: (accountId: string) => { @@ -71,6 +59,4 @@ const plugin = { handler: httpHandler, }); }, -}; - -export default plugin; +}); diff --git a/extensions/nostr/setup-entry.ts b/extensions/nostr/setup-entry.ts index 8884a71cc80..f2ac263fd0f 100644 --- a/extensions/nostr/setup-entry.ts +++ b/extensions/nostr/setup-entry.ts @@ -1,5 +1,4 @@ +import { defineSetupPluginEntry } from "openclaw/plugin-sdk/core"; import { nostrPlugin } from "./src/channel.js"; -export default { - plugin: nostrPlugin, -}; +export default defineSetupPluginEntry(nostrPlugin); diff --git a/extensions/signal/index.ts b/extensions/signal/index.ts index 0a686851120..6b20777f842 100644 --- a/extensions/signal/index.ts +++ b/extensions/signal/index.ts @@ -1,17 +1,11 @@ -import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core"; -import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/core"; +import { defineChannelPluginEntry } from "openclaw/plugin-sdk/core"; import { signalPlugin } from "./src/channel.js"; import { setSignalRuntime } from "./src/runtime.js"; -const plugin = { +export default defineChannelPluginEntry({ id: "signal", name: "Signal", description: "Signal channel plugin", - configSchema: emptyPluginConfigSchema(), - register(api: OpenClawPluginApi) { - setSignalRuntime(api.runtime); - api.registerChannel({ plugin: signalPlugin }); - }, -}; - -export default plugin; + plugin: signalPlugin, + setRuntime: setSignalRuntime, +}); diff --git a/extensions/signal/setup-entry.ts b/extensions/signal/setup-entry.ts index 18c27ec5a16..63f6d95e8fc 100644 --- a/extensions/signal/setup-entry.ts +++ b/extensions/signal/setup-entry.ts @@ -1,3 +1,4 @@ +import { defineSetupPluginEntry } from "openclaw/plugin-sdk/core"; import { signalSetupPlugin } from "./src/channel.setup.js"; -export default { plugin: signalSetupPlugin }; +export default defineSetupPluginEntry(signalSetupPlugin); diff --git a/extensions/slack/index.ts b/extensions/slack/index.ts index f1147cb9c91..44abfa36b0d 100644 --- a/extensions/slack/index.ts +++ b/extensions/slack/index.ts @@ -1,17 +1,11 @@ -import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core"; -import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/core"; +import { defineChannelPluginEntry } from "openclaw/plugin-sdk/core"; import { slackPlugin } from "./src/channel.js"; import { setSlackRuntime } from "./src/runtime.js"; -const plugin = { +export default defineChannelPluginEntry({ id: "slack", name: "Slack", description: "Slack channel plugin", - configSchema: emptyPluginConfigSchema(), - register(api: OpenClawPluginApi) { - setSlackRuntime(api.runtime); - api.registerChannel({ plugin: slackPlugin }); - }, -}; - -export default plugin; + plugin: slackPlugin, + setRuntime: setSlackRuntime, +}); diff --git a/extensions/slack/setup-entry.ts b/extensions/slack/setup-entry.ts index 1bd6eabde59..5a80ca2128b 100644 --- a/extensions/slack/setup-entry.ts +++ b/extensions/slack/setup-entry.ts @@ -1,3 +1,4 @@ +import { defineSetupPluginEntry } from "openclaw/plugin-sdk/core"; import { slackSetupPlugin } from "./src/channel.setup.js"; -export default { plugin: slackSetupPlugin }; +export default defineSetupPluginEntry(slackSetupPlugin); diff --git a/extensions/slack/src/monitor/media.test.ts b/extensions/slack/src/monitor/media.test.ts index f745f205950..9d5114e2961 100644 --- a/extensions/slack/src/monitor/media.test.ts +++ b/extensions/slack/src/monitor/media.test.ts @@ -4,7 +4,7 @@ import * as mediaFetch from "../../../../src/media/fetch.js"; import type { SavedMedia } from "../../../../src/media/store.js"; import * as mediaStore from "../../../../src/media/store.js"; import { mockPinnedHostnameResolution } from "../../../../src/test-helpers/ssrf.js"; -import { type FetchMock, withFetchPreconnect } from "../../../../src/test-utils/fetch-mock.js"; +import { type FetchMock, withFetchPreconnect } from "../../../test-utils/fetch-mock.js"; import { fetchWithSlackAuth, resolveSlackAttachmentContent, diff --git a/extensions/synology-chat/index.ts b/extensions/synology-chat/index.ts index 9078b9f86c7..79e3f49d513 100644 --- a/extensions/synology-chat/index.ts +++ b/extensions/synology-chat/index.ts @@ -1,17 +1,11 @@ -import type { OpenClawPluginApi } from "openclaw/plugin-sdk/synology-chat"; -import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/synology-chat"; +import { defineChannelPluginEntry } from "openclaw/plugin-sdk/core"; import { synologyChatPlugin } from "./src/channel.js"; import { setSynologyRuntime } from "./src/runtime.js"; -const plugin = { +export default defineChannelPluginEntry({ id: "synology-chat", name: "Synology Chat", description: "Native Synology Chat channel plugin for OpenClaw", - configSchema: emptyPluginConfigSchema(), - register(api: OpenClawPluginApi) { - setSynologyRuntime(api.runtime); - api.registerChannel({ plugin: synologyChatPlugin }); - }, -}; - -export default plugin; + plugin: synologyChatPlugin, + setRuntime: setSynologyRuntime, +}); diff --git a/extensions/synology-chat/setup-entry.ts b/extensions/synology-chat/setup-entry.ts index 45cc966e082..858696710a8 100644 --- a/extensions/synology-chat/setup-entry.ts +++ b/extensions/synology-chat/setup-entry.ts @@ -1,5 +1,4 @@ +import { defineSetupPluginEntry } from "openclaw/plugin-sdk/core"; import { synologyChatPlugin } from "./src/channel.js"; -export default { - plugin: synologyChatPlugin, -}; +export default defineSetupPluginEntry(synologyChatPlugin); diff --git a/extensions/talk-voice/index.test.ts b/extensions/talk-voice/index.test.ts index 2d0a991aa47..15876987554 100644 --- a/extensions/talk-voice/index.test.ts +++ b/extensions/talk-voice/index.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, vi } from "vitest"; -import type { OpenClawPluginCommandDefinition } from "../../src/plugins/types.js"; +import type { OpenClawPluginCommandDefinition } from "../test-utils/plugin-command.js"; import { createPluginRuntimeMock } from "../test-utils/plugin-runtime-mock.js"; import register from "./index.js"; diff --git a/extensions/telegram/index.ts b/extensions/telegram/index.ts index d47ae46b6ce..89413373c5a 100644 --- a/extensions/telegram/index.ts +++ b/extensions/telegram/index.ts @@ -1,17 +1,12 @@ -import type { ChannelPlugin, OpenClawPluginApi } from "openclaw/plugin-sdk/core"; -import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/core"; +import type { ChannelPlugin } from "openclaw/plugin-sdk/core"; +import { defineChannelPluginEntry } from "openclaw/plugin-sdk/core"; import { telegramPlugin } from "./src/channel.js"; import { setTelegramRuntime } from "./src/runtime.js"; -const plugin = { +export default defineChannelPluginEntry({ id: "telegram", name: "Telegram", description: "Telegram channel plugin", - configSchema: emptyPluginConfigSchema(), - register(api: OpenClawPluginApi) { - setTelegramRuntime(api.runtime); - api.registerChannel({ plugin: telegramPlugin as ChannelPlugin }); - }, -}; - -export default plugin; + plugin: telegramPlugin as ChannelPlugin, + setRuntime: setTelegramRuntime, +}); diff --git a/extensions/telegram/setup-entry.ts b/extensions/telegram/setup-entry.ts index 030f4bb3295..c44a073e80b 100644 --- a/extensions/telegram/setup-entry.ts +++ b/extensions/telegram/setup-entry.ts @@ -1,3 +1,4 @@ +import { defineSetupPluginEntry } from "openclaw/plugin-sdk/core"; import { telegramSetupPlugin } from "./src/channel.setup.js"; -export default { plugin: telegramSetupPlugin }; +export default defineSetupPluginEntry(telegramSetupPlugin); diff --git a/extensions/telegram/src/account-inspect.test.ts b/extensions/telegram/src/account-inspect.test.ts index 5e58626ba03..54915edb61c 100644 --- a/extensions/telegram/src/account-inspect.test.ts +++ b/extensions/telegram/src/account-inspect.test.ts @@ -3,7 +3,7 @@ import os from "node:os"; import path from "node:path"; import { describe, expect, it } from "vitest"; import type { OpenClawConfig } from "../../../src/config/config.js"; -import { withEnv } from "../../../src/test-utils/env.js"; +import { withEnv } from "../../test-utils/env.js"; import { inspectTelegramAccount } from "./account-inspect.js"; describe("inspectTelegramAccount SecretRef resolution", () => { diff --git a/extensions/telegram/src/accounts.test.ts b/extensions/telegram/src/accounts.test.ts index fb83b9071a5..6155b89d0af 100644 --- a/extensions/telegram/src/accounts.test.ts +++ b/extensions/telegram/src/accounts.test.ts @@ -1,7 +1,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../../../src/config/config.js"; import * as subsystemModule from "../../../src/logging/subsystem.js"; -import { withEnv } from "../../../src/test-utils/env.js"; +import { withEnv } from "../../test-utils/env.js"; import { listTelegramAccountIds, resetMissingDefaultWarnFlag, diff --git a/extensions/telegram/src/bot.create-telegram-bot.test.ts b/extensions/telegram/src/bot.create-telegram-bot.test.ts index d3854849b10..3390aa3ff24 100644 --- a/extensions/telegram/src/bot.create-telegram-bot.test.ts +++ b/extensions/telegram/src/bot.create-telegram-bot.test.ts @@ -2,9 +2,9 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; -import { withEnvAsync } from "../../../src/test-utils/env.js"; -import { useFrozenTime, useRealTime } from "../../../src/test-utils/frozen-time.js"; import { escapeRegExp, formatEnvelopeTimestamp } from "../../../test/helpers/envelope-timestamp.js"; +import { withEnvAsync } from "../../test-utils/env.js"; +import { useFrozenTime, useRealTime } from "../../test-utils/frozen-time.js"; import { answerCallbackQuerySpy, botCtorSpy, diff --git a/extensions/telegram/src/bot.test.ts b/extensions/telegram/src/bot.test.ts index 17f6870a964..3266c080254 100644 --- a/extensions/telegram/src/bot.test.ts +++ b/extensions/telegram/src/bot.test.ts @@ -1,11 +1,11 @@ import { rm } from "node:fs/promises"; +import type { PluginInteractiveTelegramHandlerContext } from "openclaw/plugin-sdk/core"; import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { expectChannelInboundContextContract as expectInboundContextContract } from "../../../src/channels/plugins/contracts/suites.js"; import { clearPluginInteractiveHandlers, registerPluginInteractiveHandler, } from "../../../src/plugins/interactive.js"; -import type { PluginInteractiveTelegramHandlerContext } from "../../../src/plugins/types.js"; import { escapeRegExp, formatEnvelopeTimestamp } from "../../../test/helpers/envelope-timestamp.js"; import { answerCallbackQuerySpy, diff --git a/extensions/telegram/src/probe.test.ts b/extensions/telegram/src/probe.test.ts index 23a2051cfa0..970e2559540 100644 --- a/extensions/telegram/src/probe.test.ts +++ b/extensions/telegram/src/probe.test.ts @@ -1,5 +1,5 @@ import { afterEach, type Mock, describe, expect, it, vi } from "vitest"; -import { withFetchPreconnect } from "../../../src/test-utils/fetch-mock.js"; +import { withFetchPreconnect } from "../../test-utils/fetch-mock.js"; import { probeTelegram, resetTelegramProbeFetcherCacheForTests } from "./probe.js"; const resolveTelegramFetch = vi.hoisted(() => vi.fn()); diff --git a/extensions/test-utils/chunk-test-helpers.ts b/extensions/test-utils/chunk-test-helpers.ts new file mode 100644 index 00000000000..643e28e5c24 --- /dev/null +++ b/extensions/test-utils/chunk-test-helpers.ts @@ -0,0 +1 @@ +export { countLines, hasBalancedFences } from "../../src/test-utils/chunk-test-helpers.js"; diff --git a/extensions/test-utils/env.ts b/extensions/test-utils/env.ts new file mode 100644 index 00000000000..b171aa55a6c --- /dev/null +++ b/extensions/test-utils/env.ts @@ -0,0 +1 @@ +export { captureEnv, withEnv, withEnvAsync } from "../../src/test-utils/env.js"; diff --git a/extensions/test-utils/fetch-mock.ts b/extensions/test-utils/fetch-mock.ts new file mode 100644 index 00000000000..2cd6b65e680 --- /dev/null +++ b/extensions/test-utils/fetch-mock.ts @@ -0,0 +1 @@ +export { withFetchPreconnect, type FetchMock } from "../../src/test-utils/fetch-mock.js"; diff --git a/extensions/test-utils/frozen-time.ts b/extensions/test-utils/frozen-time.ts new file mode 100644 index 00000000000..ec31962fb76 --- /dev/null +++ b/extensions/test-utils/frozen-time.ts @@ -0,0 +1 @@ +export { useFrozenTime, useRealTime } from "../../src/test-utils/frozen-time.js"; diff --git a/extensions/test-utils/mock-http-response.ts b/extensions/test-utils/mock-http-response.ts new file mode 100644 index 00000000000..bf0d8bef20c --- /dev/null +++ b/extensions/test-utils/mock-http-response.ts @@ -0,0 +1 @@ +export { createMockServerResponse } from "../../src/test-utils/mock-http-response.js"; diff --git a/extensions/test-utils/plugin-command.ts b/extensions/test-utils/plugin-command.ts new file mode 100644 index 00000000000..3b6f3aad50f --- /dev/null +++ b/extensions/test-utils/plugin-command.ts @@ -0,0 +1 @@ +export type { OpenClawPluginCommandDefinition } from "openclaw/plugin-sdk/core"; diff --git a/extensions/test-utils/plugin-registration.ts b/extensions/test-utils/plugin-registration.ts new file mode 100644 index 00000000000..7a7da8ecdad --- /dev/null +++ b/extensions/test-utils/plugin-registration.ts @@ -0,0 +1 @@ +export { registerSingleProviderPlugin } from "../../src/test-utils/plugin-registration.js"; diff --git a/extensions/test-utils/provider-usage-fetch.ts b/extensions/test-utils/provider-usage-fetch.ts new file mode 100644 index 00000000000..d70a6e1657a --- /dev/null +++ b/extensions/test-utils/provider-usage-fetch.ts @@ -0,0 +1,4 @@ +export { + createProviderUsageFetch, + makeResponse, +} from "../../src/test-utils/provider-usage-fetch.js"; diff --git a/extensions/test-utils/temp-dir.ts b/extensions/test-utils/temp-dir.ts new file mode 100644 index 00000000000..3bd69bcc7b9 --- /dev/null +++ b/extensions/test-utils/temp-dir.ts @@ -0,0 +1 @@ +export { withTempDir } from "../../src/test-utils/temp-dir.js"; diff --git a/extensions/test-utils/typed-cases.ts b/extensions/test-utils/typed-cases.ts new file mode 100644 index 00000000000..4b6bd35b1ec --- /dev/null +++ b/extensions/test-utils/typed-cases.ts @@ -0,0 +1 @@ +export { typedCases } from "../../src/test-utils/typed-cases.js"; diff --git a/extensions/tlon/index.ts b/extensions/tlon/index.ts index 2927a9a4b53..9ae569fea03 100644 --- a/extensions/tlon/index.ts +++ b/extensions/tlon/index.ts @@ -2,14 +2,12 @@ import { spawn } from "node:child_process"; import { existsSync } from "node:fs"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; -import type { OpenClawPluginApi } from "openclaw/plugin-sdk/tlon"; -import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/tlon"; +import { defineChannelPluginEntry } from "openclaw/plugin-sdk/core"; import { tlonPlugin } from "./src/channel.js"; import { setTlonRuntime } from "./src/runtime.js"; const __dirname = dirname(fileURLToPath(import.meta.url)); -// Whitelist of allowed tlon subcommands const ALLOWED_TLON_COMMANDS = new Set([ "activity", "channels", @@ -24,40 +22,29 @@ const ALLOWED_TLON_COMMANDS = new Set([ "version", ]); -/** - * Find the tlon binary from the skill package - */ let cachedTlonBinary: string | undefined; function findTlonBinary(): string { if (cachedTlonBinary) { return cachedTlonBinary; } - // Check in node_modules/.bin const skillBin = join(__dirname, "node_modules", ".bin", "tlon"); if (existsSync(skillBin)) { cachedTlonBinary = skillBin; return skillBin; } - // Check for platform-specific binary directly - const platform = process.platform; - const arch = process.arch; - const platformPkg = `@tloncorp/tlon-skill-${platform}-${arch}`; + const platformPkg = `@tloncorp/tlon-skill-${process.platform}-${process.arch}`; const platformBin = join(__dirname, "node_modules", platformPkg, "tlon"); if (existsSync(platformBin)) { cachedTlonBinary = platformBin; return platformBin; } - // Fallback to PATH cachedTlonBinary = "tlon"; return cachedTlonBinary; } -/** - * Shell-like argument splitter that respects quotes - */ function shellSplit(str: string): string[] { const args: string[] = []; let cur = ""; @@ -92,18 +79,15 @@ function shellSplit(str: string): string[] { } cur += ch; } - if (cur) args.push(cur); + if (cur) { + args.push(cur); + } return args; } -/** - * Run the tlon command and return the result - */ function runTlonCommand(binary: string, args: string[]): Promise { return new Promise((resolve, reject) => { - const child = spawn(binary, args, { - env: process.env, - }); + const child = spawn(binary, args, { env: process.env }); let stdout = ""; let stderr = ""; @@ -123,25 +107,20 @@ function runTlonCommand(binary: string, args: string[]): Promise { child.on("close", (code) => { if (code !== 0) { reject(new Error(stderr || `tlon exited with code ${code}`)); - } else { - resolve(stdout); + return; } + resolve(stdout); }); }); } -const plugin = { +export default defineChannelPluginEntry({ id: "tlon", name: "Tlon", description: "Tlon/Urbit channel plugin", - configSchema: emptyPluginConfigSchema(), - register(api: OpenClawPluginApi) { - setTlonRuntime(api.runtime); - api.registerChannel({ plugin: tlonPlugin }); - if (api.registrationMode !== "full") { - return; - } - + plugin: tlonPlugin, + setRuntime: setTlonRuntime, + registerFull(api) { api.logger.debug?.("[tlon] Registering tlon tool"); api.registerTool({ name: "tlon", @@ -164,9 +143,6 @@ const plugin = { async execute(_id: string, params: { command: string }) { try { const args = shellSplit(params.command); - const tlonBinary = findTlonBinary(); - - // Validate first argument is a whitelisted tlon subcommand const subcommand = args[0]; if (!ALLOWED_TLON_COMMANDS.has(subcommand)) { return { @@ -180,7 +156,7 @@ const plugin = { }; } - const output = await runTlonCommand(tlonBinary, args); + const output = await runTlonCommand(findTlonBinary(), args); return { content: [{ type: "text" as const, text: output }], details: undefined, @@ -194,6 +170,4 @@ const plugin = { }, }); }, -}; - -export default plugin; +}); diff --git a/extensions/tlon/setup-entry.ts b/extensions/tlon/setup-entry.ts index 667e917c8da..6a14ba3bade 100644 --- a/extensions/tlon/setup-entry.ts +++ b/extensions/tlon/setup-entry.ts @@ -1,5 +1,4 @@ +import { defineSetupPluginEntry } from "openclaw/plugin-sdk/core"; import { tlonPlugin } from "./src/channel.js"; -export default { - plugin: tlonPlugin, -}; +export default defineSetupPluginEntry(tlonPlugin); diff --git a/extensions/twitch/index.ts b/extensions/twitch/index.ts index cbdb20bff4d..1a4ea89185c 100644 --- a/extensions/twitch/index.ts +++ b/extensions/twitch/index.ts @@ -1,20 +1,13 @@ -import type { OpenClawPluginApi } from "openclaw/plugin-sdk/twitch"; -import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/twitch"; +import { defineChannelPluginEntry } from "openclaw/plugin-sdk/core"; import { twitchPlugin } from "./src/plugin.js"; import { setTwitchRuntime } from "./src/runtime.js"; export { monitorTwitchProvider } from "./src/monitor.js"; -const plugin = { +export default defineChannelPluginEntry({ id: "twitch", name: "Twitch", - description: "Twitch channel plugin", - configSchema: emptyPluginConfigSchema(), - register(api: OpenClawPluginApi) { - setTwitchRuntime(api.runtime); - // oxlint-disable-next-line typescript/no-explicit-any - api.registerChannel({ plugin: twitchPlugin as any }); - }, -}; - -export default plugin; + description: "Twitch chat channel plugin", + plugin: twitchPlugin, + setRuntime: setTwitchRuntime, +}); diff --git a/extensions/whatsapp/index.ts b/extensions/whatsapp/index.ts index c0f097ddf7d..da16917fa43 100644 --- a/extensions/whatsapp/index.ts +++ b/extensions/whatsapp/index.ts @@ -1,17 +1,11 @@ -import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core"; -import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/core"; +import { defineChannelPluginEntry } from "openclaw/plugin-sdk/core"; import { whatsappPlugin } from "./src/channel.js"; import { setWhatsAppRuntime } from "./src/runtime.js"; -const plugin = { +export default defineChannelPluginEntry({ id: "whatsapp", name: "WhatsApp", description: "WhatsApp channel plugin", - configSchema: emptyPluginConfigSchema(), - register(api: OpenClawPluginApi) { - setWhatsAppRuntime(api.runtime); - api.registerChannel({ plugin: whatsappPlugin }); - }, -}; - -export default plugin; + plugin: whatsappPlugin, + setRuntime: setWhatsAppRuntime, +}); diff --git a/extensions/whatsapp/setup-entry.ts b/extensions/whatsapp/setup-entry.ts index 5b18e10073b..a01efecdc36 100644 --- a/extensions/whatsapp/setup-entry.ts +++ b/extensions/whatsapp/setup-entry.ts @@ -1,3 +1,4 @@ +import { defineSetupPluginEntry } from "openclaw/plugin-sdk/core"; import { whatsappSetupPlugin } from "./src/channel.setup.js"; -export default { plugin: whatsappSetupPlugin }; +export default defineSetupPluginEntry(whatsappSetupPlugin); diff --git a/extensions/whatsapp/src/accounts.whatsapp-auth.test.ts b/extensions/whatsapp/src/accounts.whatsapp-auth.test.ts index 349bccc65e5..43d1739e13f 100644 --- a/extensions/whatsapp/src/accounts.whatsapp-auth.test.ts +++ b/extensions/whatsapp/src/accounts.whatsapp-auth.test.ts @@ -2,7 +2,7 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; -import { captureEnv } from "../../../src/test-utils/env.js"; +import { captureEnv } from "../../test-utils/env.js"; import { hasAnyWhatsAppAuth, listWhatsAppAuthDirs } from "./accounts.js"; describe("hasAnyWhatsAppAuth", () => { diff --git a/extensions/whatsapp/src/auto-reply.web-auto-reply.connection-and-logging.e2e.test.ts b/extensions/whatsapp/src/auto-reply.web-auto-reply.connection-and-logging.e2e.test.ts index dd324f47351..6a5184fc059 100644 --- a/extensions/whatsapp/src/auto-reply.web-auto-reply.connection-and-logging.e2e.test.ts +++ b/extensions/whatsapp/src/auto-reply.web-auto-reply.connection-and-logging.e2e.test.ts @@ -4,8 +4,8 @@ import fs from "node:fs/promises"; import { beforeAll, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../../../src/config/config.js"; import { setLoggerOverride } from "../../../src/logging.js"; -import { withEnvAsync } from "../../../src/test-utils/env.js"; import { escapeRegExp, formatEnvelopeTimestamp } from "../../../test/helpers/envelope-timestamp.js"; +import { withEnvAsync } from "../../test-utils/env.js"; import { createMockWebListener, createWebListenerFactoryCapture, diff --git a/extensions/whatsapp/src/auto-reply/web-auto-reply-utils.test.ts b/extensions/whatsapp/src/auto-reply/web-auto-reply-utils.test.ts index 0107fa126d7..d1011f5c7f8 100644 --- a/extensions/whatsapp/src/auto-reply/web-auto-reply-utils.test.ts +++ b/extensions/whatsapp/src/auto-reply/web-auto-reply-utils.test.ts @@ -2,7 +2,7 @@ import fs from "node:fs/promises"; import path from "node:path"; import { describe, expect, it, vi } from "vitest"; import { saveSessionStore } from "../../../../src/config/sessions.js"; -import { withTempDir } from "../../../../src/test-utils/temp-dir.js"; +import { withTempDir } from "../../../test-utils/temp-dir.js"; import { debugMention, isBotMentionedFromTargets, diff --git a/extensions/whatsapp/src/media.test.ts b/extensions/whatsapp/src/media.test.ts index e21d58b4bb7..45f3fbae309 100644 --- a/extensions/whatsapp/src/media.test.ts +++ b/extensions/whatsapp/src/media.test.ts @@ -7,8 +7,8 @@ import { resolveStateDir } from "../../../src/config/paths.js"; import { resolvePreferredOpenClawTmpDir } from "../../../src/infra/tmp-openclaw-dir.js"; import { optimizeImageToPng } from "../../../src/media/image-ops.js"; import { mockPinnedHostnameResolution } from "../../../src/test-helpers/ssrf.js"; -import { captureEnv } from "../../../src/test-utils/env.js"; import { sendVoiceMessageDiscord } from "../../discord/src/send.js"; +import { captureEnv } from "../../test-utils/env.js"; import { LocalMediaAccessError, loadWebMedia, diff --git a/extensions/zalo/index.ts b/extensions/zalo/index.ts index ef62ee6e560..c5091444450 100644 --- a/extensions/zalo/index.ts +++ b/extensions/zalo/index.ts @@ -1,17 +1,11 @@ -import type { OpenClawPluginApi } from "openclaw/plugin-sdk/zalo"; -import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/zalo"; +import { defineChannelPluginEntry } from "openclaw/plugin-sdk/core"; import { zaloPlugin } from "./src/channel.js"; import { setZaloRuntime } from "./src/runtime.js"; -const plugin = { +export default defineChannelPluginEntry({ id: "zalo", name: "Zalo", - description: "Zalo channel plugin (Bot API)", - configSchema: emptyPluginConfigSchema(), - register(api: OpenClawPluginApi) { - setZaloRuntime(api.runtime); - api.registerChannel(zaloPlugin); - }, -}; - -export default plugin; + description: "Zalo channel plugin", + plugin: zaloPlugin, + setRuntime: setZaloRuntime, +}); diff --git a/extensions/zalo/setup-entry.ts b/extensions/zalo/setup-entry.ts index dd8ca1b70f8..d26b0f93fe0 100644 --- a/extensions/zalo/setup-entry.ts +++ b/extensions/zalo/setup-entry.ts @@ -1,5 +1,4 @@ +import { defineSetupPluginEntry } from "openclaw/plugin-sdk/core"; import { zaloPlugin } from "./src/channel.js"; -export default { - plugin: zaloPlugin, -}; +export default defineSetupPluginEntry(zaloPlugin); diff --git a/extensions/zalouser/index.ts b/extensions/zalouser/index.ts index 8d470b043e3..2199567cff8 100644 --- a/extensions/zalouser/index.ts +++ b/extensions/zalouser/index.ts @@ -1,21 +1,16 @@ -import type { AnyAgentTool, OpenClawPluginApi } from "openclaw/plugin-sdk/zalouser"; -import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/zalouser"; +import { defineChannelPluginEntry } from "openclaw/plugin-sdk/core"; +import type { AnyAgentTool } from "openclaw/plugin-sdk/zalouser"; import { zalouserPlugin } from "./src/channel.js"; import { setZalouserRuntime } from "./src/runtime.js"; import { ZalouserToolSchema, executeZalouserTool } from "./src/tool.js"; -const plugin = { +export default defineChannelPluginEntry({ id: "zalouser", name: "Zalo Personal", description: "Zalo personal account messaging via native zca-js integration", - configSchema: emptyPluginConfigSchema(), - register(api: OpenClawPluginApi) { - setZalouserRuntime(api.runtime); - api.registerChannel(zalouserPlugin); - if (api.registrationMode !== "full") { - return; - } - + plugin: zalouserPlugin, + setRuntime: setZalouserRuntime, + registerFull(api) { api.registerTool({ name: "zalouser", label: "Zalo Personal", @@ -27,6 +22,4 @@ const plugin = { execute: executeZalouserTool, } as AnyAgentTool); }, -}; - -export default plugin; +}); diff --git a/extensions/zalouser/setup-entry.ts b/extensions/zalouser/setup-entry.ts index f983cad8f80..0320d3cf945 100644 --- a/extensions/zalouser/setup-entry.ts +++ b/extensions/zalouser/setup-entry.ts @@ -1,5 +1,4 @@ +import { defineSetupPluginEntry } from "openclaw/plugin-sdk/core"; import { zalouserPlugin } from "./src/channel.js"; -export default { - plugin: zalouserPlugin, -}; +export default defineSetupPluginEntry(zalouserPlugin); diff --git a/package.json b/package.json index 4bb825d0d7a..08acac5db40 100644 --- a/package.json +++ b/package.json @@ -443,7 +443,7 @@ "build:plugin-sdk:dts": "tsc -p tsconfig.plugin-sdk.dts.json || true", "build:strict-smoke": "pnpm canvas:a2ui:bundle && node scripts/tsdown-build.mjs && node scripts/runtime-postbuild.mjs && pnpm build:plugin-sdk:dts", "canvas:a2ui:bundle": "bash scripts/bundle-a2ui.sh", - "check": "pnpm check:host-env-policy:swift && pnpm format:check && pnpm tsgo && pnpm plugin-sdk:check-exports && pnpm lint && pnpm lint:tmp:no-random-messaging && pnpm lint:tmp:channel-agnostic-boundaries && pnpm lint:tmp:no-raw-channel-fetch && pnpm lint:agent:ingress-owner && pnpm lint:plugins:no-register-http-handler && pnpm lint:plugins:no-monolithic-plugin-sdk-entry-imports && pnpm lint:webhook:no-low-level-body-read && pnpm lint:auth:no-pairing-store-group && pnpm lint:auth:pairing-account-scope", + "check": "pnpm check:host-env-policy:swift && pnpm format:check && pnpm tsgo && pnpm plugin-sdk:check-exports && pnpm lint && pnpm lint:tmp:no-random-messaging && pnpm lint:tmp:channel-agnostic-boundaries && pnpm lint:tmp:no-raw-channel-fetch && pnpm lint:agent:ingress-owner && pnpm lint:plugins:no-register-http-handler && pnpm lint:plugins:no-monolithic-plugin-sdk-entry-imports && pnpm lint:plugins:no-extension-test-core-imports && pnpm lint:webhook:no-low-level-body-read && pnpm lint:auth:no-pairing-store-group && pnpm lint:auth:pairing-account-scope", "check:docs": "pnpm format:docs:check && pnpm lint:docs && pnpm docs:check-i18n-glossary && pnpm docs:check-links", "check:host-env-policy:swift": "node scripts/generate-host-env-security-policy-swift.mjs --check", "check:loc": "node --import tsx scripts/check-ts-max-loc.ts --max 500", @@ -495,6 +495,7 @@ "lint:docs": "pnpm dlx markdownlint-cli2", "lint:docs:fix": "pnpm dlx markdownlint-cli2 --fix", "lint:fix": "oxlint --type-aware --fix && pnpm format", + "lint:plugins:no-extension-test-core-imports": "node --import tsx scripts/check-no-extension-test-core-imports.ts", "lint:plugins:no-monolithic-plugin-sdk-entry-imports": "node --import tsx scripts/check-no-monolithic-plugin-sdk-entry-imports.ts", "lint:plugins:no-register-http-handler": "node scripts/check-no-register-http-handler.mjs", "lint:swift": "swiftlint lint --config .swiftlint.yml && (cd apps/ios && swiftlint lint --config .swiftlint.yml)", diff --git a/scripts/check-no-extension-test-core-imports.ts b/scripts/check-no-extension-test-core-imports.ts new file mode 100644 index 00000000000..b8e3b1bc764 --- /dev/null +++ b/scripts/check-no-extension-test-core-imports.ts @@ -0,0 +1,90 @@ +import fs from "node:fs"; +import path from "node:path"; + +const FORBIDDEN_PATTERNS: Array<{ pattern: RegExp; hint: string }> = [ + { + pattern: /["']openclaw\/plugin-sdk["']/, + hint: "Use openclaw/plugin-sdk/ instead of the monolithic root entry.", + }, + { + pattern: /["']openclaw\/plugin-sdk\/compat["']/, + hint: "Use a focused public plugin-sdk subpath instead of compat.", + }, + { + pattern: /["'](?:\.\.\/)+(?:src\/test-utils\/)[^"']+["']/, + hint: "Use extensions/test-utils/* bridges for shared extension test helpers.", + }, + { + pattern: /["'](?:\.\.\/)+(?:src\/plugins\/types\.js)["']/, + hint: "Use public plugin-sdk/core types or extensions/test-utils bridges instead.", + }, +]; + +function isExtensionTestFile(filePath: string): boolean { + return /\.test\.[cm]?[jt]sx?$/u.test(filePath) || /\.e2e\.test\.[cm]?[jt]sx?$/u.test(filePath); +} + +function collectExtensionTestFiles(rootDir: string): string[] { + const files: string[] = []; + const stack = [rootDir]; + while (stack.length > 0) { + const current = stack.pop(); + if (!current) { + continue; + } + let entries: fs.Dirent[] = []; + try { + entries = fs.readdirSync(current, { withFileTypes: true }); + } catch { + continue; + } + for (const entry of entries) { + const fullPath = path.join(current, entry.name); + if (entry.isDirectory()) { + if (entry.name === "node_modules" || entry.name === "dist" || entry.name === "coverage") { + continue; + } + stack.push(fullPath); + continue; + } + if (entry.isFile() && isExtensionTestFile(fullPath)) { + files.push(fullPath); + } + } + } + return files; +} + +function main() { + const extensionsDir = path.join(process.cwd(), "extensions"); + const files = collectExtensionTestFiles(extensionsDir); + const offenders: Array<{ file: string; hint: string }> = []; + + for (const file of files) { + const content = fs.readFileSync(file, "utf8"); + for (const rule of FORBIDDEN_PATTERNS) { + if (!rule.pattern.test(content)) { + continue; + } + offenders.push({ file, hint: rule.hint }); + break; + } + } + + if (offenders.length > 0) { + console.error( + "Extension test files must stay on extension test bridges or public plugin-sdk seams.", + ); + for (const offender of offenders.toSorted((a, b) => a.file.localeCompare(b.file))) { + const relative = path.relative(process.cwd(), offender.file) || offender.file; + console.error(`- ${relative}: ${offender.hint}`); + } + process.exit(1); + } + + console.log( + `OK: extension test files avoid direct core test/internal imports (${files.length} checked).`, + ); +} + +main(); diff --git a/src/plugin-sdk/core.ts b/src/plugin-sdk/core.ts index a6c842e79d5..1cfea088601 100644 --- a/src/plugin-sdk/core.ts +++ b/src/plugin-sdk/core.ts @@ -1,3 +1,13 @@ +import type { ChannelPlugin } from "../channels/plugins/types.plugin.js"; +import { emptyPluginConfigSchema } from "../plugins/config-schema.js"; +import type { PluginRuntime } from "../plugins/runtime/types.js"; +import type { + OpenClawPluginApi, + OpenClawPluginCommandDefinition, + OpenClawPluginConfigSchema, + PluginInteractiveTelegramHandlerContext, +} from "../plugins/types.js"; + export type { AnyAgentTool, MediaUnderstandingProviderPlugin, @@ -31,6 +41,8 @@ export type { ProviderAuthMethodNonInteractiveContext, ProviderAuthMethod, ProviderAuthResult, + OpenClawPluginCommandDefinition, + PluginInteractiveTelegramHandlerContext, } from "../plugins/types.js"; export type { OpenClawConfig } from "../config/config.js"; export type { GatewayRequestHandlerOptions } from "../gateway/server-methods/types.js"; @@ -70,3 +82,44 @@ export { export { buildOutboundBaseSessionKey } from "../infra/outbound/base-session-key.js"; export { normalizeOutboundThreadId } from "../infra/outbound/thread-id.js"; export { resolveThreadSessionKeys } from "../routing/session-key.js"; + +type DefineChannelPluginEntryOptions = { + id: string; + name: string; + description: string; + plugin: TPlugin; + configSchema?: () => OpenClawPluginConfigSchema; + setRuntime?: (runtime: PluginRuntime) => void; + registerFull?: (api: OpenClawPluginApi) => void; +}; + +// Shared channel-plugin entry boilerplate for bundled and third-party channels. +export function defineChannelPluginEntry({ + id, + name, + description, + plugin, + configSchema = emptyPluginConfigSchema, + setRuntime, + registerFull, +}: DefineChannelPluginEntryOptions) { + return { + id, + name, + description, + configSchema: configSchema(), + register(api: OpenClawPluginApi) { + setRuntime?.(api.runtime); + api.registerChannel({ plugin }); + if (api.registrationMode !== "full") { + return; + } + registerFull?.(api); + }, + }; +} + +// Shared setup-entry shape so bundled channels do not duplicate `{ plugin }`. +export function defineSetupPluginEntry(plugin: TPlugin) { + return { plugin }; +} diff --git a/src/plugin-sdk/subpaths.test.ts b/src/plugin-sdk/subpaths.test.ts index 0166fb52081..156f7d9b81f 100644 --- a/src/plugin-sdk/subpaths.test.ts +++ b/src/plugin-sdk/subpaths.test.ts @@ -49,6 +49,8 @@ describe("plugin-sdk subpath exports", () => { it("keeps core focused on generic shared exports", () => { expect(typeof coreSdk.emptyPluginConfigSchema).toBe("function"); + expect(typeof coreSdk.defineChannelPluginEntry).toBe("function"); + expect(typeof coreSdk.defineSetupPluginEntry).toBe("function"); expect("runPassiveAccountLifecycle" in asExports(coreSdk)).toBe(false); expect("createLoggerBackedRuntime" in asExports(coreSdk)).toBe(false); expect("registerSandboxBackend" in asExports(coreSdk)).toBe(false);