test(extensions): move channel contracts out of core

This commit is contained in:
Peter Steinberger
2026-04-20 17:59:33 +01:00
parent 1f139c198a
commit ca2d89bc4d
34 changed files with 625 additions and 726 deletions

View File

@@ -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<ChannelDirectoryEntry[]>;
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<DiscordProbe>().toMatchTypeOf<BaseProbeResult>();
expectTypeOf<DiscordTokenResolution>().toMatchTypeOf<BaseTokenResolution>();
});
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"]);
});
});

View File

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

View File

@@ -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";

View File

@@ -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<IMessageProbe>().toMatchTypeOf<BaseProbeResult>();
});
});

View File

@@ -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<LineProbeResult>().toMatchTypeOf<BaseProbeResult>();
});
});

View File

@@ -0,0 +1,32 @@
import { finalizeInboundContext } from "openclaw/plugin-sdk/reply-runtime";
import { expectChannelInboundContextContract } from "openclaw/plugin-sdk/testing";
import { describe, it } from "vitest";
describe("Signal inbound context contract", () => {
it("keeps inbound context finalized", () => {
const ctx = finalizeInboundContext({
Body: "Alice: hi",
BodyForAgent: "hi",
RawBody: "hi",
CommandBody: "hi",
BodyForCommands: "hi",
From: "group:g1",
To: "group:g1",
SessionKey: "agent:main:signal:group:g1",
AccountId: "default",
ChatType: "group",
ConversationLabel: "Alice",
GroupSubject: "Test Group",
SenderName: "Alice",
SenderId: "+15550001111",
Provider: "signal",
Surface: "signal",
MessageSid: "1700000000000",
OriginatingChannel: "signal",
OriginatingTo: "group:g1",
CommandAuthorized: true,
});
expectChannelInboundContextContract(ctx);
});
});

View File

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

View File

@@ -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<SignalProbe>().toMatchTypeOf<BaseProbeResult>();
});
});

View File

@@ -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<ChannelDirectoryEntry[]>;
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<SlackProbe>().toMatchTypeOf<BaseProbeResult>();
});
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);
});
});

View File

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

View File

@@ -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";

View File

@@ -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<ChannelDirectoryEntry[]>;
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<TelegramProbe>().toMatchTypeOf<BaseProbeResult>();
expectTypeOf<TelegramTokenResolution>().toMatchTypeOf<BaseTokenResolution>();
});
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"]);
});
});

View File

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

View File

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

View File

@@ -1 +1 @@
export { expectChannelInboundContextContract } from "../../../../src/channels/plugins/contracts/test-helpers.js";
export { expectChannelInboundContextContract } from "openclaw/plugin-sdk/testing";

View File

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

View File

@@ -0,0 +1,33 @@
import { finalizeInboundContext } from "openclaw/plugin-sdk/reply-runtime";
import { expectChannelInboundContextContract } from "openclaw/plugin-sdk/testing";
import { describe, it } from "vitest";
describe("WhatsApp inbound context contract", () => {
it("keeps inbound context finalized", () => {
const ctx = finalizeInboundContext({
Body: "Alice: hi",
BodyForAgent: "hi",
RawBody: "hi",
CommandBody: "hi",
BodyForCommands: "hi",
From: "123@g.us",
To: "+15550001111",
SessionKey: "agent:main:whatsapp:group:123",
AccountId: "default",
ChatType: "group",
ConversationLabel: "123@g.us",
GroupSubject: "Test Group",
SenderName: "Alice",
SenderId: "alice@s.whatsapp.net",
SenderE164: "+15550002222",
Provider: "whatsapp",
Surface: "whatsapp",
MessageSid: "msg1",
OriginatingChannel: "whatsapp",
OriginatingTo: "123@g.us",
CommandAuthorized: true,
});
expectChannelInboundContextContract(ctx);
});
});

View File

@@ -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";