diff --git a/src/channels/plugins/contracts/registry-setup-status.ts b/src/channels/plugins/contracts/registry-setup-status.ts new file mode 100644 index 00000000000..d437718f21f --- /dev/null +++ b/src/channels/plugins/contracts/registry-setup-status.ts @@ -0,0 +1,227 @@ +import { expect } from "vitest"; +import type { OpenClawConfig } from "../../../config/config.js"; +import { requireBundledChannelPlugin } from "../bundled.js"; +import type { ChannelPlugin } from "../types.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/src/channels/plugins/contracts/registry.ts b/src/channels/plugins/contracts/registry.ts index aa679df78a0..fc212a47745 100644 --- a/src/channels/plugins/contracts/registry.ts +++ b/src/channels/plugins/contracts/registry.ts @@ -1,4 +1,4 @@ -import { expect, vi } from "vitest"; +import { vi } from "vitest"; import type { OpenClawConfig } from "../../../config/config.js"; import { listLineAccountIds, @@ -35,37 +35,6 @@ type ActionsContractEntry = { }>; }; -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; - }>; -}; - type SurfaceContractEntry = { id: string; plugin: Pick< @@ -125,8 +94,6 @@ vi.mock(buildBundledPluginModuleId("matrix", "runtime-api.js"), async () => { let pluginContractRegistryCache: PluginContractEntry[] | undefined; let actionContractRegistryCache: ActionsContractEntry[] | undefined; -let setupContractRegistryCache: SetupContractEntry[] | undefined; -let statusContractRegistryCache: StatusContractEntry[] | undefined; let surfaceContractRegistryCache: SurfaceContractEntry[] | undefined; let threadingContractRegistryCache: ThreadingContractEntry[] | undefined; let directoryContractRegistryCache: DirectoryContractEntry[] | undefined; @@ -334,195 +301,6 @@ export function getActionContractRegistry(): ActionsContractEntry[] { return actionContractRegistryCache; } -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; -} - export function getSurfaceContractRegistry(): SurfaceContractEntry[] { surfaceContractRegistryCache ??= listBundledChannelPlugins().map((plugin) => ({ id: plugin.id, diff --git a/src/channels/plugins/contracts/setup-status.registry-backed.contract.test.ts b/src/channels/plugins/contracts/setup-status.registry-backed.contract.test.ts index c37e47974ee..2fa55522a36 100644 --- a/src/channels/plugins/contracts/setup-status.registry-backed.contract.test.ts +++ b/src/channels/plugins/contracts/setup-status.registry-backed.contract.test.ts @@ -1,5 +1,5 @@ import { describe } from "vitest"; -import { getSetupContractRegistry, getStatusContractRegistry } from "./registry.js"; +import { getSetupContractRegistry, getStatusContractRegistry } from "./registry-setup-status.js"; import { installChannelSetupContractSuite, installChannelStatusContractSuite } from "./suites.js"; for (const entry of getSetupContractRegistry()) {