mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:30:42 +00:00
test(extensions): move channel contracts out of core
This commit is contained in:
136
extensions/discord/src/directory-contract.test.ts
Normal file
136
extensions/discord/src/directory-contract.test.ts
Normal 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"]);
|
||||
});
|
||||
});
|
||||
11
extensions/discord/src/inbound-context.contract.test.ts
Normal file
11
extensions/discord/src/inbound-context.contract.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
@@ -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";
|
||||
|
||||
|
||||
9
extensions/imessage/src/probe.contract.test.ts
Normal file
9
extensions/imessage/src/probe.contract.test.ts
Normal 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>();
|
||||
});
|
||||
});
|
||||
9
extensions/line/src/probe.contract.test.ts
Normal file
9
extensions/line/src/probe.contract.test.ts
Normal 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>();
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -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 },
|
||||
|
||||
9
extensions/signal/src/probe.contract.test.ts
Normal file
9
extensions/signal/src/probe.contract.test.ts
Normal 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>();
|
||||
});
|
||||
});
|
||||
106
extensions/slack/src/directory-contract.test.ts
Normal file
106
extensions/slack/src/directory-contract.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
64
extensions/slack/src/inbound-context.contract.test.ts
Normal file
64
extensions/slack/src/inbound-context.contract.test.ts
Normal 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();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -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";
|
||||
|
||||
130
extensions/telegram/src/directory-contract.test.ts
Normal file
130
extensions/telegram/src/directory-contract.test.ts
Normal 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"]);
|
||||
});
|
||||
});
|
||||
44
extensions/telegram/src/inbound-context.contract.test.ts
Normal file
44
extensions/telegram/src/inbound-context.contract.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
@@ -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",
|
||||
});
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
export { expectChannelInboundContextContract } from "../../../../src/channels/plugins/contracts/test-helpers.js";
|
||||
export { expectChannelInboundContextContract } from "openclaw/plugin-sdk/testing";
|
||||
|
||||
66
extensions/whatsapp/src/directory-contract.test.ts
Normal file
66
extensions/whatsapp/src/directory-contract.test.ts
Normal 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"]);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -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";
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -1,3 +0,0 @@
|
||||
import { describeDiscordPluginsCoreExtensionContract } from "../../../../test/helpers/channels/plugins-core-extension-contract.js";
|
||||
|
||||
describeDiscordPluginsCoreExtensionContract();
|
||||
@@ -1,3 +0,0 @@
|
||||
import { describeIMessagePluginsCoreExtensionContract } from "../../../../test/helpers/channels/plugins-core-extension-contract.js";
|
||||
|
||||
describeIMessagePluginsCoreExtensionContract();
|
||||
@@ -1,3 +0,0 @@
|
||||
import { describeLinePluginsCoreExtensionContract } from "../../../../test/helpers/channels/plugins-core-extension-contract.js";
|
||||
|
||||
describeLinePluginsCoreExtensionContract();
|
||||
@@ -1,3 +0,0 @@
|
||||
import { describeSignalPluginsCoreExtensionContract } from "../../../../test/helpers/channels/plugins-core-extension-contract.js";
|
||||
|
||||
describeSignalPluginsCoreExtensionContract();
|
||||
@@ -1,3 +0,0 @@
|
||||
import { describeSlackPluginsCoreExtensionContract } from "../../../../test/helpers/channels/plugins-core-extension-contract.js";
|
||||
|
||||
describeSlackPluginsCoreExtensionContract();
|
||||
@@ -1,3 +0,0 @@
|
||||
import { describeTelegramPluginsCoreExtensionContract } from "../../../../test/helpers/channels/plugins-core-extension-contract.js";
|
||||
|
||||
describeTelegramPluginsCoreExtensionContract();
|
||||
@@ -1,3 +0,0 @@
|
||||
import { describeWhatsAppPluginsCoreExtensionContract } from "../../../../test/helpers/channels/plugins-core-extension-contract.js";
|
||||
|
||||
describeWhatsAppPluginsCoreExtensionContract();
|
||||
@@ -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([]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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<BuildFinalizedDiscordDirectInboundContext> {
|
||||
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);
|
||||
});
|
||||
}
|
||||
@@ -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<unknown>;
|
||||
};
|
||||
prepareSlackMessage: (params: {
|
||||
ctx: {
|
||||
resolveUserName?: () => Promise<unknown>;
|
||||
};
|
||||
account: ResolvedSlackAccount;
|
||||
message: SlackMessageEvent;
|
||||
opts: { source: string };
|
||||
}) => Promise<SlackPrepareResult>;
|
||||
};
|
||||
|
||||
const slackPrepareTestApiModuleId = resolveRelativeBundledPluginPublicModuleId({
|
||||
fromModuleUrl: import.meta.url,
|
||||
pluginId: "slack",
|
||||
artifactBasename: "inbound-contract-test-api.js",
|
||||
});
|
||||
|
||||
let slackTestApiPromise: Promise<SlackTestApi> | undefined;
|
||||
|
||||
async function loadSlackTestApi(): Promise<SlackTestApi> {
|
||||
slackTestApiPromise ??= import(slackPrepareTestApiModuleId) as Promise<SlackTestApi>;
|
||||
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>): 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);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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<string, unknown>;
|
||||
}) {
|
||||
const telegramHarnessModule = (await import(telegramHarnessModuleId)) as {
|
||||
buildTelegramMessageContextForTest: (params: {
|
||||
cfg: OpenClawConfig;
|
||||
message: Record<string, unknown>;
|
||||
}) => 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);
|
||||
});
|
||||
}
|
||||
@@ -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<DiscordDirectoryContractApiSurface> | undefined;
|
||||
let slackDirectoryContractApi: Promise<SlackDirectoryContractApiSurface> | undefined;
|
||||
let telegramDirectoryContractApi: Promise<TelegramDirectoryContractApiSurface> | undefined;
|
||||
let whatsappDirectoryContractApi: Promise<WhatsAppDirectoryContractApiSurface> | undefined;
|
||||
|
||||
async function importDirectoryContractApi<T extends object>(pluginId: string): Promise<T> {
|
||||
const moduleId = resolveRelativeBundledPluginPublicModuleId({
|
||||
fromModuleUrl: import.meta.url,
|
||||
pluginId,
|
||||
artifactBasename: "directory-contract-api.js",
|
||||
});
|
||||
return (await import(moduleId)) as T;
|
||||
}
|
||||
|
||||
function getDiscordDirectoryContractApi(): Promise<DiscordDirectoryContractApiSurface> {
|
||||
discordDirectoryContractApi ??=
|
||||
importDirectoryContractApi<DiscordDirectoryContractApiSurface>("discord");
|
||||
return discordDirectoryContractApi;
|
||||
}
|
||||
|
||||
function getSlackDirectoryContractApi(): Promise<SlackDirectoryContractApiSurface> {
|
||||
slackDirectoryContractApi ??=
|
||||
importDirectoryContractApi<SlackDirectoryContractApiSurface>("slack");
|
||||
return slackDirectoryContractApi;
|
||||
}
|
||||
|
||||
function getTelegramDirectoryContractApi(): Promise<TelegramDirectoryContractApiSurface> {
|
||||
telegramDirectoryContractApi ??=
|
||||
importDirectoryContractApi<TelegramDirectoryContractApiSurface>("telegram");
|
||||
return telegramDirectoryContractApi;
|
||||
}
|
||||
|
||||
function getWhatsAppDirectoryContractApi(): Promise<WhatsAppDirectoryContractApiSurface> {
|
||||
whatsappDirectoryContractApi ??=
|
||||
importDirectoryContractApi<WhatsAppDirectoryContractApiSurface>("whatsapp");
|
||||
return whatsappDirectoryContractApi;
|
||||
}
|
||||
|
||||
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() : ids).toEqual(expected);
|
||||
}
|
||||
|
||||
export function describeDiscordPluginsCoreExtensionContract() {
|
||||
describe("discord plugins-core extension contract", () => {
|
||||
it("DiscordProbe satisfies BaseProbeResult", () => {
|
||||
expectTypeOf<DiscordProbe>().toMatchTypeOf<BaseProbeResult>();
|
||||
});
|
||||
|
||||
it("Discord token resolution satisfies BaseTokenResolution", () => {
|
||||
expectTypeOf<DiscordTokenResolution>().toMatchTypeOf<BaseTokenResolution>();
|
||||
});
|
||||
|
||||
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<SlackProbe>().toMatchTypeOf<BaseProbeResult>();
|
||||
});
|
||||
|
||||
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<TelegramProbe>().toMatchTypeOf<BaseProbeResult>();
|
||||
});
|
||||
|
||||
it("Telegram token resolution satisfies BaseTokenResolution", () => {
|
||||
expectTypeOf<TelegramTokenResolution>().toMatchTypeOf<BaseTokenResolution>();
|
||||
});
|
||||
|
||||
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<SignalProbe>().toMatchTypeOf<BaseProbeResult>();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function describeIMessagePluginsCoreExtensionContract() {
|
||||
describe("imessage plugins-core extension contract", () => {
|
||||
it("IMessageProbe satisfies BaseProbeResult", () => {
|
||||
expectTypeOf<IMessageProbe>().toMatchTypeOf<BaseProbeResult>();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function describeLinePluginsCoreExtensionContract() {
|
||||
describe("line plugins-core extension contract", () => {
|
||||
it("LineProbeResult satisfies BaseProbeResult", () => {
|
||||
expectTypeOf<LineProbeResult>().toMatchTypeOf<BaseProbeResult>();
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user