test(extensions): move registry channel contracts

This commit is contained in:
Peter Steinberger
2026-04-20 20:53:26 +01:00
parent 9c9ca5f431
commit 958ca2ebec
14 changed files with 398 additions and 785 deletions

View File

@@ -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"],
},
],
});
});

View File

@@ -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");
},
},
],
});
});

View File

@@ -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");
},
},
],
});
});

View File

@@ -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);
},
},
],
});
});

View File

@@ -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"],
},
],
});
});

View File

@@ -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,
});
});
}

View File

@@ -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,
});
});
}

View File

@@ -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,
});
});
}

View File

@@ -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<void> {
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<T>(run: () => Promise<T>): Promise<T> {
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());

View File

@@ -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<string, unknown>;
};
let telegramContractSurface: TelegramContractSurface | undefined;
let whatsappApiSurface: WhatsAppApiSurface | undefined;
function getTelegramContractSurface(): TelegramContractSurface {
telegramContractSurface ??= loadBundledPluginContractApiSync<TelegramContractSurface>("telegram");
return telegramContractSurface;
}
function getWhatsAppApiSurface(): WhatsAppApiSurface {
whatsappApiSurface ??= loadBundledPluginApiSync<WhatsAppApiSurface>("whatsapp");
return whatsappApiSurface;
}
export const buildTelegramModelsProviderChannelData = (
...args: Parameters<TelegramContractSurface["buildTelegramModelsProviderChannelData"]>
) => getTelegramContractSurface().buildTelegramModelsProviderChannelData(...args);
export const isWhatsAppGroupJid = (...args: Parameters<WhatsAppApiSurface["isWhatsAppGroupJid"]>) =>
getWhatsAppApiSurface().isWhatsAppGroupJid(...args);
export const normalizeWhatsAppTarget = (
...args: Parameters<WhatsAppApiSurface["normalizeWhatsAppTarget"]>
) => getWhatsAppApiSurface().normalizeWhatsAppTarget(...args);
export const whatsappCommandPolicy = createLazyObjectSurface(
() => getWhatsAppApiSurface().whatsappCommandPolicy,
);

View File

@@ -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<string, unknown>;
matrixSetupWizard: Record<string, unknown>;
};
let matrixContractSurface: MatrixContractSurface | undefined;
function getMatrixContractSurface(): MatrixContractSurface {
matrixContractSurface ??= loadBundledPluginContractApiSync<MatrixContractSurface>("matrix");
return matrixContractSurface;
}
export const matrixSetupAdapter = createLazyObjectSurface(
() => getMatrixContractSurface().matrixSetupAdapter,
);
export const matrixSetupWizard = createLazyObjectSurface(
() => getMatrixContractSurface().matrixSetupWizard,
);

View File

@@ -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<ChannelPlugin, "id" | "actions">;
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;
}

View File

@@ -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<ChannelPlugin, "id" | "config" | "setup">;
cases: Array<{
name: string;
cfg: OpenClawConfig;
accountId?: string;
input: Record<string, unknown>;
expectedAccountId?: string;
expectedValidation?: string | null;
beforeTest?: () => void;
assertPatchedConfig?: (cfg: OpenClawConfig) => void;
assertResolvedAccount?: (account: unknown, cfg: OpenClawConfig) => void;
}>;
};
type StatusContractEntry = {
id: string;
plugin: Pick<ChannelPlugin, "id" | "config" | "status">;
cases: Array<{
name: string;
cfg: OpenClawConfig;
accountId?: string;
runtime?: Record<string, unknown>;
probe?: unknown;
beforeTest?: () => void;
assertSnapshot?: (snapshot: Record<string, unknown>) => void;
assertSummary?: (summary: Record<string, unknown>) => 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;
}

View File

@@ -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<AnthropicContractSurface>("anthropic");
return anthropicContractSurface;
}
export const createAnthropicBetaHeadersWrapper = (
...args: Parameters<AnthropicContractSurface["createAnthropicBetaHeadersWrapper"]>
) => getAnthropicContractSurface().createAnthropicBetaHeadersWrapper(...args);
export const createAnthropicFastModeWrapper = (
...args: Parameters<AnthropicContractSurface["createAnthropicFastModeWrapper"]>
) => getAnthropicContractSurface().createAnthropicFastModeWrapper(...args);
export const createAnthropicServiceTierWrapper = (
...args: Parameters<AnthropicContractSurface["createAnthropicServiceTierWrapper"]>
) => getAnthropicContractSurface().createAnthropicServiceTierWrapper(...args);
export const resolveAnthropicBetas = (
...args: Parameters<AnthropicContractSurface["resolveAnthropicBetas"]>
) => getAnthropicContractSurface().resolveAnthropicBetas(...args);
export const resolveAnthropicFastMode = (
...args: Parameters<AnthropicContractSurface["resolveAnthropicFastMode"]>
) => getAnthropicContractSurface().resolveAnthropicFastMode(...args);
export const resolveAnthropicServiceTier = (
...args: Parameters<AnthropicContractSurface["resolveAnthropicServiceTier"]>
) => getAnthropicContractSurface().resolveAnthropicServiceTier(...args);