mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:10:44 +00:00
test(extensions): move registry channel contracts
This commit is contained in:
45
extensions/discord/src/channel-actions.contract.test.ts
Normal file
45
extensions/discord/src/channel-actions.contract.test.ts
Normal 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"],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
70
extensions/line/src/channel-setup-status.contract.test.ts
Normal file
70
extensions/line/src/channel-setup-status.contract.test.ts
Normal 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");
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
@@ -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");
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
24
extensions/telegram/src/channel-actions.contract.test.ts
Normal file
24
extensions/telegram/src/channel-actions.contract.test.ts
Normal 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"],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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());
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
@@ -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,
|
||||
);
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
Reference in New Issue
Block a user