From ca2d89bc4dbd517b3aea5ed3be1c16b567968497 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 20 Apr 2026 17:59:33 +0100 Subject: [PATCH] test(extensions): move channel contracts out of core --- .../discord/src/directory-contract.test.ts | 136 ++++++ .../src/inbound-context.contract.test.ts | 11 + .../message-handler.inbound-context.test.ts | 2 +- .../imessage/src/probe.contract.test.ts | 9 + extensions/line/src/probe.contract.test.ts | 9 + .../src/inbound-context.contract.test.ts | 10 +- .../event-handler.inbound-context.test.ts | 2 +- extensions/signal/src/probe.contract.test.ts | 9 + .../slack/src/directory-contract.test.ts | 106 ++++ .../src/inbound-context.contract.test.ts | 64 +++ .../monitor/message-handler/prepare.test.ts | 2 +- .../telegram/src/directory-contract.test.ts | 130 +++++ .../src/inbound-context.contract.test.ts | 44 ++ .../telegram/src/security-audit.test.ts | 12 +- .../test-support/inbound-context-contract.ts | 2 +- .../whatsapp/src/directory-contract.test.ts | 66 +++ .../src/inbound-context.contract.test.ts | 10 +- .../zalouser/src/channel.sendpayload.test.ts | 2 +- .../check-no-extension-test-core-imports.ts | 4 + scripts/lib/channel-contract-test-plan.mjs | 5 +- .../contracts/inbound.contract.test.ts | 28 -- ...ns-core-extension.discord.contract.test.ts | 3 - ...s-core-extension.imessage.contract.test.ts | 3 - ...ugins-core-extension.line.contract.test.ts | 3 - ...ins-core-extension.signal.contract.test.ts | 3 - ...gins-core-extension.slack.contract.test.ts | 3 - ...s-core-extension.telegram.contract.test.ts | 3 - ...s-core-extension.whatsapp.contract.test.ts | 3 - test/extension-test-boundary.test.ts | 11 + .../channels/inbound-contract.discord.ts | 28 -- .../channels/inbound-contract.slack.ts | 108 ----- .../channels/inbound-contract.telegram.ts | 63 --- .../plugins-core-extension-contract.ts | 452 ------------------ .../channel-contract-test-plan.test.ts | 5 - 34 files changed, 625 insertions(+), 726 deletions(-) create mode 100644 extensions/discord/src/directory-contract.test.ts create mode 100644 extensions/discord/src/inbound-context.contract.test.ts create mode 100644 extensions/imessage/src/probe.contract.test.ts create mode 100644 extensions/line/src/probe.contract.test.ts rename test/helpers/channels/inbound-contract.signal.ts => extensions/signal/src/inbound-context.contract.test.ts (71%) create mode 100644 extensions/signal/src/probe.contract.test.ts create mode 100644 extensions/slack/src/directory-contract.test.ts create mode 100644 extensions/slack/src/inbound-context.contract.test.ts create mode 100644 extensions/telegram/src/directory-contract.test.ts create mode 100644 extensions/telegram/src/inbound-context.contract.test.ts create mode 100644 extensions/whatsapp/src/directory-contract.test.ts rename test/helpers/channels/inbound-contract.whatsapp.ts => extensions/whatsapp/src/inbound-context.contract.test.ts (73%) delete mode 100644 src/channels/plugins/contracts/inbound.contract.test.ts delete mode 100644 src/channels/plugins/contracts/plugins-core-extension.discord.contract.test.ts delete mode 100644 src/channels/plugins/contracts/plugins-core-extension.imessage.contract.test.ts delete mode 100644 src/channels/plugins/contracts/plugins-core-extension.line.contract.test.ts delete mode 100644 src/channels/plugins/contracts/plugins-core-extension.signal.contract.test.ts delete mode 100644 src/channels/plugins/contracts/plugins-core-extension.slack.contract.test.ts delete mode 100644 src/channels/plugins/contracts/plugins-core-extension.telegram.contract.test.ts delete mode 100644 src/channels/plugins/contracts/plugins-core-extension.whatsapp.contract.test.ts delete mode 100644 test/helpers/channels/inbound-contract.discord.ts delete mode 100644 test/helpers/channels/inbound-contract.slack.ts delete mode 100644 test/helpers/channels/inbound-contract.telegram.ts delete mode 100644 test/helpers/channels/plugins-core-extension-contract.ts diff --git a/extensions/discord/src/directory-contract.test.ts b/extensions/discord/src/directory-contract.test.ts new file mode 100644 index 00000000000..b8a2d4f4b20 --- /dev/null +++ b/extensions/discord/src/directory-contract.test.ts @@ -0,0 +1,136 @@ +import type { + BaseProbeResult, + BaseTokenResolution, + ChannelDirectoryEntry, +} from "openclaw/plugin-sdk/channel-contract"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/testing"; +import { describe, expect, expectTypeOf, it } from "vitest"; +import { + listDiscordDirectoryGroupsFromConfig, + listDiscordDirectoryPeersFromConfig, +} from "../directory-contract-api.js"; +import type { DiscordProbe } from "./probe.js"; +import type { DiscordTokenResolution } from "./token.js"; + +type DirectoryListFn = (params: { + cfg: OpenClawConfig; + accountId?: string; + query?: string | null; + limit?: number | null; +}) => Promise; + +async function listDirectoryEntriesWithDefaults(listFn: DirectoryListFn, cfg: OpenClawConfig) { + return await listFn({ + cfg, + accountId: "default", + query: null, + limit: null, + }); +} + +async function expectDirectoryIds( + listFn: DirectoryListFn, + cfg: OpenClawConfig, + expected: string[], + options?: { sorted?: boolean }, +) { + const entries = await listDirectoryEntriesWithDefaults(listFn, cfg); + const ids = entries.map((entry) => entry.id); + expect(options?.sorted ? ids.toSorted((a, b) => a.localeCompare(b)) : ids).toEqual(expected); +} + +describe("Discord directory contract", () => { + it("keeps public probe and token resolution aligned with base contracts", () => { + expectTypeOf().toMatchTypeOf(); + expectTypeOf().toMatchTypeOf(); + }); + + it("lists peers/groups from config (numeric ids only)", async () => { + const cfg = { + channels: { + discord: { + token: "discord-test", + dm: { allowFrom: ["<@111>", "<@!333>", "nope"] }, + dms: { "222": {} }, + guilds: { + "123": { + users: ["<@12345>", " discord:444 ", "not-an-id"], + channels: { + "555": {}, + "<#777>": {}, + "channel:666": {}, + general: {}, + }, + }, + }, + }, + }, + } as unknown as OpenClawConfig; + + await expectDirectoryIds( + listDiscordDirectoryPeersFromConfig, + cfg, + ["user:111", "user:12345", "user:222", "user:333", "user:444"], + { sorted: true }, + ); + await expectDirectoryIds( + listDiscordDirectoryGroupsFromConfig, + cfg, + ["channel:555", "channel:666", "channel:777"], + { sorted: true }, + ); + }); + + it("keeps directories readable when tokens are unresolved SecretRefs", async () => { + const envSecret = { + source: "env", + provider: "default", + id: "MISSING_TEST_SECRET", + } as const; + const cfg = { + channels: { + discord: { + token: envSecret, + dm: { allowFrom: ["<@111>"] }, + guilds: { + "123": { + channels: { + "555": {}, + }, + }, + }, + }, + }, + } as unknown as OpenClawConfig; + + await expectDirectoryIds(listDiscordDirectoryPeersFromConfig, cfg, ["user:111"]); + await expectDirectoryIds(listDiscordDirectoryGroupsFromConfig, cfg, ["channel:555"]); + }); + + it("applies query and limit filtering for config-backed directories", async () => { + const cfg = { + channels: { + discord: { + token: "discord-test", + guilds: { + "123": { + channels: { + "555": {}, + "666": {}, + "777": {}, + }, + }, + }, + }, + }, + } as unknown as OpenClawConfig; + + const groups = await listDiscordDirectoryGroupsFromConfig({ + cfg, + accountId: "default", + query: "666", + limit: 5, + }); + expect(groups.map((entry) => entry.id)).toEqual(["channel:666"]); + }); +}); diff --git a/extensions/discord/src/inbound-context.contract.test.ts b/extensions/discord/src/inbound-context.contract.test.ts new file mode 100644 index 00000000000..de31ee5b943 --- /dev/null +++ b/extensions/discord/src/inbound-context.contract.test.ts @@ -0,0 +1,11 @@ +import { expectChannelInboundContextContract } from "openclaw/plugin-sdk/testing"; +import { describe, it } from "vitest"; +import { buildFinalizedDiscordDirectInboundContext } from "./monitor/inbound-context.test-helpers.js"; + +describe("Discord inbound context contract", () => { + it("keeps inbound context finalized", () => { + const ctx = buildFinalizedDiscordDirectInboundContext(); + + expectChannelInboundContextContract(ctx); + }); +}); diff --git a/extensions/discord/src/monitor/message-handler.inbound-context.test.ts b/extensions/discord/src/monitor/message-handler.inbound-context.test.ts index 423e3d635b1..71cb9ecb0f9 100644 --- a/extensions/discord/src/monitor/message-handler.inbound-context.test.ts +++ b/extensions/discord/src/monitor/message-handler.inbound-context.test.ts @@ -1,6 +1,6 @@ import { finalizeInboundContext } from "openclaw/plugin-sdk/reply-dispatch-runtime"; +import { expectChannelInboundContextContract as expectInboundContextContract } from "openclaw/plugin-sdk/testing"; import { describe, expect, it } from "vitest"; -import { expectChannelInboundContextContract as expectInboundContextContract } from "../../../../src/channels/plugins/contracts/test-helpers.js"; import { buildDiscordInboundAccessContext } from "./inbound-context.js"; import { buildFinalizedDiscordDirectInboundContext } from "./inbound-context.test-helpers.js"; diff --git a/extensions/imessage/src/probe.contract.test.ts b/extensions/imessage/src/probe.contract.test.ts new file mode 100644 index 00000000000..c943406fb2f --- /dev/null +++ b/extensions/imessage/src/probe.contract.test.ts @@ -0,0 +1,9 @@ +import type { BaseProbeResult } from "openclaw/plugin-sdk/channel-contract"; +import { describe, expectTypeOf, it } from "vitest"; +import type { IMessageProbe } from "./probe.js"; + +describe("iMessage probe contract", () => { + it("keeps public probe aligned with base contract", () => { + expectTypeOf().toMatchTypeOf(); + }); +}); diff --git a/extensions/line/src/probe.contract.test.ts b/extensions/line/src/probe.contract.test.ts new file mode 100644 index 00000000000..d78a137ff43 --- /dev/null +++ b/extensions/line/src/probe.contract.test.ts @@ -0,0 +1,9 @@ +import type { BaseProbeResult } from "openclaw/plugin-sdk/channel-contract"; +import { describe, expectTypeOf, it } from "vitest"; +import type { LineProbeResult } from "./types.js"; + +describe("LINE probe contract", () => { + it("keeps public probe aligned with base contract", () => { + expectTypeOf().toMatchTypeOf(); + }); +}); diff --git a/test/helpers/channels/inbound-contract.signal.ts b/extensions/signal/src/inbound-context.contract.test.ts similarity index 71% rename from test/helpers/channels/inbound-contract.signal.ts rename to extensions/signal/src/inbound-context.contract.test.ts index ff7730190c6..d7071609778 100644 --- a/test/helpers/channels/inbound-contract.signal.ts +++ b/extensions/signal/src/inbound-context.contract.test.ts @@ -1,8 +1,8 @@ -import { it } from "vitest"; -import { finalizeInboundContext } from "../../../src/auto-reply/reply/inbound-context.js"; -import { expectChannelInboundContextContract } from "../../../src/channels/plugins/contracts/test-helpers.js"; +import { finalizeInboundContext } from "openclaw/plugin-sdk/reply-runtime"; +import { expectChannelInboundContextContract } from "openclaw/plugin-sdk/testing"; +import { describe, it } from "vitest"; -export function installSignalInboundContractSuite() { +describe("Signal inbound context contract", () => { it("keeps inbound context finalized", () => { const ctx = finalizeInboundContext({ Body: "Alice: hi", @@ -29,4 +29,4 @@ export function installSignalInboundContractSuite() { expectChannelInboundContextContract(ctx); }); -} +}); diff --git a/extensions/signal/src/monitor/event-handler.inbound-context.test.ts b/extensions/signal/src/monitor/event-handler.inbound-context.test.ts index 4914d8efcf1..8e3c9192eee 100644 --- a/extensions/signal/src/monitor/event-handler.inbound-context.test.ts +++ b/extensions/signal/src/monitor/event-handler.inbound-context.test.ts @@ -1,6 +1,6 @@ import type { MsgContext } from "openclaw/plugin-sdk/reply-runtime"; +import { expectChannelInboundContextContract as expectInboundContextContract } from "openclaw/plugin-sdk/testing"; import { beforeEach, describe, expect, it, vi } from "vitest"; -import { expectChannelInboundContextContract as expectInboundContextContract } from "../../../../src/channels/plugins/contracts/test-helpers.js"; vi.useRealTimers(); const [ { createBaseSignalEventHandlerDeps, createSignalReceiveEvent }, diff --git a/extensions/signal/src/probe.contract.test.ts b/extensions/signal/src/probe.contract.test.ts new file mode 100644 index 00000000000..65e6886d9f8 --- /dev/null +++ b/extensions/signal/src/probe.contract.test.ts @@ -0,0 +1,9 @@ +import type { BaseProbeResult } from "openclaw/plugin-sdk/channel-contract"; +import { describe, expectTypeOf, it } from "vitest"; +import type { SignalProbe } from "./probe.js"; + +describe("Signal probe contract", () => { + it("keeps public probe aligned with base contract", () => { + expectTypeOf().toMatchTypeOf(); + }); +}); diff --git a/extensions/slack/src/directory-contract.test.ts b/extensions/slack/src/directory-contract.test.ts new file mode 100644 index 00000000000..f3e0021301e --- /dev/null +++ b/extensions/slack/src/directory-contract.test.ts @@ -0,0 +1,106 @@ +import type { BaseProbeResult, ChannelDirectoryEntry } from "openclaw/plugin-sdk/channel-contract"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/testing"; +import { describe, expect, expectTypeOf, it } from "vitest"; +import { + listSlackDirectoryGroupsFromConfig, + listSlackDirectoryPeersFromConfig, +} from "../directory-contract-api.js"; +import type { SlackProbe } from "./probe.js"; + +type DirectoryListFn = (params: { + cfg: OpenClawConfig; + accountId?: string; + query?: string | null; + limit?: number | null; +}) => Promise; + +async function listDirectoryEntriesWithDefaults(listFn: DirectoryListFn, cfg: OpenClawConfig) { + return await listFn({ + cfg, + accountId: "default", + query: null, + limit: null, + }); +} + +async function expectDirectoryIds( + listFn: DirectoryListFn, + cfg: OpenClawConfig, + expected: string[], + options?: { sorted?: boolean }, +) { + const entries = await listDirectoryEntriesWithDefaults(listFn, cfg); + const ids = entries.map((entry) => entry.id); + expect(options?.sorted ? ids.toSorted((a, b) => a.localeCompare(b)) : ids).toEqual(expected); +} + +describe("Slack directory contract", () => { + it("keeps public probe aligned with base contract", () => { + expectTypeOf().toMatchTypeOf(); + }); + + it("lists peers/groups from config", async () => { + const cfg = { + channels: { + slack: { + botToken: "xoxb-test", + appToken: "xapp-test", + dm: { allowFrom: ["U123", "user:U999"] }, + dms: { U234: {} }, + channels: { C111: { users: ["U777"] } }, + }, + }, + } as unknown as OpenClawConfig; + + await expectDirectoryIds( + listSlackDirectoryPeersFromConfig, + cfg, + ["user:u123", "user:u234", "user:u777", "user:u999"], + { sorted: true }, + ); + await expectDirectoryIds(listSlackDirectoryGroupsFromConfig, cfg, ["channel:c111"]); + }); + + it("keeps directories readable when tokens are unresolved SecretRefs", async () => { + const envSecret = { + source: "env", + provider: "default", + id: "MISSING_TEST_SECRET", + } as const; + const cfg = { + channels: { + slack: { + botToken: envSecret, + appToken: envSecret, + dm: { allowFrom: ["U123"] }, + channels: { C111: {} }, + }, + }, + } as unknown as OpenClawConfig; + + await expectDirectoryIds(listSlackDirectoryPeersFromConfig, cfg, ["user:u123"]); + await expectDirectoryIds(listSlackDirectoryGroupsFromConfig, cfg, ["channel:c111"]); + }); + + it("applies query and limit filtering for config-backed directories", async () => { + const cfg = { + channels: { + slack: { + botToken: "xoxb-test", + appToken: "xapp-test", + dm: { allowFrom: ["U100", "U200"] }, + dms: { U300: {} }, + }, + }, + } as unknown as OpenClawConfig; + + const peers = await listSlackDirectoryPeersFromConfig({ + cfg, + accountId: "default", + query: "user:u", + limit: 2, + }); + expect(peers).toHaveLength(2); + expect(peers.every((entry) => entry.id.startsWith("user:u"))).toBe(true); + }); +}); diff --git a/extensions/slack/src/inbound-context.contract.test.ts b/extensions/slack/src/inbound-context.contract.test.ts new file mode 100644 index 00000000000..e2df3fe2fd3 --- /dev/null +++ b/extensions/slack/src/inbound-context.contract.test.ts @@ -0,0 +1,64 @@ +import { + createTempHomeEnv, + expectChannelInboundContextContract, + type OpenClawConfig, +} from "openclaw/plugin-sdk/testing"; +import { describe, expect, it } from "vitest"; +import { + createInboundSlackTestContext, + prepareSlackMessage, +} from "../inbound-contract-test-api.js"; +import type { ResolvedSlackAccount } from "./accounts.js"; +import type { SlackMessageEvent } from "./types.js"; + +function createSlackAccount(config: ResolvedSlackAccount["config"] = {}): ResolvedSlackAccount { + return { + accountId: "default", + enabled: true, + botTokenSource: "config", + appTokenSource: "config", + userTokenSource: "none", + config, + replyToMode: config.replyToMode, + replyToModeByChatType: config.replyToModeByChatType, + dm: config.dm, + } as ResolvedSlackAccount; +} + +function createSlackMessage(overrides: Partial): SlackMessageEvent { + return { + type: "message", + channel: "D123", + channel_type: "im", + user: "U1", + text: "hi", + ts: "1.000", + ...overrides, + }; +} + +describe("Slack inbound context contract", () => { + it("keeps inbound context finalized", async () => { + const tempHome = await createTempHomeEnv("openclaw-slack-inbound-contract-"); + try { + const ctx = createInboundSlackTestContext({ + cfg: { + channels: { slack: { enabled: true } }, + } as OpenClawConfig, + }); + ctx.resolveUserName = async () => ({ name: "Alice" }) as never; + + const prepared = await prepareSlackMessage({ + ctx, + account: createSlackAccount(), + message: createSlackMessage({}), + opts: { source: "message" }, + }); + + expect(prepared).toBeTruthy(); + expectChannelInboundContextContract(prepared!.ctxPayload); + } finally { + await tempHome.restore(); + } + }); +}); diff --git a/extensions/slack/src/monitor/message-handler/prepare.test.ts b/extensions/slack/src/monitor/message-handler/prepare.test.ts index 56f60f38060..e096022cbff 100644 --- a/extensions/slack/src/monitor/message-handler/prepare.test.ts +++ b/extensions/slack/src/monitor/message-handler/prepare.test.ts @@ -3,8 +3,8 @@ import type { App } from "@slack/bolt"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime"; import { resolveAgentRoute } from "openclaw/plugin-sdk/routing"; import { resolveThreadSessionKeys } from "openclaw/plugin-sdk/routing"; +import { expectChannelInboundContextContract as expectInboundContextContract } from "openclaw/plugin-sdk/testing"; import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; -import { expectChannelInboundContextContract as expectInboundContextContract } from "../../../../../src/channels/plugins/contracts/test-helpers.js"; import type { ResolvedSlackAccount } from "../../accounts.js"; import type { SlackMessageEvent } from "../../types.js"; import type { SlackMonitorContext } from "../context.js"; diff --git a/extensions/telegram/src/directory-contract.test.ts b/extensions/telegram/src/directory-contract.test.ts new file mode 100644 index 00000000000..2519a3eec11 --- /dev/null +++ b/extensions/telegram/src/directory-contract.test.ts @@ -0,0 +1,130 @@ +import type { + BaseProbeResult, + BaseTokenResolution, + ChannelDirectoryEntry, +} from "openclaw/plugin-sdk/channel-contract"; +import { type OpenClawConfig, withEnvAsync } from "openclaw/plugin-sdk/testing"; +import { describe, expect, expectTypeOf, it } from "vitest"; +import { + listTelegramDirectoryGroupsFromConfig, + listTelegramDirectoryPeersFromConfig, +} from "../directory-contract-api.js"; +import type { TelegramProbe } from "./probe.js"; +import type { TelegramTokenResolution } from "./token.js"; + +type DirectoryListFn = (params: { + cfg: OpenClawConfig; + accountId?: string; + query?: string | null; + limit?: number | null; +}) => Promise; + +async function listDirectoryEntriesWithDefaults(listFn: DirectoryListFn, cfg: OpenClawConfig) { + return await listFn({ + cfg, + accountId: "default", + query: null, + limit: null, + }); +} + +async function expectDirectoryIds( + listFn: DirectoryListFn, + cfg: OpenClawConfig, + expected: string[], + options?: { sorted?: boolean }, +) { + const entries = await listDirectoryEntriesWithDefaults(listFn, cfg); + const ids = entries.map((entry) => entry.id); + expect(options?.sorted ? ids.toSorted((a, b) => a.localeCompare(b)) : ids).toEqual(expected); +} + +describe("Telegram directory contract", () => { + it("keeps public probe and token resolution aligned with base contracts", () => { + expectTypeOf().toMatchTypeOf(); + expectTypeOf().toMatchTypeOf(); + }); + + it("lists peers/groups from config", async () => { + const cfg = { + channels: { + telegram: { + botToken: "telegram-test", + allowFrom: ["123", "alice", "tg:@bob"], + dms: { "456": {} }, + groups: { "-1001": {}, "*": {} }, + }, + }, + } as unknown as OpenClawConfig; + + await expectDirectoryIds( + listTelegramDirectoryPeersFromConfig, + cfg, + ["123", "456", "@alice", "@bob"], + { sorted: true }, + ); + await expectDirectoryIds(listTelegramDirectoryGroupsFromConfig, cfg, ["-1001"]); + }); + + it("keeps fallback semantics when accountId is omitted", async () => { + await withEnvAsync({ TELEGRAM_BOT_TOKEN: "tok-env" }, async () => { + const cfg = { + channels: { + telegram: { + allowFrom: ["alice"], + groups: { "-1001": {} }, + accounts: { + work: { + botToken: "tok-work", + allowFrom: ["bob"], + groups: { "-2002": {} }, + }, + }, + }, + }, + } as unknown as OpenClawConfig; + + await expectDirectoryIds(listTelegramDirectoryPeersFromConfig, cfg, ["@alice"]); + await expectDirectoryIds(listTelegramDirectoryGroupsFromConfig, cfg, ["-1001"]); + }); + }); + + it("keeps directories readable when tokens are unresolved SecretRefs", async () => { + const envSecret = { + source: "env", + provider: "default", + id: "MISSING_TEST_SECRET", + } as const; + const cfg = { + channels: { + telegram: { + botToken: envSecret, + allowFrom: ["alice"], + groups: { "-1001": {} }, + }, + }, + } as unknown as OpenClawConfig; + + await expectDirectoryIds(listTelegramDirectoryPeersFromConfig, cfg, ["@alice"]); + await expectDirectoryIds(listTelegramDirectoryGroupsFromConfig, cfg, ["-1001"]); + }); + + it("applies query and limit filtering for config-backed directories", async () => { + const cfg = { + channels: { + telegram: { + botToken: "telegram-test", + groups: { "-1001": {}, "-1002": {}, "-2001": {} }, + }, + }, + } as unknown as OpenClawConfig; + + const groups = await listTelegramDirectoryGroupsFromConfig({ + cfg, + accountId: "default", + query: "-100", + limit: 1, + }); + expect(groups.map((entry) => entry.id)).toEqual(["-1001"]); + }); +}); diff --git a/extensions/telegram/src/inbound-context.contract.test.ts b/extensions/telegram/src/inbound-context.contract.test.ts new file mode 100644 index 00000000000..bbc05b72430 --- /dev/null +++ b/extensions/telegram/src/inbound-context.contract.test.ts @@ -0,0 +1,44 @@ +import { + expectChannelInboundContextContract, + type OpenClawConfig, +} from "openclaw/plugin-sdk/testing"; +import { describe, it } from "vitest"; +import { buildTelegramMessageContextForTest } from "./bot-message-context.test-harness.js"; + +describe("Telegram inbound context contract", () => { + it("keeps inbound context finalized", async () => { + const context = await buildTelegramMessageContextForTest({ + cfg: { + agents: { + defaults: { + envelopeTimezone: "utc", + }, + }, + channels: { + telegram: { + groupPolicy: "open", + groups: { "*": { requireMention: false } }, + }, + }, + } satisfies OpenClawConfig, + message: { + chat: { id: 42, type: "group", title: "Ops" }, + text: "hello", + date: 1_736_380_800, + message_id: 2, + from: { + id: 99, + first_name: "Ada", + last_name: "Lovelace", + username: "ada", + }, + }, + }); + + const payload = context?.ctxPayload; + if (!payload) { + throw new Error("expected telegram inbound payload"); + } + expectChannelInboundContextContract(payload); + }); +}); diff --git a/extensions/telegram/src/security-audit.test.ts b/extensions/telegram/src/security-audit.test.ts index 033b5facd1b..f389d1f4b57 100644 --- a/extensions/telegram/src/security-audit.test.ts +++ b/extensions/telegram/src/security-audit.test.ts @@ -23,6 +23,14 @@ function createTelegramAccount( }; } +function getTelegramConfig(cfg: OpenClawConfig) { + const config = cfg.channels?.telegram; + if (!config) { + throw new Error("expected telegram config"); + } + return config; +} + describe("Telegram security audit findings", () => { it("flags group commands without a sender allowlist", async () => { const cfg: OpenClawConfig = { @@ -39,7 +47,7 @@ describe("Telegram security audit findings", () => { readChannelAllowFromStoreMock.mockResolvedValue([]); const findings = await collectTelegramSecurityAuditFindings({ cfg, - account: createTelegramAccount(cfg.channels!.telegram), + account: createTelegramAccount(getTelegramConfig(cfg)), accountId: "default", }); @@ -69,7 +77,7 @@ describe("Telegram security audit findings", () => { readChannelAllowFromStoreMock.mockResolvedValue([]); const findings = await collectTelegramSecurityAuditFindings({ cfg, - account: createTelegramAccount(cfg.channels!.telegram), + account: createTelegramAccount(getTelegramConfig(cfg)), accountId: "default", }); diff --git a/extensions/telegram/src/test-support/inbound-context-contract.ts b/extensions/telegram/src/test-support/inbound-context-contract.ts index 0b119dfbbf3..124067f12ca 100644 --- a/extensions/telegram/src/test-support/inbound-context-contract.ts +++ b/extensions/telegram/src/test-support/inbound-context-contract.ts @@ -1 +1 @@ -export { expectChannelInboundContextContract } from "../../../../src/channels/plugins/contracts/test-helpers.js"; +export { expectChannelInboundContextContract } from "openclaw/plugin-sdk/testing"; diff --git a/extensions/whatsapp/src/directory-contract.test.ts b/extensions/whatsapp/src/directory-contract.test.ts new file mode 100644 index 00000000000..26af1ad1bc5 --- /dev/null +++ b/extensions/whatsapp/src/directory-contract.test.ts @@ -0,0 +1,66 @@ +import type { ChannelDirectoryEntry } from "openclaw/plugin-sdk/channel-contract"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/testing"; +import { describe, expect, it } from "vitest"; +import { + listWhatsAppDirectoryGroupsFromConfig, + listWhatsAppDirectoryPeersFromConfig, +} from "../directory-contract-api.js"; + +type DirectoryListFn = (params: { + cfg: OpenClawConfig; + accountId?: string; + query?: string | null; + limit?: number | null; +}) => Promise; + +async function listDirectoryEntriesWithDefaults(listFn: DirectoryListFn, cfg: OpenClawConfig) { + return await listFn({ + cfg, + accountId: "default", + query: null, + limit: null, + }); +} + +async function expectDirectoryIds( + listFn: DirectoryListFn, + cfg: OpenClawConfig, + expected: string[], +) { + const entries = await listDirectoryEntriesWithDefaults(listFn, cfg); + expect(entries.map((entry) => entry.id)).toEqual(expected); +} + +describe("WhatsApp directory contract", () => { + it("lists peers/groups from config", async () => { + const cfg = { + channels: { + whatsapp: { + allowFrom: ["+15550000000", "*", "123@g.us"], + groups: { "999@g.us": { requireMention: true }, "*": {} }, + }, + }, + } as unknown as OpenClawConfig; + + await expectDirectoryIds(listWhatsAppDirectoryPeersFromConfig, cfg, ["+15550000000"]); + await expectDirectoryIds(listWhatsAppDirectoryGroupsFromConfig, cfg, ["999@g.us"]); + }); + + it("applies query and limit filtering for config-backed directories", async () => { + const cfg = { + channels: { + whatsapp: { + groups: { "111@g.us": {}, "222@g.us": {}, "333@s.whatsapp.net": {} }, + }, + }, + } as unknown as OpenClawConfig; + + const groups = await listWhatsAppDirectoryGroupsFromConfig({ + cfg, + accountId: "default", + query: "@g.us", + limit: 1, + }); + expect(groups.map((entry) => entry.id)).toEqual(["111@g.us"]); + }); +}); diff --git a/test/helpers/channels/inbound-contract.whatsapp.ts b/extensions/whatsapp/src/inbound-context.contract.test.ts similarity index 73% rename from test/helpers/channels/inbound-contract.whatsapp.ts rename to extensions/whatsapp/src/inbound-context.contract.test.ts index b6eebd85cfd..dbc4ca1aedb 100644 --- a/test/helpers/channels/inbound-contract.whatsapp.ts +++ b/extensions/whatsapp/src/inbound-context.contract.test.ts @@ -1,8 +1,8 @@ -import { it } from "vitest"; -import { finalizeInboundContext } from "../../../src/auto-reply/reply/inbound-context.js"; -import { expectChannelInboundContextContract } from "../../../src/channels/plugins/contracts/test-helpers.js"; +import { finalizeInboundContext } from "openclaw/plugin-sdk/reply-runtime"; +import { expectChannelInboundContextContract } from "openclaw/plugin-sdk/testing"; +import { describe, it } from "vitest"; -export function installWhatsAppInboundContractSuite() { +describe("WhatsApp inbound context contract", () => { it("keeps inbound context finalized", () => { const ctx = finalizeInboundContext({ Body: "Alice: hi", @@ -30,4 +30,4 @@ export function installWhatsAppInboundContractSuite() { expectChannelInboundContextContract(ctx); }); -} +}); diff --git a/extensions/zalouser/src/channel.sendpayload.test.ts b/extensions/zalouser/src/channel.sendpayload.test.ts index 1d80eb007f9..d39bc5b547d 100644 --- a/extensions/zalouser/src/channel.sendpayload.test.ts +++ b/extensions/zalouser/src/channel.sendpayload.test.ts @@ -1,5 +1,5 @@ +import { primeChannelOutboundSendMock } from "openclaw/plugin-sdk/testing"; import { beforeEach, describe, expect, it, vi } from "vitest"; -import { primeChannelOutboundSendMock } from "../../../src/channels/plugins/contracts/test-helpers.js"; import "./accounts.test-mocks.js"; import "./zalo-js.test-mocks.js"; import type { ReplyPayload } from "../runtime-api.js"; diff --git a/scripts/check-no-extension-test-core-imports.ts b/scripts/check-no-extension-test-core-imports.ts index 1556083f65a..afaed36c517 100644 --- a/scripts/check-no-extension-test-core-imports.ts +++ b/scripts/check-no-extension-test-core-imports.ts @@ -27,6 +27,10 @@ const FORBIDDEN_PATTERNS: Array<{ pattern: RegExp; hint: string }> = [ pattern: /["'](?:\.\.\/)+(?:src\/plugins\/types\.js)["']/, hint: "Use public plugin-sdk/core types or test/helpers/plugins/* instead.", }, + { + pattern: /["'](?:\.\.\/)+(?:src\/channels\/plugins\/contracts\/test-helpers\.js)["']/, + hint: "Use openclaw/plugin-sdk/testing for channel contract test helpers.", + }, ]; function isExtensionTestFile(filePath: string): boolean { diff --git a/scripts/lib/channel-contract-test-plan.mjs b/scripts/lib/channel-contract-test-plan.mjs index 1f191d27d65..55a47385c89 100644 --- a/scripts/lib/channel-contract-test-plan.mjs +++ b/scripts/lib/channel-contract-test-plan.mjs @@ -19,7 +19,6 @@ export function createChannelContractTestShards() { "checks-fast-contracts-channels-registry-b": [], "checks-fast-contracts-channels-core-a": [], "checks-fast-contracts-channels-core-b": [], - "checks-fast-contracts-channels-extensions": [], }; const pushBalanced = (firstKey, secondKey, file) => { const target = groups[firstKey].length <= groups[secondKey].length ? firstKey : secondKey; @@ -28,9 +27,7 @@ export function createChannelContractTestShards() { for (const file of listContractTestFiles(rootDir)) { const name = relative(rootDir, file).replaceAll("\\", "/"); - if (name.startsWith("plugins-core-extension.")) { - groups["checks-fast-contracts-channels-extensions"].push(file); - } else if (name.startsWith("plugins-core.") || name.startsWith("plugin.")) { + if (name.startsWith("plugins-core.") || name.startsWith("plugin.")) { pushBalanced( "checks-fast-contracts-channels-core-a", "checks-fast-contracts-channels-core-b", diff --git a/src/channels/plugins/contracts/inbound.contract.test.ts b/src/channels/plugins/contracts/inbound.contract.test.ts deleted file mode 100644 index aa06edf2570..00000000000 --- a/src/channels/plugins/contracts/inbound.contract.test.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { describe } from "vitest"; -import { installDiscordInboundContractSuite } from "../../../../test/helpers/channels/inbound-contract.discord.js"; -import { installSignalInboundContractSuite } from "../../../../test/helpers/channels/inbound-contract.signal.js"; -import { installSlackInboundContractSuite } from "../../../../test/helpers/channels/inbound-contract.slack.js"; -import { installTelegramInboundContractSuite } from "../../../../test/helpers/channels/inbound-contract.telegram.js"; -import { installWhatsAppInboundContractSuite } from "../../../../test/helpers/channels/inbound-contract.whatsapp.js"; - -describe("inbound channel contracts", () => { - describe("discord", () => { - installDiscordInboundContractSuite(); - }); - - describe("signal", () => { - installSignalInboundContractSuite(); - }); - - describe("slack", () => { - installSlackInboundContractSuite(); - }); - - describe("telegram", () => { - installTelegramInboundContractSuite(); - }); - - describe("whatsapp", () => { - installWhatsAppInboundContractSuite(); - }); -}); diff --git a/src/channels/plugins/contracts/plugins-core-extension.discord.contract.test.ts b/src/channels/plugins/contracts/plugins-core-extension.discord.contract.test.ts deleted file mode 100644 index fdb67a36fe5..00000000000 --- a/src/channels/plugins/contracts/plugins-core-extension.discord.contract.test.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { describeDiscordPluginsCoreExtensionContract } from "../../../../test/helpers/channels/plugins-core-extension-contract.js"; - -describeDiscordPluginsCoreExtensionContract(); diff --git a/src/channels/plugins/contracts/plugins-core-extension.imessage.contract.test.ts b/src/channels/plugins/contracts/plugins-core-extension.imessage.contract.test.ts deleted file mode 100644 index e31249967ed..00000000000 --- a/src/channels/plugins/contracts/plugins-core-extension.imessage.contract.test.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { describeIMessagePluginsCoreExtensionContract } from "../../../../test/helpers/channels/plugins-core-extension-contract.js"; - -describeIMessagePluginsCoreExtensionContract(); diff --git a/src/channels/plugins/contracts/plugins-core-extension.line.contract.test.ts b/src/channels/plugins/contracts/plugins-core-extension.line.contract.test.ts deleted file mode 100644 index 0f06b6505bf..00000000000 --- a/src/channels/plugins/contracts/plugins-core-extension.line.contract.test.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { describeLinePluginsCoreExtensionContract } from "../../../../test/helpers/channels/plugins-core-extension-contract.js"; - -describeLinePluginsCoreExtensionContract(); diff --git a/src/channels/plugins/contracts/plugins-core-extension.signal.contract.test.ts b/src/channels/plugins/contracts/plugins-core-extension.signal.contract.test.ts deleted file mode 100644 index afe7b8dd55c..00000000000 --- a/src/channels/plugins/contracts/plugins-core-extension.signal.contract.test.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { describeSignalPluginsCoreExtensionContract } from "../../../../test/helpers/channels/plugins-core-extension-contract.js"; - -describeSignalPluginsCoreExtensionContract(); diff --git a/src/channels/plugins/contracts/plugins-core-extension.slack.contract.test.ts b/src/channels/plugins/contracts/plugins-core-extension.slack.contract.test.ts deleted file mode 100644 index 5d787d8e9ec..00000000000 --- a/src/channels/plugins/contracts/plugins-core-extension.slack.contract.test.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { describeSlackPluginsCoreExtensionContract } from "../../../../test/helpers/channels/plugins-core-extension-contract.js"; - -describeSlackPluginsCoreExtensionContract(); diff --git a/src/channels/plugins/contracts/plugins-core-extension.telegram.contract.test.ts b/src/channels/plugins/contracts/plugins-core-extension.telegram.contract.test.ts deleted file mode 100644 index 93dd6224c37..00000000000 --- a/src/channels/plugins/contracts/plugins-core-extension.telegram.contract.test.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { describeTelegramPluginsCoreExtensionContract } from "../../../../test/helpers/channels/plugins-core-extension-contract.js"; - -describeTelegramPluginsCoreExtensionContract(); diff --git a/src/channels/plugins/contracts/plugins-core-extension.whatsapp.contract.test.ts b/src/channels/plugins/contracts/plugins-core-extension.whatsapp.contract.test.ts deleted file mode 100644 index 061e606bdd2..00000000000 --- a/src/channels/plugins/contracts/plugins-core-extension.whatsapp.contract.test.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { describeWhatsAppPluginsCoreExtensionContract } from "../../../../test/helpers/channels/plugins-core-extension-contract.js"; - -describeWhatsAppPluginsCoreExtensionContract(); diff --git a/test/extension-test-boundary.test.ts b/test/extension-test-boundary.test.ts index a779f4c1a8c..466a5dac765 100644 --- a/test/extension-test-boundary.test.ts +++ b/test/extension-test-boundary.test.ts @@ -185,4 +185,15 @@ describe("non-extension test boundaries", () => { expect(offenders).toEqual([]); }); + + it("keeps extension channel contract helpers on the public testing surface", () => { + const files = walkCode(path.join(repoRoot, "extensions")); + + const offenders = files.filter((file) => { + const source = fs.readFileSync(path.join(repoRoot, file), "utf8"); + return source.includes("src/channels/plugins/contracts/test-helpers.js"); + }); + + expect(offenders).toEqual([]); + }); }); diff --git a/test/helpers/channels/inbound-contract.discord.ts b/test/helpers/channels/inbound-contract.discord.ts deleted file mode 100644 index ada975492b5..00000000000 --- a/test/helpers/channels/inbound-contract.discord.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { it } from "vitest"; -import { expectChannelInboundContextContract } from "../../../src/channels/plugins/contracts/test-helpers.js"; -import { resolveRelativeBundledPluginPublicModuleId } from "../../../src/test-utils/bundled-plugin-public-surface.js"; - -type BuildFinalizedDiscordDirectInboundContext = - () => import("../../../src/auto-reply/templating.js").MsgContext; - -const discordInboundContextHarnessModuleId = resolveRelativeBundledPluginPublicModuleId({ - fromModuleUrl: import.meta.url, - pluginId: "discord", - artifactBasename: "src/monitor/inbound-context.test-helpers.js", -}); - -async function getBuildFinalizedDiscordDirectInboundContext(): Promise { - const module = (await import(discordInboundContextHarnessModuleId)) as { - buildFinalizedDiscordDirectInboundContext: BuildFinalizedDiscordDirectInboundContext; - }; - return module.buildFinalizedDiscordDirectInboundContext; -} - -export function installDiscordInboundContractSuite() { - it("keeps inbound context finalized", async () => { - const buildContext = await getBuildFinalizedDiscordDirectInboundContext(); - const ctx = buildContext(); - - expectChannelInboundContextContract(ctx); - }); -} diff --git a/test/helpers/channels/inbound-contract.slack.ts b/test/helpers/channels/inbound-contract.slack.ts deleted file mode 100644 index cf3340dbbea..00000000000 --- a/test/helpers/channels/inbound-contract.slack.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { expect, it } from "vitest"; -import type { MsgContext } from "../../../src/auto-reply/templating.js"; -import { expectChannelInboundContextContract } from "../../../src/channels/plugins/contracts/test-helpers.js"; -import type { OpenClawConfig } from "../../../src/config/config.js"; -import { resolveRelativeBundledPluginPublicModuleId } from "../../../src/test-utils/bundled-plugin-public-surface.js"; -import { withTempHome } from "../temp-home.js"; - -type ResolvedSlackAccount = { - accountId: string; - enabled: boolean; - botTokenSource: string; - appTokenSource: string; - userTokenSource: string; - config: { - replyToMode?: unknown; - replyToModeByChatType?: unknown; - dm?: unknown; - }; - replyToMode?: unknown; - replyToModeByChatType?: unknown; - dm?: unknown; -}; - -type SlackMessageEvent = { - channel: string; - channel_type?: string; - user?: string; - text?: string; - ts: string; -}; - -type SlackPrepareResult = { ctxPayload: MsgContext } | null | undefined; - -type SlackTestApi = { - createInboundSlackTestContext: (params: { cfg: OpenClawConfig }) => { - resolveUserName?: () => Promise; - }; - prepareSlackMessage: (params: { - ctx: { - resolveUserName?: () => Promise; - }; - account: ResolvedSlackAccount; - message: SlackMessageEvent; - opts: { source: string }; - }) => Promise; -}; - -const slackPrepareTestApiModuleId = resolveRelativeBundledPluginPublicModuleId({ - fromModuleUrl: import.meta.url, - pluginId: "slack", - artifactBasename: "inbound-contract-test-api.js", -}); - -let slackTestApiPromise: Promise | undefined; - -async function loadSlackTestApi(): Promise { - slackTestApiPromise ??= import(slackPrepareTestApiModuleId) as Promise; - return await slackTestApiPromise; -} - -function createSlackAccount(config: ResolvedSlackAccount["config"] = {}): ResolvedSlackAccount { - return { - accountId: "default", - enabled: true, - botTokenSource: "config", - appTokenSource: "config", - userTokenSource: "none", - config, - replyToMode: config.replyToMode, - replyToModeByChatType: config.replyToModeByChatType, - dm: config.dm, - }; -} - -function createSlackMessage(overrides: Partial): SlackMessageEvent { - return { - channel: "D123", - channel_type: "im", - user: "U1", - text: "hi", - ts: "1.000", - ...overrides, - } as SlackMessageEvent; -} - -export function installSlackInboundContractSuite() { - it("keeps inbound context finalized", async () => { - await withTempHome(async () => { - const { createInboundSlackTestContext, prepareSlackMessage } = await loadSlackTestApi(); - const ctx = createInboundSlackTestContext({ - cfg: { - channels: { slack: { enabled: true } }, - } as OpenClawConfig, - }); - ctx.resolveUserName = async () => ({ name: "Alice" }) as never; - - const prepared = await prepareSlackMessage({ - ctx, - account: createSlackAccount(), - message: createSlackMessage({}), - opts: { source: "message" }, - }); - - expect(prepared).toBeTruthy(); - expectChannelInboundContextContract(prepared!.ctxPayload); - }); - }); -} diff --git a/test/helpers/channels/inbound-contract.telegram.ts b/test/helpers/channels/inbound-contract.telegram.ts deleted file mode 100644 index ed08fbc6aa4..00000000000 --- a/test/helpers/channels/inbound-contract.telegram.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { it } from "vitest"; -import { expectChannelInboundContextContract } from "../../../src/channels/plugins/contracts/test-helpers.js"; -import type { OpenClawConfig } from "../../../src/config/config.js"; -import { resolveRelativeBundledPluginPublicModuleId } from "../../../src/test-utils/bundled-plugin-public-surface.js"; - -const telegramHarnessModuleId = resolveRelativeBundledPluginPublicModuleId({ - fromModuleUrl: import.meta.url, - pluginId: "telegram", - artifactBasename: "src/bot-message-context.test-harness.js", -}); - -async function buildTelegramMessageContextForTest(params: { - cfg: OpenClawConfig; - message: Record; -}) { - const telegramHarnessModule = (await import(telegramHarnessModuleId)) as { - buildTelegramMessageContextForTest: (params: { - cfg: OpenClawConfig; - message: Record; - }) => Promise< - { ctxPayload: import("../../../src/auto-reply/templating.js").MsgContext } | null | undefined - >; - }; - return await telegramHarnessModule.buildTelegramMessageContextForTest(params); -} - -export function installTelegramInboundContractSuite() { - it("keeps inbound context finalized", async () => { - const context = await buildTelegramMessageContextForTest({ - cfg: { - agents: { - defaults: { - envelopeTimezone: "utc", - }, - }, - channels: { - telegram: { - groupPolicy: "open", - groups: { "*": { requireMention: false } }, - }, - }, - } satisfies OpenClawConfig, - message: { - chat: { id: 42, type: "group", title: "Ops" }, - text: "hello", - date: 1736380800, - message_id: 2, - from: { - id: 99, - first_name: "Ada", - last_name: "Lovelace", - username: "ada", - }, - }, - }); - - const payload = context?.ctxPayload; - if (!payload) { - throw new Error("expected telegram inbound payload"); - } - expectChannelInboundContextContract(payload); - }); -} diff --git a/test/helpers/channels/plugins-core-extension-contract.ts b/test/helpers/channels/plugins-core-extension-contract.ts deleted file mode 100644 index 6f84cdd372e..00000000000 --- a/test/helpers/channels/plugins-core-extension-contract.ts +++ /dev/null @@ -1,452 +0,0 @@ -import { describe, expect, expectTypeOf, it } from "vitest"; -import type { - BaseProbeResult, - BaseTokenResolution, - ChannelDirectoryEntry, -} from "../../../src/channels/plugins/types.js"; -import type { OpenClawConfig } from "../../../src/config/config.js"; -import type { LineProbeResult } from "../../../src/plugin-sdk/line.js"; -import { resolveRelativeBundledPluginPublicModuleId } from "../../../src/test-utils/bundled-plugin-public-surface.js"; -import { withEnvAsync } from "../../../src/test-utils/env.js"; - -type DiscordDirectoryContractApiSurface = { - listDiscordDirectoryPeersFromConfig: DirectoryListFn; - listDiscordDirectoryGroupsFromConfig: DirectoryListFn; -}; -type DiscordProbe = BaseProbeResult; -type DiscordTokenResolution = BaseTokenResolution; -type IMessageProbe = BaseProbeResult; -type SignalProbe = BaseProbeResult; -type SlackDirectoryContractApiSurface = { - listSlackDirectoryPeersFromConfig: DirectoryListFn; - listSlackDirectoryGroupsFromConfig: DirectoryListFn; -}; -type SlackProbe = BaseProbeResult; -type TelegramDirectoryContractApiSurface = { - listTelegramDirectoryPeersFromConfig: DirectoryListFn; - listTelegramDirectoryGroupsFromConfig: DirectoryListFn; -}; -type TelegramProbe = BaseProbeResult; -type TelegramTokenResolution = BaseTokenResolution; -type WhatsAppDirectoryContractApiSurface = { - listWhatsAppDirectoryPeersFromConfig: DirectoryListFn; - listWhatsAppDirectoryGroupsFromConfig: DirectoryListFn; -}; - -let discordDirectoryContractApi: Promise | undefined; -let slackDirectoryContractApi: Promise | undefined; -let telegramDirectoryContractApi: Promise | undefined; -let whatsappDirectoryContractApi: Promise | undefined; - -async function importDirectoryContractApi(pluginId: string): Promise { - const moduleId = resolveRelativeBundledPluginPublicModuleId({ - fromModuleUrl: import.meta.url, - pluginId, - artifactBasename: "directory-contract-api.js", - }); - return (await import(moduleId)) as T; -} - -function getDiscordDirectoryContractApi(): Promise { - discordDirectoryContractApi ??= - importDirectoryContractApi("discord"); - return discordDirectoryContractApi; -} - -function getSlackDirectoryContractApi(): Promise { - slackDirectoryContractApi ??= - importDirectoryContractApi("slack"); - return slackDirectoryContractApi; -} - -function getTelegramDirectoryContractApi(): Promise { - telegramDirectoryContractApi ??= - importDirectoryContractApi("telegram"); - return telegramDirectoryContractApi; -} - -function getWhatsAppDirectoryContractApi(): Promise { - whatsappDirectoryContractApi ??= - importDirectoryContractApi("whatsapp"); - return whatsappDirectoryContractApi; -} - -type DirectoryListFn = (params: { - cfg: OpenClawConfig; - accountId?: string; - query?: string | null; - limit?: number | null; -}) => Promise; - -async function listDirectoryEntriesWithDefaults(listFn: DirectoryListFn, cfg: OpenClawConfig) { - return await listFn({ - cfg, - accountId: "default", - query: null, - limit: null, - }); -} - -async function expectDirectoryIds( - listFn: DirectoryListFn, - cfg: OpenClawConfig, - expected: string[], - options?: { sorted?: boolean }, -) { - const entries = await listDirectoryEntriesWithDefaults(listFn, cfg); - const ids = entries.map((entry) => entry.id); - expect(options?.sorted ? ids.toSorted() : ids).toEqual(expected); -} - -export function describeDiscordPluginsCoreExtensionContract() { - describe("discord plugins-core extension contract", () => { - it("DiscordProbe satisfies BaseProbeResult", () => { - expectTypeOf().toMatchTypeOf(); - }); - - it("Discord token resolution satisfies BaseTokenResolution", () => { - expectTypeOf().toMatchTypeOf(); - }); - - it("lists peers/groups from config (numeric ids only)", async () => { - const { listDiscordDirectoryGroupsFromConfig, listDiscordDirectoryPeersFromConfig } = - await getDiscordDirectoryContractApi(); - const cfg = { - channels: { - discord: { - token: "discord-test", - dm: { allowFrom: ["<@111>", "<@!333>", "nope"] }, - dms: { "222": {} }, - guilds: { - "123": { - users: ["<@12345>", " discord:444 ", "not-an-id"], - channels: { - "555": {}, - "<#777>": {}, - "channel:666": {}, - general: {}, - }, - }, - }, - }, - }, - } as unknown as OpenClawConfig; - - await expectDirectoryIds( - listDiscordDirectoryPeersFromConfig, - cfg, - ["user:111", "user:12345", "user:222", "user:333", "user:444"], - { sorted: true }, - ); - await expectDirectoryIds( - listDiscordDirectoryGroupsFromConfig, - cfg, - ["channel:555", "channel:666", "channel:777"], - { - sorted: true, - }, - ); - }); - - it("keeps directories readable when tokens are unresolved SecretRefs", async () => { - const { listDiscordDirectoryGroupsFromConfig, listDiscordDirectoryPeersFromConfig } = - await getDiscordDirectoryContractApi(); - const envSecret = { - source: "env", - provider: "default", - id: "MISSING_TEST_SECRET", - } as const; - const cfg = { - channels: { - discord: { - token: envSecret, - dm: { allowFrom: ["<@111>"] }, - guilds: { - "123": { - channels: { - "555": {}, - }, - }, - }, - }, - }, - } as unknown as OpenClawConfig; - - await expectDirectoryIds(listDiscordDirectoryPeersFromConfig, cfg, ["user:111"]); - await expectDirectoryIds(listDiscordDirectoryGroupsFromConfig, cfg, ["channel:555"]); - }); - - it("applies query and limit filtering for config-backed directories", async () => { - const { listDiscordDirectoryGroupsFromConfig } = await getDiscordDirectoryContractApi(); - const cfg = { - channels: { - discord: { - token: "discord-test", - guilds: { - "123": { - channels: { - "555": {}, - "666": {}, - "777": {}, - }, - }, - }, - }, - }, - } as unknown as OpenClawConfig; - - const groups = await listDiscordDirectoryGroupsFromConfig({ - cfg, - accountId: "default", - query: "666", - limit: 5, - }); - expect(groups.map((entry) => entry.id)).toEqual(["channel:666"]); - }); - }); -} - -export function describeSlackPluginsCoreExtensionContract() { - describe("slack plugins-core extension contract", () => { - it("SlackProbe satisfies BaseProbeResult", () => { - expectTypeOf().toMatchTypeOf(); - }); - - it("lists peers/groups from config", async () => { - const { listSlackDirectoryGroupsFromConfig, listSlackDirectoryPeersFromConfig } = - await getSlackDirectoryContractApi(); - const cfg = { - channels: { - slack: { - botToken: "xoxb-test", - appToken: "xapp-test", - dm: { allowFrom: ["U123", "user:U999"] }, - dms: { U234: {} }, - channels: { C111: { users: ["U777"] } }, - }, - }, - } as unknown as OpenClawConfig; - - await expectDirectoryIds( - listSlackDirectoryPeersFromConfig, - cfg, - ["user:u123", "user:u234", "user:u777", "user:u999"], - { sorted: true }, - ); - await expectDirectoryIds(listSlackDirectoryGroupsFromConfig, cfg, ["channel:c111"]); - }); - - it("keeps directories readable when tokens are unresolved SecretRefs", async () => { - const { listSlackDirectoryGroupsFromConfig, listSlackDirectoryPeersFromConfig } = - await getSlackDirectoryContractApi(); - const envSecret = { - source: "env", - provider: "default", - id: "MISSING_TEST_SECRET", - } as const; - const cfg = { - channels: { - slack: { - botToken: envSecret, - appToken: envSecret, - dm: { allowFrom: ["U123"] }, - channels: { C111: {} }, - }, - }, - } as unknown as OpenClawConfig; - - await expectDirectoryIds(listSlackDirectoryPeersFromConfig, cfg, ["user:u123"]); - await expectDirectoryIds(listSlackDirectoryGroupsFromConfig, cfg, ["channel:c111"]); - }); - - it("applies query and limit filtering for config-backed directories", async () => { - const { listSlackDirectoryPeersFromConfig } = await getSlackDirectoryContractApi(); - const cfg = { - channels: { - slack: { - botToken: "xoxb-test", - appToken: "xapp-test", - dm: { allowFrom: ["U100", "U200"] }, - dms: { U300: {} }, - }, - }, - } as unknown as OpenClawConfig; - - const peers = await listSlackDirectoryPeersFromConfig({ - cfg, - accountId: "default", - query: "user:u", - limit: 2, - }); - expect(peers).toHaveLength(2); - expect(peers.every((entry) => entry.id.startsWith("user:u"))).toBe(true); - }); - }); -} - -export function describeTelegramPluginsCoreExtensionContract() { - describe("telegram plugins-core extension contract", () => { - it("TelegramProbe satisfies BaseProbeResult", () => { - expectTypeOf().toMatchTypeOf(); - }); - - it("Telegram token resolution satisfies BaseTokenResolution", () => { - expectTypeOf().toMatchTypeOf(); - }); - - it("lists peers/groups from config", async () => { - const { listTelegramDirectoryGroupsFromConfig, listTelegramDirectoryPeersFromConfig } = - await getTelegramDirectoryContractApi(); - const cfg = { - channels: { - telegram: { - botToken: "telegram-test", - allowFrom: ["123", "alice", "tg:@bob"], - dms: { "456": {} }, - groups: { "-1001": {}, "*": {} }, - }, - }, - } as unknown as OpenClawConfig; - - await expectDirectoryIds( - listTelegramDirectoryPeersFromConfig, - cfg, - ["123", "456", "@alice", "@bob"], - { - sorted: true, - }, - ); - await expectDirectoryIds(listTelegramDirectoryGroupsFromConfig, cfg, ["-1001"]); - }); - - it("keeps fallback semantics when accountId is omitted", async () => { - const { listTelegramDirectoryGroupsFromConfig, listTelegramDirectoryPeersFromConfig } = - await getTelegramDirectoryContractApi(); - await withEnvAsync({ TELEGRAM_BOT_TOKEN: "tok-env" }, async () => { - const cfg = { - channels: { - telegram: { - allowFrom: ["alice"], - groups: { "-1001": {} }, - accounts: { - work: { - botToken: "tok-work", - allowFrom: ["bob"], - groups: { "-2002": {} }, - }, - }, - }, - }, - } as unknown as OpenClawConfig; - - await expectDirectoryIds(listTelegramDirectoryPeersFromConfig, cfg, ["@alice"]); - await expectDirectoryIds(listTelegramDirectoryGroupsFromConfig, cfg, ["-1001"]); - }); - }); - - it("keeps directories readable when tokens are unresolved SecretRefs", async () => { - const { listTelegramDirectoryGroupsFromConfig, listTelegramDirectoryPeersFromConfig } = - await getTelegramDirectoryContractApi(); - const envSecret = { - source: "env", - provider: "default", - id: "MISSING_TEST_SECRET", - } as const; - const cfg = { - channels: { - telegram: { - botToken: envSecret, - allowFrom: ["alice"], - groups: { "-1001": {} }, - }, - }, - } as unknown as OpenClawConfig; - - await expectDirectoryIds(listTelegramDirectoryPeersFromConfig, cfg, ["@alice"]); - await expectDirectoryIds(listTelegramDirectoryGroupsFromConfig, cfg, ["-1001"]); - }); - - it("applies query and limit filtering for config-backed directories", async () => { - const { listTelegramDirectoryGroupsFromConfig } = await getTelegramDirectoryContractApi(); - const cfg = { - channels: { - telegram: { - botToken: "telegram-test", - groups: { "-1001": {}, "-1002": {}, "-2001": {} }, - }, - }, - } as unknown as OpenClawConfig; - - const groups = await listTelegramDirectoryGroupsFromConfig({ - cfg, - accountId: "default", - query: "-100", - limit: 1, - }); - expect(groups.map((entry) => entry.id)).toEqual(["-1001"]); - }); - }); -} - -export function describeWhatsAppPluginsCoreExtensionContract() { - describe("whatsapp plugins-core extension contract", () => { - it("lists peers/groups from config", async () => { - const { listWhatsAppDirectoryGroupsFromConfig, listWhatsAppDirectoryPeersFromConfig } = - await getWhatsAppDirectoryContractApi(); - const cfg = { - channels: { - whatsapp: { - allowFrom: ["+15550000000", "*", "123@g.us"], - groups: { "999@g.us": { requireMention: true }, "*": {} }, - }, - }, - } as unknown as OpenClawConfig; - - await expectDirectoryIds(listWhatsAppDirectoryPeersFromConfig, cfg, ["+15550000000"]); - await expectDirectoryIds(listWhatsAppDirectoryGroupsFromConfig, cfg, ["999@g.us"]); - }); - - it("applies query and limit filtering for config-backed directories", async () => { - const { listWhatsAppDirectoryGroupsFromConfig } = await getWhatsAppDirectoryContractApi(); - const cfg = { - channels: { - whatsapp: { - groups: { "111@g.us": {}, "222@g.us": {}, "333@s.whatsapp.net": {} }, - }, - }, - } as unknown as OpenClawConfig; - - const groups = await listWhatsAppDirectoryGroupsFromConfig({ - cfg, - accountId: "default", - query: "@g.us", - limit: 1, - }); - expect(groups.map((entry) => entry.id)).toEqual(["111@g.us"]); - }); - }); -} - -export function describeSignalPluginsCoreExtensionContract() { - describe("signal plugins-core extension contract", () => { - it("SignalProbe satisfies BaseProbeResult", () => { - expectTypeOf().toMatchTypeOf(); - }); - }); -} - -export function describeIMessagePluginsCoreExtensionContract() { - describe("imessage plugins-core extension contract", () => { - it("IMessageProbe satisfies BaseProbeResult", () => { - expectTypeOf().toMatchTypeOf(); - }); - }); -} - -export function describeLinePluginsCoreExtensionContract() { - describe("line plugins-core extension contract", () => { - it("LineProbeResult satisfies BaseProbeResult", () => { - expectTypeOf().toMatchTypeOf(); - }); - }); -} diff --git a/test/scripts/channel-contract-test-plan.test.ts b/test/scripts/channel-contract-test-plan.test.ts index 5b862b2de80..37fab59a5c0 100644 --- a/test/scripts/channel-contract-test-plan.test.ts +++ b/test/scripts/channel-contract-test-plan.test.ts @@ -43,11 +43,6 @@ describe("scripts/lib/channel-contract-test-plan.mjs", () => { runtime: "node", task: "contracts-channels", }, - { - checkName: "checks-fast-contracts-channels-extensions", - runtime: "node", - task: "contracts-channels", - }, ]); });