diff --git a/extensions/discord/src/channel-actions.contract.test.ts b/extensions/discord/src/channel-actions.contract.test.ts new file mode 100644 index 00000000000..0fd7e980f5e --- /dev/null +++ b/extensions/discord/src/channel-actions.contract.test.ts @@ -0,0 +1,45 @@ +import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime"; +import { describe } from "vitest"; +import { installChannelActionsContractSuite } from "../../../test/helpers/channels/registry-contract-suites.js"; +import { discordPlugin } from "../api.js"; + +describe("discord actions contract", () => { + installChannelActionsContractSuite({ + plugin: discordPlugin, + cases: [ + { + name: "describes configured Discord actions and capabilities", + cfg: { + channels: { + discord: { + token: "Bot token-main", + actions: { + polls: true, + reactions: true, + permissions: false, + messages: false, + pins: false, + threads: false, + search: false, + stickers: false, + memberInfo: false, + roleInfo: false, + emojiUploads: false, + stickerUploads: false, + channelInfo: false, + channels: false, + voiceStatus: false, + events: false, + roles: false, + moderation: false, + presence: false, + }, + }, + }, + } as OpenClawConfig, + expectedActions: ["send", "poll", "react", "reactions", "emoji-list"], + expectedCapabilities: ["interactive", "components"], + }, + ], + }); +}); diff --git a/extensions/line/src/channel-setup-status.contract.test.ts b/extensions/line/src/channel-setup-status.contract.test.ts new file mode 100644 index 00000000000..c0e8942ef94 --- /dev/null +++ b/extensions/line/src/channel-setup-status.contract.test.ts @@ -0,0 +1,70 @@ +import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime"; +import { describe, expect } from "vitest"; +import { + installChannelSetupContractSuite, + installChannelStatusContractSuite, +} from "../../../test/helpers/channels/registry-contract-suites.js"; +import { linePlugin, lineSetupPlugin } from "../api.js"; + +describe("line setup contract", () => { + installChannelSetupContractSuite({ + plugin: lineSetupPlugin, + cases: [ + { + name: "default account stores token and secret", + cfg: {} as OpenClawConfig, + input: { + channelAccessToken: "line-token", + channelSecret: "line-secret", + } as never, + expectedAccountId: "default", + assertPatchedConfig: (cfg) => { + expect(cfg.channels?.line?.enabled).toBe(true); + expect(cfg.channels?.line?.channelAccessToken).toBe("line-token"); + expect(cfg.channels?.line?.channelSecret).toBe("line-secret"); + }, + }, + { + name: "non-default env setup is rejected", + cfg: {} as OpenClawConfig, + accountId: "ops", + input: { + useEnv: true, + }, + expectedAccountId: "ops", + expectedValidation: "LINE_CHANNEL_ACCESS_TOKEN can only be used for the default account.", + }, + ], + }); +}); + +describe("line status contract", () => { + installChannelStatusContractSuite({ + plugin: linePlugin, + cases: [ + { + name: "configured account produces a webhook status snapshot", + cfg: { + channels: { + line: { + enabled: true, + channelAccessToken: "line-token", + channelSecret: "line-secret", + }, + }, + } as OpenClawConfig, + runtime: { + accountId: "default", + running: true, + }, + probe: { ok: true }, + assertSnapshot: (snapshot) => { + expect(snapshot.accountId).toBe("default"); + expect(snapshot.enabled).toBe(true); + expect(snapshot.configured).toBe(true); + expect(snapshot.mode).toBe("webhook"); + }, + }, + ], + }); +}); diff --git a/extensions/mattermost/src/channel-actions-setup-status.contract.test.ts b/extensions/mattermost/src/channel-actions-setup-status.contract.test.ts new file mode 100644 index 00000000000..d2f6c0b6890 --- /dev/null +++ b/extensions/mattermost/src/channel-actions-setup-status.contract.test.ts @@ -0,0 +1,122 @@ +import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime"; +import { describe, expect } from "vitest"; +import { + installChannelActionsContractSuite, + installChannelSetupContractSuite, + installChannelStatusContractSuite, +} from "../../../test/helpers/channels/registry-contract-suites.js"; +import { mattermostPlugin, mattermostSetupPlugin } from "../channel-plugin-api.js"; + +describe("mattermost actions contract", () => { + installChannelActionsContractSuite({ + plugin: mattermostPlugin, + unsupportedAction: "poll", + cases: [ + { + name: "configured account exposes send and react", + cfg: { + channels: { + mattermost: { + enabled: true, + botToken: "test-token", + baseUrl: "https://chat.example.com", + }, + }, + } as OpenClawConfig, + expectedActions: ["send", "react"], + expectedCapabilities: ["buttons"], + }, + { + name: "reactions can be disabled while send stays available", + cfg: { + channels: { + mattermost: { + enabled: true, + botToken: "test-token", + baseUrl: "https://chat.example.com", + actions: { reactions: false }, + }, + }, + } as OpenClawConfig, + expectedActions: ["send"], + expectedCapabilities: ["buttons"], + }, + { + name: "missing bot credentials disables the actions surface", + cfg: { + channels: { + mattermost: { + enabled: true, + }, + }, + } as OpenClawConfig, + expectedActions: [], + expectedCapabilities: [], + }, + ], + }); +}); + +describe("mattermost setup contract", () => { + installChannelSetupContractSuite({ + plugin: mattermostSetupPlugin, + cases: [ + { + name: "default account stores token and normalized base URL", + cfg: {} as OpenClawConfig, + input: { + botToken: "test-token", + httpUrl: "https://chat.example.com/", + }, + expectedAccountId: "default", + assertPatchedConfig: (cfg) => { + expect(cfg.channels?.mattermost?.enabled).toBe(true); + expect(cfg.channels?.mattermost?.botToken).toBe("test-token"); + expect(cfg.channels?.mattermost?.baseUrl).toBe("https://chat.example.com"); + }, + }, + { + name: "missing credentials are rejected", + cfg: {} as OpenClawConfig, + input: { + httpUrl: "", + }, + expectedAccountId: "default", + expectedValidation: "Mattermost requires --bot-token and --http-url (or --use-env).", + }, + ], + }); +}); + +describe("mattermost status contract", () => { + installChannelStatusContractSuite({ + plugin: mattermostPlugin, + cases: [ + { + name: "configured account preserves connectivity details in the snapshot", + cfg: { + channels: { + mattermost: { + enabled: true, + botToken: "test-token", + baseUrl: "https://chat.example.com", + }, + }, + } as OpenClawConfig, + runtime: { + accountId: "default", + connected: true, + lastConnectedAt: 1234, + }, + probe: { ok: true }, + assertSnapshot: (snapshot) => { + expect(snapshot.accountId).toBe("default"); + expect(snapshot.enabled).toBe(true); + expect(snapshot.configured).toBe(true); + expect(snapshot.connected).toBe(true); + expect(snapshot.baseUrl).toBe("https://chat.example.com"); + }, + }, + ], + }); +}); diff --git a/extensions/slack/src/channel-actions-setup-status.contract.test.ts b/extensions/slack/src/channel-actions-setup-status.contract.test.ts new file mode 100644 index 00000000000..ef709796752 --- /dev/null +++ b/extensions/slack/src/channel-actions-setup-status.contract.test.ts @@ -0,0 +1,137 @@ +import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime"; +import { describe, expect } from "vitest"; +import { + installChannelActionsContractSuite, + installChannelSetupContractSuite, + installChannelStatusContractSuite, +} from "../../../test/helpers/channels/registry-contract-suites.js"; +import { slackPlugin } from "../api.js"; +import { slackSetupPlugin } from "../setup-plugin-api.js"; + +const slackDefaultActions = [ + "send", + "react", + "reactions", + "read", + "edit", + "delete", + "download-file", + "upload-file", + "pin", + "unpin", + "list-pins", + "member-info", + "emoji-list", +] as const; + +describe("slack actions contract", () => { + installChannelActionsContractSuite({ + plugin: slackPlugin, + unsupportedAction: "poll", + cases: [ + { + name: "configured account exposes default Slack actions", + cfg: { + channels: { + slack: { + botToken: "xoxb-test", + appToken: "xapp-test", + }, + }, + } as OpenClawConfig, + expectedActions: slackDefaultActions, + expectedCapabilities: ["blocks"], + }, + { + name: "interactive replies add the shared interactive capability", + cfg: { + channels: { + slack: { + botToken: "xoxb-test", + appToken: "xapp-test", + capabilities: { + interactiveReplies: true, + }, + }, + }, + } as OpenClawConfig, + expectedActions: slackDefaultActions, + expectedCapabilities: ["blocks", "interactive"], + }, + { + name: "missing tokens disables the actions surface", + cfg: { + channels: { + slack: { + enabled: true, + }, + }, + } as OpenClawConfig, + expectedActions: [], + expectedCapabilities: [], + }, + ], + }); +}); + +describe("slack setup contract", () => { + installChannelSetupContractSuite({ + plugin: slackSetupPlugin, + cases: [ + { + name: "default account stores tokens and enables the channel", + cfg: {} as OpenClawConfig, + input: { + botToken: "xoxb-test", + appToken: "xapp-test", + }, + expectedAccountId: "default", + assertPatchedConfig: (cfg) => { + expect(cfg.channels?.slack?.enabled).toBe(true); + expect(cfg.channels?.slack?.botToken).toBe("xoxb-test"); + expect(cfg.channels?.slack?.appToken).toBe("xapp-test"); + }, + }, + { + name: "non-default env setup is rejected", + cfg: {} as OpenClawConfig, + accountId: "ops", + input: { + useEnv: true, + }, + expectedAccountId: "ops", + expectedValidation: "Slack env tokens can only be used for the default account.", + }, + ], + }); +}); + +describe("slack status contract", () => { + installChannelStatusContractSuite({ + plugin: slackPlugin, + cases: [ + { + name: "configured account produces a configured status snapshot", + cfg: { + channels: { + slack: { + botToken: "xoxb-test", + appToken: "xapp-test", + }, + }, + } as OpenClawConfig, + runtime: { + accountId: "default", + connected: true, + running: true, + }, + probe: { ok: true }, + assertSnapshot: (snapshot) => { + expect(snapshot.accountId).toBe("default"); + expect(snapshot.enabled).toBe(true); + expect(snapshot.configured).toBe(true); + }, + }, + ], + }); +}); diff --git a/extensions/telegram/src/channel-actions.contract.test.ts b/extensions/telegram/src/channel-actions.contract.test.ts new file mode 100644 index 00000000000..cfe70faf795 --- /dev/null +++ b/extensions/telegram/src/channel-actions.contract.test.ts @@ -0,0 +1,24 @@ +import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime"; +import { describe } from "vitest"; +import { installChannelActionsContractSuite } from "../../../test/helpers/channels/registry-contract-suites.js"; +import { telegramPlugin } from "../api.js"; + +describe("telegram actions contract", () => { + installChannelActionsContractSuite({ + plugin: telegramPlugin, + cases: [ + { + name: "exposes configured Telegram actions and capabilities", + cfg: { + channels: { + telegram: { + botToken: "123:telegram-test-token", + }, + }, + } as OpenClawConfig, + expectedActions: ["send", "poll", "react", "delete", "edit", "topic-create", "topic-edit"], + expectedCapabilities: ["interactive", "buttons"], + }, + ], + }); +}); diff --git a/src/channels/plugins/contracts/actions.registry-backed.contract.test.ts b/src/channels/plugins/contracts/actions.registry-backed.contract.test.ts deleted file mode 100644 index 11b640d6195..00000000000 --- a/src/channels/plugins/contracts/actions.registry-backed.contract.test.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { describe } from "vitest"; -import { getActionContractRegistry } from "../../../../test/helpers/channels/registry-actions.js"; -import { installChannelActionsContractSuite } from "../../../../test/helpers/channels/registry-contract-suites.js"; - -for (const entry of getActionContractRegistry()) { - describe(`${entry.id} actions contract`, () => { - installChannelActionsContractSuite({ - plugin: entry.plugin, - cases: entry.cases as never, - unsupportedAction: entry.unsupportedAction as never, - }); - }); -} diff --git a/src/channels/plugins/contracts/setup.registry-backed.contract.test.ts b/src/channels/plugins/contracts/setup.registry-backed.contract.test.ts deleted file mode 100644 index a13fef852bc..00000000000 --- a/src/channels/plugins/contracts/setup.registry-backed.contract.test.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { describe } from "vitest"; -import { installChannelSetupContractSuite } from "../../../../test/helpers/channels/registry-contract-suites.js"; -import { getSetupContractRegistry } from "../../../../test/helpers/channels/registry-setup-status.js"; - -for (const entry of getSetupContractRegistry()) { - describe(`${entry.id} setup contract`, () => { - installChannelSetupContractSuite({ - plugin: entry.plugin, - cases: entry.cases as never, - }); - }); -} diff --git a/src/channels/plugins/contracts/status.registry-backed.contract.test.ts b/src/channels/plugins/contracts/status.registry-backed.contract.test.ts deleted file mode 100644 index 574c7c819a3..00000000000 --- a/src/channels/plugins/contracts/status.registry-backed.contract.test.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { describe } from "vitest"; -import { installChannelStatusContractSuite } from "../../../../test/helpers/channels/registry-contract-suites.js"; -import { getStatusContractRegistry } from "../../../../test/helpers/channels/registry-setup-status.js"; - -for (const entry of getStatusContractRegistry()) { - describe(`${entry.id} status contract`, () => { - installChannelStatusContractSuite({ - plugin: entry.plugin, - cases: entry.cases as never, - }); - }); -} diff --git a/src/commands/onboard-channels.e2e.test.ts b/src/commands/onboard-channels.e2e.test.ts index d330fd3c8aa..965dd80580c 100644 --- a/src/commands/onboard-channels.e2e.test.ts +++ b/src/commands/onboard-channels.e2e.test.ts @@ -1,8 +1,4 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; -import { - matrixSetupAdapter, - matrixSetupWizard, -} from "../../test/helpers/channels/matrix-setup-contract.js"; import type { ChannelPluginCatalogEntry } from "../channels/plugins/catalog.js"; import { ensureChannelSetupPluginInstalled, @@ -118,117 +114,6 @@ function createMSTeamsCatalogEntry(): ChannelPluginCatalogEntry { }; } -async function setMatrixOnboardingRegistryForTests(): Promise { - setActivePluginRegistry( - createTestRegistry([ - { - pluginId: "matrix", - source: "test", - plugin: { - ...createChannelTestPluginBase({ - id: "matrix", - label: "Matrix", - capabilities: { chatTypes: ["direct", "group", "thread"] }, - }), - meta: { - id: "matrix", - label: "Matrix", - selectionLabel: "Matrix (plugin)", - docsPath: "/channels/matrix", - blurb: "open protocol; configure a homeserver + access token.", - }, - setup: matrixSetupAdapter, - setupWizard: matrixSetupWizard, - }, - }, - ]), - ); -} - -async function withClearedMatrixSetupEnv(run: () => Promise): Promise { - const previousEnv = { - MATRIX_HOMESERVER: process.env.MATRIX_HOMESERVER, - MATRIX_USER_ID: process.env.MATRIX_USER_ID, - MATRIX_ACCESS_TOKEN: process.env.MATRIX_ACCESS_TOKEN, - MATRIX_PASSWORD: process.env.MATRIX_PASSWORD, - MATRIX_DEVICE_ID: process.env.MATRIX_DEVICE_ID, - MATRIX_DEVICE_NAME: process.env.MATRIX_DEVICE_NAME, - }; - delete process.env.MATRIX_HOMESERVER; - delete process.env.MATRIX_USER_ID; - delete process.env.MATRIX_ACCESS_TOKEN; - delete process.env.MATRIX_PASSWORD; - delete process.env.MATRIX_DEVICE_ID; - delete process.env.MATRIX_DEVICE_NAME; - - try { - return await run(); - } finally { - for (const [key, value] of Object.entries(previousEnv)) { - if (value === undefined) { - delete process.env[key]; - } else { - process.env[key] = value; - } - } - } -} - -function createMatrixQuickstartPrompter(notes: string[]): WizardPrompter { - const select = vi.fn(async ({ message }: { message: string }) => { - if (message === "Select channel (QuickStart)") { - return "matrix"; - } - if (message === "Matrix auth method") { - return "token"; - } - throw new Error(`unexpected select prompt: ${message}`); - }); - const multiselect = vi.fn(async () => { - throw new Error("unexpected multiselect"); - }); - const text = vi.fn(async ({ message }: { message: string }) => { - if (message === "Matrix homeserver URL") { - return "https://matrix.example.org"; - } - if (message === "Matrix access token") { - return "matrix-token"; - } - if (message === "Matrix device name (optional)") { - return "OpenClaw Gateway"; - } - throw new Error(`unexpected text prompt: ${message}`); - }); - const confirm = vi.fn(async ({ message }: { message: string }) => { - if (message === "Enable end-to-end encryption (E2EE)?") { - return false; - } - if (message === "Configure Matrix rooms access?") { - return false; - } - if (message === "Configure DM access policies now? (default: pairing)") { - return false; - } - if (message === "Configure Matrix invite auto-join?") { - return false; - } - if (message.startsWith("Matrix env vars detected")) { - return false; - } - throw new Error(`unexpected confirm prompt: ${message}`); - }); - - return createPrompter({ - select: select as unknown as WizardPrompter["select"], - multiselect, - text: text as unknown as WizardPrompter["text"], - confirm: confirm as unknown as WizardPrompter["confirm"], - note: vi.fn(async (message: unknown) => { - notes.push(String(message)); - }), - }); -} - function setMinimalOnboardingRegistryForTests(): void { setActivePluginRegistry( createTestRegistry([ @@ -484,32 +369,6 @@ function createUnexpectedConfigureCall(message: string) { }); } -async function expectQuickstartPickerSkipsWithoutRuntime() { - const select = vi.fn(async ({ message }: { message: string }) => { - if (message === "Select channel (QuickStart)") { - return "__skip__"; - } - return "__done__"; - }); - const { multiselect, text } = createUnexpectedPromptGuards(); - const prompter = createPrompter({ - select: select as unknown as WizardPrompter["select"], - multiselect, - text, - }); - - await expect( - runSetupChannels({} as OpenClawConfig, prompter, { - quickstartDefaults: true, - }), - ).resolves.toEqual({} as OpenClawConfig); - - expect(select).toHaveBeenCalledWith( - expect.objectContaining({ message: "Select channel (QuickStart)" }), - ); - expect(multiselect).not.toHaveBeenCalled(); -} - async function runConfiguredTelegramSetup(params: { strictUnexpected?: boolean; configureWhenConfigured: NonNullable< @@ -650,67 +509,6 @@ describe("setupChannels", () => { vi.mocked(loadChannelSetupPluginRegistrySnapshotForChannel).mockClear(); vi.mocked(reloadChannelSetupPluginRegistry).mockClear(); }); - it("QuickStart uses single-select (no multiselect) and doesn't prompt for Telegram token when WhatsApp is chosen", async () => { - const select = vi.fn(async () => "whatsapp"); - const multiselect = vi.fn(async () => { - throw new Error("unexpected multiselect"); - }); - const text = vi.fn(async ({ message }: { message: string }) => { - if (message.includes("Enter Telegram bot token")) { - throw new Error("unexpected Telegram token prompt"); - } - if (message.includes("Your personal WhatsApp number")) { - return "+15555550123"; - } - throw new Error(`unexpected text prompt: ${message}`); - }); - - const prompter = createPrompter({ - select: select as unknown as WizardPrompter["select"], - multiselect, - text: text as unknown as WizardPrompter["text"], - }); - - await runSetupChannels({} as OpenClawConfig, prompter, { - quickstartDefaults: true, - forceAllowFromChannels: ["whatsapp"], - }); - - expect(select).toHaveBeenCalledWith( - expect.objectContaining({ message: "Select channel (QuickStart)" }), - ); - expect(multiselect).not.toHaveBeenCalled(); - }); - - it("renders the QuickStart channel picker without requiring the LINE runtime", async () => { - await expectQuickstartPickerSkipsWithoutRuntime(); - }); - - it("runs Matrix guided setup through setupChannels without falling back", async () => { - await withClearedMatrixSetupEnv(async () => { - await setMatrixOnboardingRegistryForTests(); - - const notes: string[] = []; - const prompter = createMatrixQuickstartPrompter(notes); - const cfg = await runSetupChannels({} as OpenClawConfig, prompter, { - quickstartDefaults: true, - }); - - expect(cfg.channels?.matrix).toMatchObject({ - enabled: true, - homeserver: "https://matrix.example.org", - accessToken: "matrix-token", - deviceName: "OpenClaw Gateway", - encryption: false, - }); - expect(notes.join("\n")).not.toContain("matrix does not support guided setup yet."); - }); - }); - - it("renders the QuickStart channel picker without requiring the Matrix runtime", async () => { - await expectQuickstartPickerSkipsWithoutRuntime(); - }); - it("continues Telegram setup when the plugin registry is empty", async () => { // Simulate missing registry entries (the scenario reported in #25545). setActivePluginRegistry(createEmptyPluginRegistry()); diff --git a/test/helpers/channels/command-contract.ts b/test/helpers/channels/command-contract.ts deleted file mode 100644 index db64bf4c202..00000000000 --- a/test/helpers/channels/command-contract.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { - loadBundledPluginApiSync, - loadBundledPluginContractApiSync, -} from "../../../src/test-utils/bundled-plugin-public-surface.js"; -import { createLazyObjectSurface } from "./lazy-object-surface.js"; - -type TelegramContractSurface = { - buildTelegramModelsProviderChannelData: (...args: unknown[]) => unknown; -}; -type WhatsAppApiSurface = { - isWhatsAppGroupJid: (...args: unknown[]) => boolean; - normalizeWhatsAppTarget: (...args: unknown[]) => string | null; - whatsappCommandPolicy: Record; -}; - -let telegramContractSurface: TelegramContractSurface | undefined; -let whatsappApiSurface: WhatsAppApiSurface | undefined; - -function getTelegramContractSurface(): TelegramContractSurface { - telegramContractSurface ??= loadBundledPluginContractApiSync("telegram"); - return telegramContractSurface; -} - -function getWhatsAppApiSurface(): WhatsAppApiSurface { - whatsappApiSurface ??= loadBundledPluginApiSync("whatsapp"); - return whatsappApiSurface; -} - -export const buildTelegramModelsProviderChannelData = ( - ...args: Parameters -) => getTelegramContractSurface().buildTelegramModelsProviderChannelData(...args); - -export const isWhatsAppGroupJid = (...args: Parameters) => - getWhatsAppApiSurface().isWhatsAppGroupJid(...args); - -export const normalizeWhatsAppTarget = ( - ...args: Parameters -) => getWhatsAppApiSurface().normalizeWhatsAppTarget(...args); - -export const whatsappCommandPolicy = createLazyObjectSurface( - () => getWhatsAppApiSurface().whatsappCommandPolicy, -); diff --git a/test/helpers/channels/matrix-setup-contract.ts b/test/helpers/channels/matrix-setup-contract.ts deleted file mode 100644 index 90cb63eca4b..00000000000 --- a/test/helpers/channels/matrix-setup-contract.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { loadBundledPluginContractApiSync } from "../../../src/test-utils/bundled-plugin-public-surface.js"; -import { createLazyObjectSurface } from "./lazy-object-surface.js"; - -type MatrixContractSurface = { - matrixSetupAdapter: Record; - matrixSetupWizard: Record; -}; - -let matrixContractSurface: MatrixContractSurface | undefined; - -function getMatrixContractSurface(): MatrixContractSurface { - matrixContractSurface ??= loadBundledPluginContractApiSync("matrix"); - return matrixContractSurface; -} - -export const matrixSetupAdapter = createLazyObjectSurface( - () => getMatrixContractSurface().matrixSetupAdapter, -); - -export const matrixSetupWizard = createLazyObjectSurface( - () => getMatrixContractSurface().matrixSetupWizard, -); diff --git a/test/helpers/channels/registry-actions.ts b/test/helpers/channels/registry-actions.ts deleted file mode 100644 index 356e945f00a..00000000000 --- a/test/helpers/channels/registry-actions.ts +++ /dev/null @@ -1,213 +0,0 @@ -import { requireBundledChannelPlugin } from "../../../src/channels/plugins/bundled.js"; -import type { ChannelPlugin } from "../../../src/channels/plugins/types.js"; -import type { OpenClawConfig } from "../../../src/config/config.js"; - -type ActionsContractEntry = { - id: string; - plugin: Pick; - unsupportedAction?: string; - cases: Array<{ - name: string; - cfg: OpenClawConfig; - expectedActions: string[]; - expectedCapabilities?: string[]; - beforeTest?: () => void; - }>; -}; - -let actionContractRegistryCache: ActionsContractEntry[] | undefined; - -export function getActionContractRegistry(): ActionsContractEntry[] { - actionContractRegistryCache ??= [ - { - id: "slack", - plugin: requireBundledChannelPlugin("slack"), - unsupportedAction: "poll", - cases: [ - { - name: "configured account exposes default Slack actions", - cfg: { - channels: { - slack: { - botToken: "xoxb-test", - appToken: "xapp-test", - }, - }, - } as OpenClawConfig, - expectedActions: [ - "send", - "react", - "reactions", - "read", - "edit", - "delete", - "download-file", - "upload-file", - "pin", - "unpin", - "list-pins", - "member-info", - "emoji-list", - ], - expectedCapabilities: ["blocks"], - }, - { - name: "interactive replies add the shared interactive capability", - cfg: { - channels: { - slack: { - botToken: "xoxb-test", - appToken: "xapp-test", - capabilities: { - interactiveReplies: true, - }, - }, - }, - } as OpenClawConfig, - expectedActions: [ - "send", - "react", - "reactions", - "read", - "edit", - "delete", - "download-file", - "upload-file", - "pin", - "unpin", - "list-pins", - "member-info", - "emoji-list", - ], - expectedCapabilities: ["blocks", "interactive"], - }, - { - name: "missing tokens disables the actions surface", - cfg: { - channels: { - slack: { - enabled: true, - }, - }, - } as OpenClawConfig, - expectedActions: [], - expectedCapabilities: [], - }, - ], - }, - { - id: "mattermost", - plugin: requireBundledChannelPlugin("mattermost"), - unsupportedAction: "poll", - cases: [ - { - name: "configured account exposes send and react", - cfg: { - channels: { - mattermost: { - enabled: true, - botToken: "test-token", - baseUrl: "https://chat.example.com", - }, - }, - } as OpenClawConfig, - expectedActions: ["send", "react"], - expectedCapabilities: ["buttons"], - }, - { - name: "reactions can be disabled while send stays available", - cfg: { - channels: { - mattermost: { - enabled: true, - botToken: "test-token", - baseUrl: "https://chat.example.com", - actions: { reactions: false }, - }, - }, - } as OpenClawConfig, - expectedActions: ["send"], - expectedCapabilities: ["buttons"], - }, - { - name: "missing bot credentials disables the actions surface", - cfg: { - channels: { - mattermost: { - enabled: true, - }, - }, - } as OpenClawConfig, - expectedActions: [], - expectedCapabilities: [], - }, - ], - }, - { - id: "telegram", - plugin: requireBundledChannelPlugin("telegram"), - cases: [ - { - name: "exposes configured Telegram actions and capabilities", - cfg: { - channels: { - telegram: { - botToken: "123:telegram-test-token", - }, - }, - } as OpenClawConfig, - expectedActions: [ - "send", - "poll", - "react", - "delete", - "edit", - "topic-create", - "topic-edit", - ], - expectedCapabilities: ["interactive", "buttons"], - }, - ], - }, - { - id: "discord", - plugin: requireBundledChannelPlugin("discord"), - cases: [ - { - name: "describes configured Discord actions and capabilities", - cfg: { - channels: { - discord: { - token: "Bot token-main", - actions: { - polls: true, - reactions: true, - permissions: false, - messages: false, - pins: false, - threads: false, - search: false, - stickers: false, - memberInfo: false, - roleInfo: false, - emojiUploads: false, - stickerUploads: false, - channelInfo: false, - channels: false, - voiceStatus: false, - events: false, - roles: false, - moderation: false, - presence: false, - }, - }, - }, - } as OpenClawConfig, - expectedActions: ["send", "poll", "react", "reactions", "emoji-list"], - expectedCapabilities: ["interactive", "components"], - }, - ], - }, - ]; - return actionContractRegistryCache; -} diff --git a/test/helpers/channels/registry-setup-status.ts b/test/helpers/channels/registry-setup-status.ts deleted file mode 100644 index ea40df998fd..00000000000 --- a/test/helpers/channels/registry-setup-status.ts +++ /dev/null @@ -1,227 +0,0 @@ -import { expect } from "vitest"; -import { requireBundledChannelPlugin } from "../../../src/channels/plugins/bundled.js"; -import type { ChannelPlugin } from "../../../src/channels/plugins/types.js"; -import type { OpenClawConfig } from "../../../src/config/config.js"; - -type SetupContractEntry = { - id: string; - plugin: Pick; - cases: Array<{ - name: string; - cfg: OpenClawConfig; - accountId?: string; - input: Record; - expectedAccountId?: string; - expectedValidation?: string | null; - beforeTest?: () => void; - assertPatchedConfig?: (cfg: OpenClawConfig) => void; - assertResolvedAccount?: (account: unknown, cfg: OpenClawConfig) => void; - }>; -}; - -type StatusContractEntry = { - id: string; - plugin: Pick; - cases: Array<{ - name: string; - cfg: OpenClawConfig; - accountId?: string; - runtime?: Record; - probe?: unknown; - beforeTest?: () => void; - assertSnapshot?: (snapshot: Record) => void; - assertSummary?: (summary: Record) => void; - }>; -}; - -let setupContractRegistryCache: SetupContractEntry[] | undefined; -let statusContractRegistryCache: StatusContractEntry[] | undefined; - -export function getSetupContractRegistry(): SetupContractEntry[] { - setupContractRegistryCache ??= [ - { - id: "slack", - plugin: requireBundledChannelPlugin("slack"), - cases: [ - { - name: "default account stores tokens and enables the channel", - cfg: {} as OpenClawConfig, - input: { - botToken: "xoxb-test", - appToken: "xapp-test", - }, - expectedAccountId: "default", - assertPatchedConfig: (cfg) => { - expect(cfg.channels?.slack?.enabled).toBe(true); - expect(cfg.channels?.slack?.botToken).toBe("xoxb-test"); - expect(cfg.channels?.slack?.appToken).toBe("xapp-test"); - }, - }, - { - name: "non-default env setup is rejected", - cfg: {} as OpenClawConfig, - accountId: "ops", - input: { - useEnv: true, - }, - expectedAccountId: "ops", - expectedValidation: "Slack env tokens can only be used for the default account.", - }, - ], - }, - { - id: "mattermost", - plugin: requireBundledChannelPlugin("mattermost"), - cases: [ - { - name: "default account stores token and normalized base URL", - cfg: {} as OpenClawConfig, - input: { - botToken: "test-token", - httpUrl: "https://chat.example.com/", - }, - expectedAccountId: "default", - assertPatchedConfig: (cfg) => { - expect(cfg.channels?.mattermost?.enabled).toBe(true); - expect(cfg.channels?.mattermost?.botToken).toBe("test-token"); - expect(cfg.channels?.mattermost?.baseUrl).toBe("https://chat.example.com"); - }, - }, - { - name: "missing credentials are rejected", - cfg: {} as OpenClawConfig, - input: { - httpUrl: "", - }, - expectedAccountId: "default", - expectedValidation: "Mattermost requires --bot-token and --http-url (or --use-env).", - }, - ], - }, - { - id: "line", - plugin: requireBundledChannelPlugin("line"), - cases: [ - { - name: "default account stores token and secret", - cfg: {} as OpenClawConfig, - input: { - channelAccessToken: "line-token", - channelSecret: "line-secret", - }, - expectedAccountId: "default", - assertPatchedConfig: (cfg) => { - expect(cfg.channels?.line?.enabled).toBe(true); - expect(cfg.channels?.line?.channelAccessToken).toBe("line-token"); - expect(cfg.channels?.line?.channelSecret).toBe("line-secret"); - }, - }, - { - name: "non-default env setup is rejected", - cfg: {} as OpenClawConfig, - accountId: "ops", - input: { - useEnv: true, - }, - expectedAccountId: "ops", - expectedValidation: "LINE_CHANNEL_ACCESS_TOKEN can only be used for the default account.", - }, - ], - }, - ]; - return setupContractRegistryCache; -} - -export function getStatusContractRegistry(): StatusContractEntry[] { - statusContractRegistryCache ??= [ - { - id: "slack", - plugin: requireBundledChannelPlugin("slack"), - cases: [ - { - name: "configured account produces a configured status snapshot", - cfg: { - channels: { - slack: { - botToken: "xoxb-test", - appToken: "xapp-test", - }, - }, - } as OpenClawConfig, - runtime: { - accountId: "default", - connected: true, - running: true, - }, - probe: { ok: true }, - assertSnapshot: (snapshot) => { - expect(snapshot.accountId).toBe("default"); - expect(snapshot.enabled).toBe(true); - expect(snapshot.configured).toBe(true); - }, - }, - ], - }, - { - id: "mattermost", - plugin: requireBundledChannelPlugin("mattermost"), - cases: [ - { - name: "configured account preserves connectivity details in the snapshot", - cfg: { - channels: { - mattermost: { - enabled: true, - botToken: "test-token", - baseUrl: "https://chat.example.com", - }, - }, - } as OpenClawConfig, - runtime: { - accountId: "default", - connected: true, - lastConnectedAt: 1234, - }, - probe: { ok: true }, - assertSnapshot: (snapshot) => { - expect(snapshot.accountId).toBe("default"); - expect(snapshot.enabled).toBe(true); - expect(snapshot.configured).toBe(true); - expect(snapshot.connected).toBe(true); - expect(snapshot.baseUrl).toBe("https://chat.example.com"); - }, - }, - ], - }, - { - id: "line", - plugin: requireBundledChannelPlugin("line"), - cases: [ - { - name: "configured account produces a webhook status snapshot", - cfg: { - channels: { - line: { - enabled: true, - channelAccessToken: "line-token", - channelSecret: "line-secret", - }, - }, - } as OpenClawConfig, - runtime: { - accountId: "default", - running: true, - }, - probe: { ok: true }, - assertSnapshot: (snapshot) => { - expect(snapshot.accountId).toBe("default"); - expect(snapshot.enabled).toBe(true); - expect(snapshot.configured).toBe(true); - expect(snapshot.mode).toBe("webhook"); - }, - }, - ], - }, - ]; - return statusContractRegistryCache; -} diff --git a/test/helpers/providers/anthropic-contract.ts b/test/helpers/providers/anthropic-contract.ts deleted file mode 100644 index ffd5380dec9..00000000000 --- a/test/helpers/providers/anthropic-contract.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { loadBundledPluginContractApiSync } from "../../../src/test-utils/bundled-plugin-public-surface.js"; - -type AnthropicContractSurface = { - createAnthropicBetaHeadersWrapper: (...args: unknown[]) => unknown; - createAnthropicFastModeWrapper: (...args: unknown[]) => unknown; - createAnthropicServiceTierWrapper: (...args: unknown[]) => unknown; - resolveAnthropicBetas: (...args: unknown[]) => unknown; - resolveAnthropicFastMode: (...args: unknown[]) => unknown; - resolveAnthropicServiceTier: (...args: unknown[]) => unknown; -}; - -let anthropicContractSurface: AnthropicContractSurface | undefined; - -function getAnthropicContractSurface(): AnthropicContractSurface { - anthropicContractSurface ??= - loadBundledPluginContractApiSync("anthropic"); - return anthropicContractSurface; -} - -export const createAnthropicBetaHeadersWrapper = ( - ...args: Parameters -) => getAnthropicContractSurface().createAnthropicBetaHeadersWrapper(...args); - -export const createAnthropicFastModeWrapper = ( - ...args: Parameters -) => getAnthropicContractSurface().createAnthropicFastModeWrapper(...args); - -export const createAnthropicServiceTierWrapper = ( - ...args: Parameters -) => getAnthropicContractSurface().createAnthropicServiceTierWrapper(...args); - -export const resolveAnthropicBetas = ( - ...args: Parameters -) => getAnthropicContractSurface().resolveAnthropicBetas(...args); - -export const resolveAnthropicFastMode = ( - ...args: Parameters -) => getAnthropicContractSurface().resolveAnthropicFastMode(...args); - -export const resolveAnthropicServiceTier = ( - ...args: Parameters -) => getAnthropicContractSurface().resolveAnthropicServiceTier(...args);