883 lines
24 KiB
TypeScript
883 lines
24 KiB
TypeScript
import fs from "node:fs";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { expect, vi } from "vitest";
|
|
import {
|
|
__testing as discordThreadBindingTesting,
|
|
createThreadBindingManager as createDiscordThreadBindingManager,
|
|
} from "../../../../extensions/discord/runtime-api.js";
|
|
import { createFeishuThreadBindingManager } from "../../../../extensions/feishu/api.js";
|
|
import {
|
|
createMatrixThreadBindingManager,
|
|
resetMatrixThreadBindingsForTests,
|
|
} from "../../../../extensions/matrix/api.js";
|
|
import { setMatrixRuntime } from "../../../../extensions/matrix/index.js";
|
|
import { createTelegramThreadBindingManager } from "../../../../extensions/telegram/runtime-api.js";
|
|
import type { OpenClawConfig } from "../../../config/config.js";
|
|
import {
|
|
getSessionBindingService,
|
|
type SessionBindingCapabilities,
|
|
type SessionBindingRecord,
|
|
} from "../../../infra/outbound/session-binding-service.js";
|
|
import {
|
|
resolveDefaultLineAccountId,
|
|
resolveLineAccount,
|
|
listLineAccountIds,
|
|
} from "../../../line/accounts.js";
|
|
import {
|
|
bundledChannelPlugins,
|
|
bundledChannelRuntimeSetters,
|
|
requireBundledChannelPlugin,
|
|
} from "../bundled.js";
|
|
import type { ChannelPlugin } from "../types.js";
|
|
|
|
type PluginContractEntry = {
|
|
id: string;
|
|
plugin: Pick<ChannelPlugin, "id" | "meta" | "capabilities" | "config">;
|
|
};
|
|
|
|
type ActionsContractEntry = {
|
|
id: string;
|
|
plugin: Pick<ChannelPlugin, "id" | "actions">;
|
|
unsupportedAction?: string;
|
|
cases: Array<{
|
|
name: string;
|
|
cfg: OpenClawConfig;
|
|
expectedActions: string[];
|
|
expectedCapabilities?: string[];
|
|
beforeTest?: () => void;
|
|
}>;
|
|
};
|
|
|
|
type SetupContractEntry = {
|
|
id: string;
|
|
plugin: Pick<ChannelPlugin, "id" | "config" | "setup">;
|
|
cases: Array<{
|
|
name: string;
|
|
cfg: OpenClawConfig;
|
|
accountId?: string;
|
|
input: Record<string, unknown>;
|
|
expectedAccountId?: string;
|
|
expectedValidation?: string | null;
|
|
beforeTest?: () => void;
|
|
assertPatchedConfig?: (cfg: OpenClawConfig) => void;
|
|
assertResolvedAccount?: (account: unknown, cfg: OpenClawConfig) => void;
|
|
}>;
|
|
};
|
|
|
|
type StatusContractEntry = {
|
|
id: string;
|
|
plugin: Pick<ChannelPlugin, "id" | "config" | "status">;
|
|
cases: Array<{
|
|
name: string;
|
|
cfg: OpenClawConfig;
|
|
accountId?: string;
|
|
runtime?: Record<string, unknown>;
|
|
probe?: unknown;
|
|
beforeTest?: () => void;
|
|
assertSnapshot?: (snapshot: Record<string, unknown>) => void;
|
|
assertSummary?: (summary: Record<string, unknown>) => void;
|
|
}>;
|
|
};
|
|
|
|
export const channelPluginSurfaceKeys = [
|
|
"actions",
|
|
"setup",
|
|
"status",
|
|
"outbound",
|
|
"messaging",
|
|
"threading",
|
|
"directory",
|
|
"gateway",
|
|
] as const;
|
|
|
|
export type ChannelPluginSurface =
|
|
| "actions"
|
|
| "setup"
|
|
| "status"
|
|
| "outbound"
|
|
| "messaging"
|
|
| "threading"
|
|
| "directory"
|
|
| "gateway";
|
|
|
|
type SurfaceContractEntry = {
|
|
id: string;
|
|
plugin: Pick<
|
|
ChannelPlugin,
|
|
| "id"
|
|
| "actions"
|
|
| "setup"
|
|
| "status"
|
|
| "outbound"
|
|
| "messaging"
|
|
| "threading"
|
|
| "directory"
|
|
| "gateway"
|
|
>;
|
|
surfaces: readonly ChannelPluginSurface[];
|
|
};
|
|
|
|
type ThreadingContractEntry = {
|
|
id: string;
|
|
plugin: Pick<ChannelPlugin, "id" | "threading">;
|
|
};
|
|
|
|
type DirectoryContractEntry = {
|
|
id: string;
|
|
plugin: Pick<ChannelPlugin, "id" | "directory">;
|
|
coverage: "lookups" | "presence";
|
|
cfg?: OpenClawConfig;
|
|
accountId?: string;
|
|
};
|
|
|
|
type SessionBindingContractEntry = {
|
|
id: string;
|
|
expectedCapabilities: SessionBindingCapabilities;
|
|
getCapabilities: () => SessionBindingCapabilities | Promise<SessionBindingCapabilities>;
|
|
bindAndResolve: () => Promise<SessionBindingRecord>;
|
|
unbindAndVerify: (binding: SessionBindingRecord) => Promise<void>;
|
|
cleanup: () => Promise<void> | void;
|
|
};
|
|
|
|
function expectResolvedSessionBinding(params: {
|
|
channel: string;
|
|
accountId: string;
|
|
conversationId: string;
|
|
parentConversationId?: string;
|
|
targetSessionKey: string;
|
|
}) {
|
|
expect(
|
|
getSessionBindingService().resolveByConversation({
|
|
channel: params.channel,
|
|
accountId: params.accountId,
|
|
conversationId: params.conversationId,
|
|
parentConversationId: params.parentConversationId,
|
|
}),
|
|
)?.toMatchObject({
|
|
targetSessionKey: params.targetSessionKey,
|
|
});
|
|
}
|
|
|
|
async function unbindAndExpectClearedSessionBinding(binding: SessionBindingRecord) {
|
|
const service = getSessionBindingService();
|
|
const removed = await service.unbind({
|
|
bindingId: binding.bindingId,
|
|
reason: "contract-test",
|
|
});
|
|
expect(removed.map((entry) => entry.bindingId)).toContain(binding.bindingId);
|
|
expect(service.resolveByConversation(binding.conversation)).toBeNull();
|
|
}
|
|
|
|
function expectClearedSessionBinding(params: {
|
|
channel: string;
|
|
accountId: string;
|
|
conversationId: string;
|
|
}) {
|
|
expect(
|
|
getSessionBindingService().resolveByConversation({
|
|
channel: params.channel,
|
|
accountId: params.accountId,
|
|
conversationId: params.conversationId,
|
|
}),
|
|
).toBeNull();
|
|
}
|
|
|
|
const telegramDescribeMessageToolMock = vi.fn();
|
|
const discordDescribeMessageToolMock = vi.fn();
|
|
const sendMessageMatrixMock = vi.hoisted(() =>
|
|
vi.fn(async (to: string, _message: string, opts?: { threadId?: string }) => ({
|
|
messageId: opts?.threadId ? "$matrix-thread" : "$matrix-root",
|
|
roomId: to.replace(/^room:/, ""),
|
|
})),
|
|
);
|
|
|
|
bundledChannelRuntimeSetters.setTelegramRuntime({
|
|
channel: {
|
|
telegram: {
|
|
messageActions: {
|
|
listActions: telegramListActionsMock,
|
|
getCapabilities: telegramGetCapabilitiesMock,
|
|
},
|
|
},
|
|
},
|
|
} as never);
|
|
|
|
bundledChannelRuntimeSetters.setDiscordRuntime({
|
|
channel: {
|
|
discord: {
|
|
messageActions: {
|
|
listActions: discordListActionsMock,
|
|
getCapabilities: discordGetCapabilitiesMock,
|
|
},
|
|
},
|
|
},
|
|
} as never);
|
|
|
|
bundledChannelRuntimeSetters.setLineRuntime({
|
|
channel: {
|
|
line: {
|
|
listLineAccountIds,
|
|
resolveDefaultLineAccountId,
|
|
resolveLineAccount: ({ cfg, accountId }: { cfg: OpenClawConfig; accountId?: string }) =>
|
|
resolveLineAccount({ cfg, accountId }),
|
|
},
|
|
},
|
|
} as never);
|
|
|
|
vi.mock("../../../../extensions/matrix/runtime-api.js", async () => {
|
|
const actual = await vi.importActual<
|
|
typeof import("../../../../extensions/matrix/runtime-api.js")
|
|
>("../../../../extensions/matrix/runtime-api.js");
|
|
return {
|
|
...actual,
|
|
sendMessageMatrix: sendMessageMatrixMock,
|
|
};
|
|
});
|
|
|
|
const matrixSessionBindingStateDir = fs.mkdtempSync(
|
|
path.join(os.tmpdir(), "openclaw-matrix-session-binding-contract-"),
|
|
);
|
|
const matrixSessionBindingAuth = {
|
|
accountId: "ops",
|
|
homeserver: "https://matrix.example.org",
|
|
userId: "@bot:example.org",
|
|
accessToken: "token",
|
|
} as const;
|
|
|
|
function resetMatrixSessionBindingStateDir() {
|
|
fs.rmSync(matrixSessionBindingStateDir, { recursive: true, force: true });
|
|
fs.mkdirSync(matrixSessionBindingStateDir, { recursive: true });
|
|
}
|
|
|
|
async function createContractMatrixThreadBindingManager() {
|
|
resetMatrixSessionBindingStateDir();
|
|
setMatrixRuntime({
|
|
state: {
|
|
resolveStateDir: () => matrixSessionBindingStateDir,
|
|
},
|
|
} as never);
|
|
return await createMatrixThreadBindingManager({
|
|
accountId: matrixSessionBindingAuth.accountId,
|
|
auth: matrixSessionBindingAuth,
|
|
client: {} as never,
|
|
idleTimeoutMs: 24 * 60 * 60 * 1000,
|
|
maxAgeMs: 0,
|
|
enableSweeper: false,
|
|
});
|
|
}
|
|
|
|
export const pluginContractRegistry: PluginContractEntry[] = bundledChannelPlugins.map(
|
|
(plugin) => ({
|
|
id: plugin.id,
|
|
plugin,
|
|
}),
|
|
);
|
|
|
|
export const actionContractRegistry: ActionsContractEntry[] = [
|
|
{
|
|
id: "slack",
|
|
plugin: requireBundledChannelPlugin("slack"),
|
|
unsupportedAction: "poll",
|
|
cases: [
|
|
{
|
|
name: "configured account exposes default Slack actions",
|
|
cfg: {
|
|
channels: {
|
|
slack: {
|
|
botToken: "xoxb-test",
|
|
appToken: "xapp-test",
|
|
},
|
|
},
|
|
} as OpenClawConfig,
|
|
expectedActions: [
|
|
"send",
|
|
"react",
|
|
"reactions",
|
|
"read",
|
|
"edit",
|
|
"delete",
|
|
"download-file",
|
|
"pin",
|
|
"unpin",
|
|
"list-pins",
|
|
"member-info",
|
|
"emoji-list",
|
|
],
|
|
expectedCapabilities: ["blocks"],
|
|
},
|
|
{
|
|
name: "interactive replies add the shared interactive capability",
|
|
cfg: {
|
|
channels: {
|
|
slack: {
|
|
botToken: "xoxb-test",
|
|
appToken: "xapp-test",
|
|
capabilities: {
|
|
interactiveReplies: true,
|
|
},
|
|
},
|
|
},
|
|
} as OpenClawConfig,
|
|
expectedActions: [
|
|
"send",
|
|
"react",
|
|
"reactions",
|
|
"read",
|
|
"edit",
|
|
"delete",
|
|
"download-file",
|
|
"pin",
|
|
"unpin",
|
|
"list-pins",
|
|
"member-info",
|
|
"emoji-list",
|
|
],
|
|
expectedCapabilities: ["blocks", "interactive"],
|
|
},
|
|
{
|
|
name: "missing tokens disables the actions surface",
|
|
cfg: {
|
|
channels: {
|
|
slack: {
|
|
enabled: true,
|
|
},
|
|
},
|
|
} as OpenClawConfig,
|
|
expectedActions: [],
|
|
expectedCapabilities: [],
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: "mattermost",
|
|
plugin: requireBundledChannelPlugin("mattermost"),
|
|
unsupportedAction: "poll",
|
|
cases: [
|
|
{
|
|
name: "configured account exposes send and react",
|
|
cfg: {
|
|
channels: {
|
|
mattermost: {
|
|
enabled: true,
|
|
botToken: "test-token",
|
|
baseUrl: "https://chat.example.com",
|
|
},
|
|
},
|
|
} as OpenClawConfig,
|
|
expectedActions: ["send", "react"],
|
|
expectedCapabilities: ["buttons"],
|
|
},
|
|
{
|
|
name: "reactions can be disabled while send stays available",
|
|
cfg: {
|
|
channels: {
|
|
mattermost: {
|
|
enabled: true,
|
|
botToken: "test-token",
|
|
baseUrl: "https://chat.example.com",
|
|
actions: { reactions: false },
|
|
},
|
|
},
|
|
} as OpenClawConfig,
|
|
expectedActions: ["send"],
|
|
expectedCapabilities: ["buttons"],
|
|
},
|
|
{
|
|
name: "missing bot credentials disables the actions surface",
|
|
cfg: {
|
|
channels: {
|
|
mattermost: {
|
|
enabled: true,
|
|
},
|
|
},
|
|
} as OpenClawConfig,
|
|
expectedActions: [],
|
|
expectedCapabilities: [],
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: "telegram",
|
|
plugin: requireBundledChannelPlugin("telegram"),
|
|
cases: [
|
|
{
|
|
name: "forwards runtime-backed Telegram actions and capabilities",
|
|
cfg: {} as OpenClawConfig,
|
|
expectedActions: ["send", "poll", "react"],
|
|
expectedCapabilities: ["interactive", "buttons"],
|
|
beforeTest: () => {
|
|
telegramListActionsMock.mockReset();
|
|
telegramGetCapabilitiesMock.mockReset();
|
|
telegramListActionsMock.mockReturnValue(["send", "poll", "react"]);
|
|
telegramGetCapabilitiesMock.mockReturnValue(["interactive", "buttons"]);
|
|
},
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: "discord",
|
|
plugin: requireBundledChannelPlugin("discord"),
|
|
cases: [
|
|
{
|
|
name: "forwards runtime-backed Discord actions and capabilities",
|
|
cfg: {} as OpenClawConfig,
|
|
expectedActions: ["send", "react", "poll"],
|
|
expectedCapabilities: ["interactive", "components"],
|
|
beforeTest: () => {
|
|
discordListActionsMock.mockReset();
|
|
discordGetCapabilitiesMock.mockReset();
|
|
discordListActionsMock.mockReturnValue(["send", "react", "poll"]);
|
|
discordGetCapabilitiesMock.mockReturnValue(["interactive", "components"]);
|
|
},
|
|
},
|
|
],
|
|
},
|
|
];
|
|
|
|
export const setupContractRegistry: SetupContractEntry[] = [
|
|
{
|
|
id: "slack",
|
|
plugin: requireBundledChannelPlugin("slack"),
|
|
cases: [
|
|
{
|
|
name: "default account stores tokens and enables the channel",
|
|
cfg: {} as OpenClawConfig,
|
|
input: {
|
|
botToken: "xoxb-test",
|
|
appToken: "xapp-test",
|
|
},
|
|
expectedAccountId: "default",
|
|
assertPatchedConfig: (cfg) => {
|
|
expect(cfg.channels?.slack?.enabled).toBe(true);
|
|
expect(cfg.channels?.slack?.botToken).toBe("xoxb-test");
|
|
expect(cfg.channels?.slack?.appToken).toBe("xapp-test");
|
|
},
|
|
},
|
|
{
|
|
name: "non-default env setup is rejected",
|
|
cfg: {} as OpenClawConfig,
|
|
accountId: "ops",
|
|
input: {
|
|
useEnv: true,
|
|
},
|
|
expectedAccountId: "ops",
|
|
expectedValidation: "Slack env tokens can only be used for the default account.",
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: "mattermost",
|
|
plugin: requireBundledChannelPlugin("mattermost"),
|
|
cases: [
|
|
{
|
|
name: "default account stores token and normalized base URL",
|
|
cfg: {} as OpenClawConfig,
|
|
input: {
|
|
botToken: "test-token",
|
|
httpUrl: "https://chat.example.com/",
|
|
},
|
|
expectedAccountId: "default",
|
|
assertPatchedConfig: (cfg) => {
|
|
expect(cfg.channels?.mattermost?.enabled).toBe(true);
|
|
expect(cfg.channels?.mattermost?.botToken).toBe("test-token");
|
|
expect(cfg.channels?.mattermost?.baseUrl).toBe("https://chat.example.com");
|
|
},
|
|
},
|
|
{
|
|
name: "missing credentials are rejected",
|
|
cfg: {} as OpenClawConfig,
|
|
input: {
|
|
httpUrl: "",
|
|
},
|
|
expectedAccountId: "default",
|
|
expectedValidation: "Mattermost requires --bot-token and --http-url (or --use-env).",
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: "line",
|
|
plugin: requireBundledChannelPlugin("line"),
|
|
cases: [
|
|
{
|
|
name: "default account stores token and secret",
|
|
cfg: {} as OpenClawConfig,
|
|
input: {
|
|
channelAccessToken: "line-token",
|
|
channelSecret: "line-secret",
|
|
},
|
|
expectedAccountId: "default",
|
|
assertPatchedConfig: (cfg) => {
|
|
expect(cfg.channels?.line?.enabled).toBe(true);
|
|
expect(cfg.channels?.line?.channelAccessToken).toBe("line-token");
|
|
expect(cfg.channels?.line?.channelSecret).toBe("line-secret");
|
|
},
|
|
},
|
|
{
|
|
name: "non-default env setup is rejected",
|
|
cfg: {} as OpenClawConfig,
|
|
accountId: "ops",
|
|
input: {
|
|
useEnv: true,
|
|
},
|
|
expectedAccountId: "ops",
|
|
expectedValidation: "LINE_CHANNEL_ACCESS_TOKEN can only be used for the default account.",
|
|
},
|
|
],
|
|
},
|
|
];
|
|
|
|
export const statusContractRegistry: StatusContractEntry[] = [
|
|
{
|
|
id: "slack",
|
|
plugin: requireBundledChannelPlugin("slack"),
|
|
cases: [
|
|
{
|
|
name: "configured account produces a configured status snapshot",
|
|
cfg: {
|
|
channels: {
|
|
slack: {
|
|
botToken: "xoxb-test",
|
|
appToken: "xapp-test",
|
|
},
|
|
},
|
|
} as OpenClawConfig,
|
|
runtime: {
|
|
accountId: "default",
|
|
connected: true,
|
|
running: true,
|
|
},
|
|
probe: { ok: true },
|
|
assertSnapshot: (snapshot) => {
|
|
expect(snapshot.accountId).toBe("default");
|
|
expect(snapshot.enabled).toBe(true);
|
|
expect(snapshot.configured).toBe(true);
|
|
},
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: "mattermost",
|
|
plugin: requireBundledChannelPlugin("mattermost"),
|
|
cases: [
|
|
{
|
|
name: "configured account preserves connectivity details in the snapshot",
|
|
cfg: {
|
|
channels: {
|
|
mattermost: {
|
|
enabled: true,
|
|
botToken: "test-token",
|
|
baseUrl: "https://chat.example.com",
|
|
},
|
|
},
|
|
} as OpenClawConfig,
|
|
runtime: {
|
|
accountId: "default",
|
|
connected: true,
|
|
lastConnectedAt: 1234,
|
|
},
|
|
probe: { ok: true },
|
|
assertSnapshot: (snapshot) => {
|
|
expect(snapshot.accountId).toBe("default");
|
|
expect(snapshot.enabled).toBe(true);
|
|
expect(snapshot.configured).toBe(true);
|
|
expect(snapshot.connected).toBe(true);
|
|
expect(snapshot.baseUrl).toBe("https://chat.example.com");
|
|
},
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: "line",
|
|
plugin: requireBundledChannelPlugin("line"),
|
|
cases: [
|
|
{
|
|
name: "configured account produces a webhook status snapshot",
|
|
cfg: {
|
|
channels: {
|
|
line: {
|
|
enabled: true,
|
|
channelAccessToken: "line-token",
|
|
channelSecret: "line-secret",
|
|
},
|
|
},
|
|
} as OpenClawConfig,
|
|
runtime: {
|
|
accountId: "default",
|
|
running: true,
|
|
},
|
|
probe: { ok: true },
|
|
assertSnapshot: (snapshot) => {
|
|
expect(snapshot.accountId).toBe("default");
|
|
expect(snapshot.enabled).toBe(true);
|
|
expect(snapshot.configured).toBe(true);
|
|
expect(snapshot.mode).toBe("webhook");
|
|
},
|
|
},
|
|
],
|
|
},
|
|
];
|
|
|
|
export const surfaceContractRegistry: SurfaceContractEntry[] = bundledChannelPlugins.map(
|
|
(plugin) => ({
|
|
id: plugin.id,
|
|
plugin,
|
|
surfaces: channelPluginSurfaceKeys.filter((surface) => Boolean(plugin[surface])),
|
|
}),
|
|
);
|
|
|
|
export const threadingContractRegistry: ThreadingContractEntry[] = surfaceContractRegistry
|
|
.filter((entry) => entry.surfaces.includes("threading"))
|
|
.map((entry) => ({
|
|
id: entry.id,
|
|
plugin: entry.plugin,
|
|
}));
|
|
|
|
const directoryPresenceOnlyIds = new Set(["whatsapp", "zalouser"]);
|
|
|
|
export const directoryContractRegistry: DirectoryContractEntry[] = surfaceContractRegistry
|
|
.filter((entry) => entry.surfaces.includes("directory"))
|
|
.map((entry) => ({
|
|
id: entry.id,
|
|
plugin: entry.plugin,
|
|
coverage: directoryPresenceOnlyIds.has(entry.id) ? "presence" : "lookups",
|
|
}));
|
|
|
|
const baseSessionBindingCfg = {
|
|
session: { mainKey: "main", scope: "per-sender" },
|
|
} satisfies OpenClawConfig;
|
|
|
|
export const sessionBindingContractRegistry: SessionBindingContractEntry[] = [
|
|
{
|
|
id: "discord",
|
|
expectedCapabilities: {
|
|
adapterAvailable: true,
|
|
bindSupported: true,
|
|
unbindSupported: true,
|
|
placements: ["current", "child"],
|
|
},
|
|
getCapabilities: () => {
|
|
createDiscordThreadBindingManager({
|
|
accountId: "default",
|
|
persist: false,
|
|
enableSweeper: false,
|
|
});
|
|
return getSessionBindingService().getCapabilities({
|
|
channel: "discord",
|
|
accountId: "default",
|
|
});
|
|
},
|
|
bindAndResolve: async () => {
|
|
createDiscordThreadBindingManager({
|
|
accountId: "default",
|
|
persist: false,
|
|
enableSweeper: false,
|
|
});
|
|
const service = getSessionBindingService();
|
|
const binding = await service.bind({
|
|
targetSessionKey: "agent:discord:child:thread-1",
|
|
targetKind: "subagent",
|
|
conversation: {
|
|
channel: "discord",
|
|
accountId: "default",
|
|
conversationId: "channel:123456789012345678",
|
|
},
|
|
placement: "current",
|
|
metadata: {
|
|
label: "codex-discord",
|
|
},
|
|
});
|
|
expectResolvedSessionBinding({
|
|
channel: "discord",
|
|
accountId: "default",
|
|
conversationId: "channel:123456789012345678",
|
|
targetSessionKey: "agent:discord:child:thread-1",
|
|
});
|
|
return binding;
|
|
},
|
|
unbindAndVerify: unbindAndExpectClearedSessionBinding,
|
|
cleanup: async () => {
|
|
const manager = createDiscordThreadBindingManager({
|
|
accountId: "default",
|
|
persist: false,
|
|
enableSweeper: false,
|
|
});
|
|
manager.stop();
|
|
discordThreadBindingTesting.resetThreadBindingsForTests();
|
|
expectClearedSessionBinding({
|
|
channel: "discord",
|
|
accountId: "default",
|
|
conversationId: "channel:123456789012345678",
|
|
});
|
|
},
|
|
},
|
|
{
|
|
id: "feishu",
|
|
expectedCapabilities: {
|
|
adapterAvailable: true,
|
|
bindSupported: true,
|
|
unbindSupported: true,
|
|
placements: ["current"],
|
|
},
|
|
getCapabilities: () => {
|
|
createFeishuThreadBindingManager({ cfg: baseSessionBindingCfg, accountId: "default" });
|
|
return getSessionBindingService().getCapabilities({
|
|
channel: "feishu",
|
|
accountId: "default",
|
|
});
|
|
},
|
|
bindAndResolve: async () => {
|
|
createFeishuThreadBindingManager({ cfg: baseSessionBindingCfg, accountId: "default" });
|
|
const service = getSessionBindingService();
|
|
const binding = await service.bind({
|
|
targetSessionKey: "agent:codex:acp:binding:feishu:default:abc123",
|
|
targetKind: "session",
|
|
conversation: {
|
|
channel: "feishu",
|
|
accountId: "default",
|
|
conversationId: "oc_group_chat:topic:om_topic_root",
|
|
parentConversationId: "oc_group_chat",
|
|
},
|
|
placement: "current",
|
|
metadata: {
|
|
agentId: "codex",
|
|
label: "codex-main",
|
|
},
|
|
});
|
|
expectResolvedSessionBinding({
|
|
channel: "feishu",
|
|
accountId: "default",
|
|
conversationId: "oc_group_chat:topic:om_topic_root",
|
|
targetSessionKey: "agent:codex:acp:binding:feishu:default:abc123",
|
|
});
|
|
return binding;
|
|
},
|
|
unbindAndVerify: unbindAndExpectClearedSessionBinding,
|
|
cleanup: async () => {
|
|
const manager = createFeishuThreadBindingManager({
|
|
cfg: baseSessionBindingCfg,
|
|
accountId: "default",
|
|
});
|
|
manager.stop();
|
|
expectClearedSessionBinding({
|
|
channel: "feishu",
|
|
accountId: "default",
|
|
conversationId: "oc_group_chat:topic:om_topic_root",
|
|
});
|
|
},
|
|
},
|
|
{
|
|
id: "matrix",
|
|
expectedCapabilities: {
|
|
adapterAvailable: true,
|
|
bindSupported: true,
|
|
unbindSupported: true,
|
|
placements: ["current", "child"],
|
|
},
|
|
getCapabilities: async () => {
|
|
await createContractMatrixThreadBindingManager();
|
|
return getSessionBindingService().getCapabilities({
|
|
channel: "matrix",
|
|
accountId: matrixSessionBindingAuth.accountId,
|
|
});
|
|
},
|
|
bindAndResolve: async () => {
|
|
await createContractMatrixThreadBindingManager();
|
|
const service = getSessionBindingService();
|
|
const binding = await service.bind({
|
|
targetSessionKey: "agent:matrix:child:thread-1",
|
|
targetKind: "subagent",
|
|
conversation: {
|
|
channel: "matrix",
|
|
accountId: matrixSessionBindingAuth.accountId,
|
|
conversationId: "$thread",
|
|
parentConversationId: "!room:example",
|
|
},
|
|
placement: "current",
|
|
metadata: {
|
|
label: "codex-matrix",
|
|
},
|
|
});
|
|
expectResolvedSessionBinding({
|
|
channel: "matrix",
|
|
accountId: matrixSessionBindingAuth.accountId,
|
|
conversationId: "$thread",
|
|
targetSessionKey: "agent:matrix:child:thread-1",
|
|
});
|
|
return binding;
|
|
},
|
|
unbindAndVerify: unbindAndExpectClearedSessionBinding,
|
|
cleanup: async () => {
|
|
resetMatrixThreadBindingsForTests();
|
|
resetMatrixSessionBindingStateDir();
|
|
expectClearedSessionBinding({
|
|
channel: "matrix",
|
|
accountId: matrixSessionBindingAuth.accountId,
|
|
conversationId: "$thread",
|
|
});
|
|
},
|
|
},
|
|
{
|
|
id: "telegram",
|
|
expectedCapabilities: {
|
|
adapterAvailable: true,
|
|
bindSupported: true,
|
|
unbindSupported: true,
|
|
placements: ["current"],
|
|
},
|
|
getCapabilities: () => {
|
|
createTelegramThreadBindingManager({
|
|
accountId: "default",
|
|
persist: false,
|
|
enableSweeper: false,
|
|
});
|
|
return getSessionBindingService().getCapabilities({
|
|
channel: "telegram",
|
|
accountId: "default",
|
|
});
|
|
},
|
|
bindAndResolve: async () => {
|
|
createTelegramThreadBindingManager({
|
|
accountId: "default",
|
|
persist: false,
|
|
enableSweeper: false,
|
|
});
|
|
const service = getSessionBindingService();
|
|
const binding = await service.bind({
|
|
targetSessionKey: "agent:main:subagent:child-1",
|
|
targetKind: "subagent",
|
|
conversation: {
|
|
channel: "telegram",
|
|
accountId: "default",
|
|
conversationId: "-100200300:topic:77",
|
|
},
|
|
placement: "current",
|
|
metadata: {
|
|
boundBy: "user-1",
|
|
},
|
|
});
|
|
expectResolvedSessionBinding({
|
|
channel: "telegram",
|
|
accountId: "default",
|
|
conversationId: "-100200300:topic:77",
|
|
targetSessionKey: "agent:main:subagent:child-1",
|
|
});
|
|
return binding;
|
|
},
|
|
unbindAndVerify: unbindAndExpectClearedSessionBinding,
|
|
cleanup: async () => {
|
|
const manager = createTelegramThreadBindingManager({
|
|
accountId: "default",
|
|
persist: false,
|
|
enableSweeper: false,
|
|
});
|
|
manager.stop();
|
|
expectClearedSessionBinding({
|
|
channel: "telegram",
|
|
accountId: "default",
|
|
conversationId: "-100200300:topic:77",
|
|
});
|
|
},
|
|
},
|
|
];
|