mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-06 06:41:08 +00:00
test(contracts): split session binding registry seams
This commit is contained in:
@@ -1,12 +1,8 @@
|
||||
import {
|
||||
describeChannelRegistryBackedContracts,
|
||||
describeSessionBindingRegistryBackedContract,
|
||||
} from "../../../../test/helpers/channels/registry-backed-contract.js";
|
||||
import { describeChannelRegistryBackedContracts } from "../../../../test/helpers/channels/registry-backed-contract.js";
|
||||
import {
|
||||
actionContractRegistry,
|
||||
directoryContractRegistry,
|
||||
pluginContractRegistry,
|
||||
sessionBindingContractRegistry,
|
||||
setupContractRegistry,
|
||||
statusContractRegistry,
|
||||
surfaceContractRegistry,
|
||||
@@ -26,7 +22,3 @@ const registryIds = new Set<string>([
|
||||
for (const id of [...registryIds].toSorted()) {
|
||||
describeChannelRegistryBackedContracts(id);
|
||||
}
|
||||
|
||||
for (const entry of sessionBindingContractRegistry) {
|
||||
describeSessionBindingRegistryBackedContract(entry.id);
|
||||
}
|
||||
|
||||
503
src/channels/plugins/contracts/registry-session-binding.ts
Normal file
503
src/channels/plugins/contracts/registry-session-binding.ts
Normal file
@@ -0,0 +1,503 @@
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { expect } from "vitest";
|
||||
import type { OpenClawConfig } from "../../../config/config.js";
|
||||
import {
|
||||
getSessionBindingService,
|
||||
type SessionBindingCapabilities,
|
||||
type SessionBindingRecord,
|
||||
} from "../../../infra/outbound/session-binding-service.js";
|
||||
import { createChannelConversationBindingManager } from "../conversation-bindings.js";
|
||||
import {
|
||||
sessionBindingContractChannelIds,
|
||||
type SessionBindingContractChannelId,
|
||||
} from "./manifest.js";
|
||||
import "./registry.js";
|
||||
|
||||
type SessionBindingContractEntry = {
|
||||
id: string;
|
||||
expectedCapabilities: SessionBindingCapabilities;
|
||||
getCapabilities: () => SessionBindingCapabilities | Promise<SessionBindingCapabilities>;
|
||||
bindAndResolve: () => Promise<SessionBindingRecord>;
|
||||
unbindAndVerify: (binding: SessionBindingRecord) => Promise<void>;
|
||||
cleanup: () => Promise<void> | void;
|
||||
};
|
||||
let discordRuntimeApiPromise:
|
||||
| Promise<typeof import("../../../../extensions/discord/runtime-api.js")>
|
||||
| undefined;
|
||||
let feishuApiPromise: Promise<typeof import("../../../../extensions/feishu/api.js")> | undefined;
|
||||
let matrixApiPromise: Promise<typeof import("../../../../extensions/matrix/api.js")> | undefined;
|
||||
let matrixRuntimeApiPromise:
|
||||
| Promise<typeof import("../../../../extensions/matrix/runtime-api.js")>
|
||||
| undefined;
|
||||
|
||||
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;
|
||||
|
||||
async function getDiscordRuntimeApi() {
|
||||
discordRuntimeApiPromise ??= import("../../../../extensions/discord/runtime-api.js");
|
||||
return await discordRuntimeApiPromise;
|
||||
}
|
||||
|
||||
async function getFeishuApi() {
|
||||
feishuApiPromise ??= import("../../../../extensions/feishu/api.js");
|
||||
return await feishuApiPromise;
|
||||
}
|
||||
|
||||
async function getMatrixApi() {
|
||||
matrixApiPromise ??= import("../../../../extensions/matrix/api.js");
|
||||
return await matrixApiPromise;
|
||||
}
|
||||
|
||||
async function getMatrixRuntimeApi() {
|
||||
matrixRuntimeApiPromise ??= import("../../../../extensions/matrix/runtime-api.js");
|
||||
return await matrixRuntimeApiPromise;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
function resetMatrixSessionBindingStateDir() {
|
||||
fs.rmSync(matrixSessionBindingStateDir, { recursive: true, force: true });
|
||||
fs.mkdirSync(matrixSessionBindingStateDir, { recursive: true });
|
||||
}
|
||||
|
||||
async function createContractMatrixThreadBindingManager() {
|
||||
resetMatrixSessionBindingStateDir();
|
||||
const { setMatrixRuntime } = await getMatrixRuntimeApi();
|
||||
const { createMatrixThreadBindingManager } = await getMatrixApi();
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
const baseSessionBindingCfg = {
|
||||
session: { mainKey: "main", scope: "per-sender" },
|
||||
} satisfies OpenClawConfig;
|
||||
|
||||
const sessionBindingContractEntries: Record<
|
||||
SessionBindingContractChannelId,
|
||||
Omit<SessionBindingContractEntry, "id">
|
||||
> = {
|
||||
bluebubbles: {
|
||||
expectedCapabilities: {
|
||||
adapterAvailable: true,
|
||||
bindSupported: true,
|
||||
unbindSupported: true,
|
||||
placements: ["current"],
|
||||
},
|
||||
getCapabilities: () => {
|
||||
void createChannelConversationBindingManager({
|
||||
channelId: "bluebubbles",
|
||||
cfg: baseSessionBindingCfg,
|
||||
accountId: "default",
|
||||
});
|
||||
return getSessionBindingService().getCapabilities({
|
||||
channel: "bluebubbles",
|
||||
accountId: "default",
|
||||
});
|
||||
},
|
||||
bindAndResolve: async () => {
|
||||
await createChannelConversationBindingManager({
|
||||
channelId: "bluebubbles",
|
||||
cfg: baseSessionBindingCfg,
|
||||
accountId: "default",
|
||||
});
|
||||
const service = getSessionBindingService();
|
||||
const binding = await service.bind({
|
||||
targetSessionKey: "agent:codex:acp:binding:bluebubbles:default:abc123",
|
||||
targetKind: "session",
|
||||
conversation: {
|
||||
channel: "bluebubbles",
|
||||
accountId: "default",
|
||||
conversationId: "+15555550123",
|
||||
},
|
||||
placement: "current",
|
||||
metadata: {
|
||||
agentId: "codex",
|
||||
label: "codex-main",
|
||||
},
|
||||
});
|
||||
expectResolvedSessionBinding({
|
||||
channel: "bluebubbles",
|
||||
accountId: "default",
|
||||
conversationId: "+15555550123",
|
||||
targetSessionKey: "agent:codex:acp:binding:bluebubbles:default:abc123",
|
||||
});
|
||||
return binding;
|
||||
},
|
||||
unbindAndVerify: unbindAndExpectClearedSessionBinding,
|
||||
cleanup: async () => {
|
||||
const manager = await createChannelConversationBindingManager({
|
||||
channelId: "bluebubbles",
|
||||
cfg: baseSessionBindingCfg,
|
||||
accountId: "default",
|
||||
});
|
||||
await manager?.stop();
|
||||
expectClearedSessionBinding({
|
||||
channel: "bluebubbles",
|
||||
accountId: "default",
|
||||
conversationId: "+15555550123",
|
||||
});
|
||||
},
|
||||
},
|
||||
discord: {
|
||||
expectedCapabilities: {
|
||||
adapterAvailable: true,
|
||||
bindSupported: true,
|
||||
unbindSupported: true,
|
||||
placements: ["current", "child"],
|
||||
},
|
||||
getCapabilities: async () => {
|
||||
const { createThreadBindingManager } = await getDiscordRuntimeApi();
|
||||
createThreadBindingManager({
|
||||
accountId: "default",
|
||||
persist: false,
|
||||
enableSweeper: false,
|
||||
});
|
||||
return getSessionBindingService().getCapabilities({
|
||||
channel: "discord",
|
||||
accountId: "default",
|
||||
});
|
||||
},
|
||||
bindAndResolve: async () => {
|
||||
const { createThreadBindingManager } = await getDiscordRuntimeApi();
|
||||
createThreadBindingManager({
|
||||
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 { createThreadBindingManager } = await getDiscordRuntimeApi();
|
||||
const manager = createThreadBindingManager({
|
||||
accountId: "default",
|
||||
persist: false,
|
||||
enableSweeper: false,
|
||||
});
|
||||
manager.stop();
|
||||
expectClearedSessionBinding({
|
||||
channel: "discord",
|
||||
accountId: "default",
|
||||
conversationId: "channel:123456789012345678",
|
||||
});
|
||||
},
|
||||
},
|
||||
feishu: {
|
||||
expectedCapabilities: {
|
||||
adapterAvailable: true,
|
||||
bindSupported: true,
|
||||
unbindSupported: true,
|
||||
placements: ["current"],
|
||||
},
|
||||
getCapabilities: async () => {
|
||||
const { createFeishuThreadBindingManager } = await getFeishuApi();
|
||||
createFeishuThreadBindingManager({ cfg: baseSessionBindingCfg, accountId: "default" });
|
||||
return getSessionBindingService().getCapabilities({
|
||||
channel: "feishu",
|
||||
accountId: "default",
|
||||
});
|
||||
},
|
||||
bindAndResolve: async () => {
|
||||
const { createFeishuThreadBindingManager } = await getFeishuApi();
|
||||
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 { createFeishuThreadBindingManager } = await getFeishuApi();
|
||||
const manager = createFeishuThreadBindingManager({
|
||||
cfg: baseSessionBindingCfg,
|
||||
accountId: "default",
|
||||
});
|
||||
manager.stop();
|
||||
expectClearedSessionBinding({
|
||||
channel: "feishu",
|
||||
accountId: "default",
|
||||
conversationId: "oc_group_chat:topic:om_topic_root",
|
||||
});
|
||||
},
|
||||
},
|
||||
imessage: {
|
||||
expectedCapabilities: {
|
||||
adapterAvailable: true,
|
||||
bindSupported: true,
|
||||
unbindSupported: true,
|
||||
placements: ["current"],
|
||||
},
|
||||
getCapabilities: () => {
|
||||
void createChannelConversationBindingManager({
|
||||
channelId: "imessage",
|
||||
cfg: baseSessionBindingCfg,
|
||||
accountId: "default",
|
||||
});
|
||||
return getSessionBindingService().getCapabilities({
|
||||
channel: "imessage",
|
||||
accountId: "default",
|
||||
});
|
||||
},
|
||||
bindAndResolve: async () => {
|
||||
await createChannelConversationBindingManager({
|
||||
channelId: "imessage",
|
||||
cfg: baseSessionBindingCfg,
|
||||
accountId: "default",
|
||||
});
|
||||
const service = getSessionBindingService();
|
||||
const binding = await service.bind({
|
||||
targetSessionKey: "agent:codex:acp:binding:imessage:default:abc123",
|
||||
targetKind: "session",
|
||||
conversation: {
|
||||
channel: "imessage",
|
||||
accountId: "default",
|
||||
conversationId: "+15555550123",
|
||||
},
|
||||
placement: "current",
|
||||
metadata: {
|
||||
agentId: "codex",
|
||||
label: "codex-main",
|
||||
},
|
||||
});
|
||||
expectResolvedSessionBinding({
|
||||
channel: "imessage",
|
||||
accountId: "default",
|
||||
conversationId: "+15555550123",
|
||||
targetSessionKey: "agent:codex:acp:binding:imessage:default:abc123",
|
||||
});
|
||||
return binding;
|
||||
},
|
||||
unbindAndVerify: unbindAndExpectClearedSessionBinding,
|
||||
cleanup: async () => {
|
||||
const manager = await createChannelConversationBindingManager({
|
||||
channelId: "imessage",
|
||||
cfg: baseSessionBindingCfg,
|
||||
accountId: "default",
|
||||
});
|
||||
await manager?.stop();
|
||||
expectClearedSessionBinding({
|
||||
channel: "imessage",
|
||||
accountId: "default",
|
||||
conversationId: "+15555550123",
|
||||
});
|
||||
},
|
||||
},
|
||||
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 () => {
|
||||
const { resetMatrixThreadBindingsForTests } = await getMatrixApi();
|
||||
resetMatrixThreadBindingsForTests();
|
||||
resetMatrixSessionBindingStateDir();
|
||||
expectClearedSessionBinding({
|
||||
channel: "matrix",
|
||||
accountId: matrixSessionBindingAuth.accountId,
|
||||
conversationId: "$thread",
|
||||
});
|
||||
},
|
||||
},
|
||||
telegram: {
|
||||
expectedCapabilities: {
|
||||
adapterAvailable: true,
|
||||
bindSupported: true,
|
||||
unbindSupported: true,
|
||||
placements: ["current", "child"],
|
||||
},
|
||||
getCapabilities: () => {
|
||||
void createChannelConversationBindingManager({
|
||||
channelId: "telegram",
|
||||
cfg: baseSessionBindingCfg,
|
||||
accountId: "default",
|
||||
});
|
||||
return getSessionBindingService().getCapabilities({
|
||||
channel: "telegram",
|
||||
accountId: "default",
|
||||
});
|
||||
},
|
||||
bindAndResolve: async () => {
|
||||
await createChannelConversationBindingManager({
|
||||
channelId: "telegram",
|
||||
cfg: baseSessionBindingCfg,
|
||||
accountId: "default",
|
||||
});
|
||||
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 = await createChannelConversationBindingManager({
|
||||
channelId: "telegram",
|
||||
cfg: baseSessionBindingCfg,
|
||||
accountId: "default",
|
||||
});
|
||||
await manager?.stop();
|
||||
expectClearedSessionBinding({
|
||||
channel: "telegram",
|
||||
accountId: "default",
|
||||
conversationId: "-100200300:topic:77",
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const sessionBindingContractRegistry: SessionBindingContractEntry[] =
|
||||
sessionBindingContractChannelIds.map((id) => ({
|
||||
id,
|
||||
...sessionBindingContractEntries[id],
|
||||
}));
|
||||
@@ -1,76 +1,17 @@
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { expect, vi } from "vitest";
|
||||
import type { OpenClawConfig } from "../../../config/config.js";
|
||||
import {
|
||||
getSessionBindingService,
|
||||
type SessionBindingCapabilities,
|
||||
type SessionBindingRecord,
|
||||
} from "../../../infra/outbound/session-binding-service.js";
|
||||
import {
|
||||
listLineAccountIds,
|
||||
resolveDefaultLineAccountId,
|
||||
resolveLineAccount,
|
||||
} from "../../../plugin-sdk/line.js";
|
||||
import { loadBundledPluginTestApiSync } from "../../../test-utils/bundled-plugin-public-surface.js";
|
||||
import {
|
||||
listBundledChannelPlugins,
|
||||
requireBundledChannelPlugin,
|
||||
setBundledChannelRuntime,
|
||||
} from "../bundled.js";
|
||||
import { createChannelConversationBindingManager } from "../conversation-bindings.js";
|
||||
import type { ChannelPlugin } from "../types.js";
|
||||
import {
|
||||
channelPluginSurfaceKeys,
|
||||
type ChannelPluginSurface,
|
||||
sessionBindingContractChannelIds,
|
||||
type SessionBindingContractChannelId,
|
||||
} from "./manifest.js";
|
||||
|
||||
type DiscordThreadBindingTesting = {
|
||||
resetThreadBindingsForTests: () => void;
|
||||
};
|
||||
|
||||
let discordThreadBindingTestingCache: DiscordThreadBindingTesting | undefined;
|
||||
let discordRuntimeApiPromise:
|
||||
| Promise<typeof import("../../../../extensions/discord/runtime-api.js")>
|
||||
| undefined;
|
||||
let feishuApiPromise: Promise<typeof import("../../../../extensions/feishu/api.js")> | undefined;
|
||||
let matrixApiPromise: Promise<typeof import("../../../../extensions/matrix/api.js")> | undefined;
|
||||
let matrixRuntimeApiPromise:
|
||||
| Promise<typeof import("../../../../extensions/matrix/runtime-api.js")>
|
||||
| undefined;
|
||||
|
||||
function getDiscordThreadBindingTesting(): DiscordThreadBindingTesting {
|
||||
if (!discordThreadBindingTestingCache) {
|
||||
({ discordThreadBindingTesting: discordThreadBindingTestingCache } =
|
||||
loadBundledPluginTestApiSync<{
|
||||
discordThreadBindingTesting: DiscordThreadBindingTesting;
|
||||
}>("discord"));
|
||||
}
|
||||
return discordThreadBindingTestingCache;
|
||||
}
|
||||
|
||||
async function getDiscordRuntimeApi() {
|
||||
discordRuntimeApiPromise ??= import("../../../../extensions/discord/runtime-api.js");
|
||||
return await discordRuntimeApiPromise;
|
||||
}
|
||||
|
||||
async function getFeishuApi() {
|
||||
feishuApiPromise ??= import("../../../../extensions/feishu/api.js");
|
||||
return await feishuApiPromise;
|
||||
}
|
||||
|
||||
async function getMatrixApi() {
|
||||
matrixApiPromise ??= import("../../../../extensions/matrix/api.js");
|
||||
return await matrixApiPromise;
|
||||
}
|
||||
|
||||
async function getMatrixRuntimeApi() {
|
||||
matrixRuntimeApiPromise ??= import("../../../../extensions/matrix/runtime-api.js");
|
||||
return await matrixRuntimeApiPromise;
|
||||
}
|
||||
import { channelPluginSurfaceKeys, type ChannelPluginSurface } from "./manifest.js";
|
||||
|
||||
function buildBundledPluginModuleId(pluginId: string, artifactBasename: string): string {
|
||||
return ["..", "..", "..", "..", "extensions", pluginId, artifactBasename].join("/");
|
||||
@@ -155,58 +96,6 @@ type DirectoryContractEntry = {
|
||||
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 sendMessageMatrixMock = vi.hoisted(() =>
|
||||
vi.fn(async (to: string, _message: string, opts?: { threadId?: string }) => ({
|
||||
messageId: opts?.threadId ? "$matrix-thread" : "$matrix-root",
|
||||
@@ -234,40 +123,6 @@ vi.mock(buildBundledPluginModuleId("matrix", "runtime-api.js"), async () => {
|
||||
};
|
||||
});
|
||||
|
||||
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();
|
||||
const { setMatrixRuntime } = await getMatrixRuntimeApi();
|
||||
const { createMatrixThreadBindingManager } = await getMatrixApi();
|
||||
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[] = listBundledChannelPlugins().map(
|
||||
(plugin) => ({
|
||||
id: plugin.id,
|
||||
@@ -666,377 +521,3 @@ export const directoryContractRegistry: DirectoryContractEntry[] = surfaceContra
|
||||
plugin: entry.plugin,
|
||||
coverage: directoryPresenceOnlyIds.has(entry.id) ? "presence" : "lookups",
|
||||
}));
|
||||
|
||||
const baseSessionBindingCfg = {
|
||||
session: { mainKey: "main", scope: "per-sender" },
|
||||
} satisfies OpenClawConfig;
|
||||
|
||||
const sessionBindingContractEntries: Record<
|
||||
SessionBindingContractChannelId,
|
||||
Omit<SessionBindingContractEntry, "id">
|
||||
> = {
|
||||
bluebubbles: {
|
||||
expectedCapabilities: {
|
||||
adapterAvailable: true,
|
||||
bindSupported: true,
|
||||
unbindSupported: true,
|
||||
placements: ["current"],
|
||||
},
|
||||
getCapabilities: () => {
|
||||
void createChannelConversationBindingManager({
|
||||
channelId: "bluebubbles",
|
||||
cfg: baseSessionBindingCfg,
|
||||
accountId: "default",
|
||||
});
|
||||
return getSessionBindingService().getCapabilities({
|
||||
channel: "bluebubbles",
|
||||
accountId: "default",
|
||||
});
|
||||
},
|
||||
bindAndResolve: async () => {
|
||||
await createChannelConversationBindingManager({
|
||||
channelId: "bluebubbles",
|
||||
cfg: baseSessionBindingCfg,
|
||||
accountId: "default",
|
||||
});
|
||||
const service = getSessionBindingService();
|
||||
const binding = await service.bind({
|
||||
targetSessionKey: "agent:codex:acp:binding:bluebubbles:default:abc123",
|
||||
targetKind: "session",
|
||||
conversation: {
|
||||
channel: "bluebubbles",
|
||||
accountId: "default",
|
||||
conversationId: "+15555550123",
|
||||
},
|
||||
placement: "current",
|
||||
metadata: {
|
||||
agentId: "codex",
|
||||
label: "codex-main",
|
||||
},
|
||||
});
|
||||
expectResolvedSessionBinding({
|
||||
channel: "bluebubbles",
|
||||
accountId: "default",
|
||||
conversationId: "+15555550123",
|
||||
targetSessionKey: "agent:codex:acp:binding:bluebubbles:default:abc123",
|
||||
});
|
||||
return binding;
|
||||
},
|
||||
unbindAndVerify: unbindAndExpectClearedSessionBinding,
|
||||
cleanup: async () => {
|
||||
const manager = await createChannelConversationBindingManager({
|
||||
channelId: "bluebubbles",
|
||||
cfg: baseSessionBindingCfg,
|
||||
accountId: "default",
|
||||
});
|
||||
await manager?.stop();
|
||||
expectClearedSessionBinding({
|
||||
channel: "bluebubbles",
|
||||
accountId: "default",
|
||||
conversationId: "+15555550123",
|
||||
});
|
||||
},
|
||||
},
|
||||
discord: {
|
||||
expectedCapabilities: {
|
||||
adapterAvailable: true,
|
||||
bindSupported: true,
|
||||
unbindSupported: true,
|
||||
placements: ["current", "child"],
|
||||
},
|
||||
getCapabilities: async () => {
|
||||
const { createThreadBindingManager } = await getDiscordRuntimeApi();
|
||||
createThreadBindingManager({
|
||||
accountId: "default",
|
||||
persist: false,
|
||||
enableSweeper: false,
|
||||
});
|
||||
return getSessionBindingService().getCapabilities({
|
||||
channel: "discord",
|
||||
accountId: "default",
|
||||
});
|
||||
},
|
||||
bindAndResolve: async () => {
|
||||
const { createThreadBindingManager } = await getDiscordRuntimeApi();
|
||||
createThreadBindingManager({
|
||||
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 { createThreadBindingManager } = await getDiscordRuntimeApi();
|
||||
const manager = createThreadBindingManager({
|
||||
accountId: "default",
|
||||
persist: false,
|
||||
enableSweeper: false,
|
||||
});
|
||||
manager.stop();
|
||||
getDiscordThreadBindingTesting().resetThreadBindingsForTests();
|
||||
expectClearedSessionBinding({
|
||||
channel: "discord",
|
||||
accountId: "default",
|
||||
conversationId: "channel:123456789012345678",
|
||||
});
|
||||
},
|
||||
},
|
||||
feishu: {
|
||||
expectedCapabilities: {
|
||||
adapterAvailable: true,
|
||||
bindSupported: true,
|
||||
unbindSupported: true,
|
||||
placements: ["current"],
|
||||
},
|
||||
getCapabilities: async () => {
|
||||
const { createFeishuThreadBindingManager } = await getFeishuApi();
|
||||
createFeishuThreadBindingManager({ cfg: baseSessionBindingCfg, accountId: "default" });
|
||||
return getSessionBindingService().getCapabilities({
|
||||
channel: "feishu",
|
||||
accountId: "default",
|
||||
});
|
||||
},
|
||||
bindAndResolve: async () => {
|
||||
const { createFeishuThreadBindingManager } = await getFeishuApi();
|
||||
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 { createFeishuThreadBindingManager } = await getFeishuApi();
|
||||
const manager = createFeishuThreadBindingManager({
|
||||
cfg: baseSessionBindingCfg,
|
||||
accountId: "default",
|
||||
});
|
||||
manager.stop();
|
||||
expectClearedSessionBinding({
|
||||
channel: "feishu",
|
||||
accountId: "default",
|
||||
conversationId: "oc_group_chat:topic:om_topic_root",
|
||||
});
|
||||
},
|
||||
},
|
||||
imessage: {
|
||||
expectedCapabilities: {
|
||||
adapterAvailable: true,
|
||||
bindSupported: true,
|
||||
unbindSupported: true,
|
||||
placements: ["current"],
|
||||
},
|
||||
getCapabilities: () => {
|
||||
void createChannelConversationBindingManager({
|
||||
channelId: "imessage",
|
||||
cfg: baseSessionBindingCfg,
|
||||
accountId: "default",
|
||||
});
|
||||
return getSessionBindingService().getCapabilities({
|
||||
channel: "imessage",
|
||||
accountId: "default",
|
||||
});
|
||||
},
|
||||
bindAndResolve: async () => {
|
||||
await createChannelConversationBindingManager({
|
||||
channelId: "imessage",
|
||||
cfg: baseSessionBindingCfg,
|
||||
accountId: "default",
|
||||
});
|
||||
const service = getSessionBindingService();
|
||||
const binding = await service.bind({
|
||||
targetSessionKey: "agent:codex:acp:binding:imessage:default:abc123",
|
||||
targetKind: "session",
|
||||
conversation: {
|
||||
channel: "imessage",
|
||||
accountId: "default",
|
||||
conversationId: "+15555550123",
|
||||
},
|
||||
placement: "current",
|
||||
metadata: {
|
||||
agentId: "codex",
|
||||
label: "codex-main",
|
||||
},
|
||||
});
|
||||
expectResolvedSessionBinding({
|
||||
channel: "imessage",
|
||||
accountId: "default",
|
||||
conversationId: "+15555550123",
|
||||
targetSessionKey: "agent:codex:acp:binding:imessage:default:abc123",
|
||||
});
|
||||
return binding;
|
||||
},
|
||||
unbindAndVerify: unbindAndExpectClearedSessionBinding,
|
||||
cleanup: async () => {
|
||||
const manager = await createChannelConversationBindingManager({
|
||||
channelId: "imessage",
|
||||
cfg: baseSessionBindingCfg,
|
||||
accountId: "default",
|
||||
});
|
||||
await manager?.stop();
|
||||
expectClearedSessionBinding({
|
||||
channel: "imessage",
|
||||
accountId: "default",
|
||||
conversationId: "+15555550123",
|
||||
});
|
||||
},
|
||||
},
|
||||
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 () => {
|
||||
const { resetMatrixThreadBindingsForTests } = await getMatrixApi();
|
||||
resetMatrixThreadBindingsForTests();
|
||||
resetMatrixSessionBindingStateDir();
|
||||
expectClearedSessionBinding({
|
||||
channel: "matrix",
|
||||
accountId: matrixSessionBindingAuth.accountId,
|
||||
conversationId: "$thread",
|
||||
});
|
||||
},
|
||||
},
|
||||
telegram: {
|
||||
expectedCapabilities: {
|
||||
adapterAvailable: true,
|
||||
bindSupported: true,
|
||||
unbindSupported: true,
|
||||
placements: ["current", "child"],
|
||||
},
|
||||
getCapabilities: () => {
|
||||
void createChannelConversationBindingManager({
|
||||
channelId: "telegram",
|
||||
cfg: baseSessionBindingCfg,
|
||||
accountId: "default",
|
||||
});
|
||||
return getSessionBindingService().getCapabilities({
|
||||
channel: "telegram",
|
||||
accountId: "default",
|
||||
});
|
||||
},
|
||||
bindAndResolve: async () => {
|
||||
await createChannelConversationBindingManager({
|
||||
channelId: "telegram",
|
||||
cfg: baseSessionBindingCfg,
|
||||
accountId: "default",
|
||||
});
|
||||
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 = await createChannelConversationBindingManager({
|
||||
channelId: "telegram",
|
||||
cfg: baseSessionBindingCfg,
|
||||
accountId: "default",
|
||||
});
|
||||
await manager?.stop();
|
||||
expectClearedSessionBinding({
|
||||
channel: "telegram",
|
||||
accountId: "default",
|
||||
conversationId: "-100200300:topic:77",
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const sessionBindingContractRegistry: SessionBindingContractEntry[] =
|
||||
sessionBindingContractChannelIds.map((id) => ({
|
||||
id,
|
||||
...sessionBindingContractEntries[id],
|
||||
}));
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import { describeSessionBindingRegistryBackedContract } from "../../../../test/helpers/channels/session-binding-registry-backed-contract.js";
|
||||
import { sessionBindingContractRegistry } from "./registry-session-binding.js";
|
||||
|
||||
for (const entry of sessionBindingContractRegistry) {
|
||||
describeSessionBindingRegistryBackedContract(entry.id);
|
||||
}
|
||||
@@ -1,9 +1,8 @@
|
||||
import { afterEach, beforeEach, describe } from "vitest";
|
||||
import { describe } from "vitest";
|
||||
import {
|
||||
actionContractRegistry,
|
||||
directoryContractRegistry,
|
||||
pluginContractRegistry,
|
||||
sessionBindingContractRegistry,
|
||||
setupContractRegistry,
|
||||
statusContractRegistry,
|
||||
surfaceContractRegistry,
|
||||
@@ -17,57 +16,7 @@ import {
|
||||
installChannelStatusContractSuite,
|
||||
installChannelSurfaceContractSuite,
|
||||
installChannelThreadingContractSuite,
|
||||
installSessionBindingContractSuite,
|
||||
} from "../../../src/channels/plugins/contracts/suites.js";
|
||||
import { setDefaultChannelPluginRegistryForTests } from "../../../src/commands/channel-test-registry.js";
|
||||
import {
|
||||
clearRuntimeConfigSnapshot,
|
||||
setRuntimeConfigSnapshot,
|
||||
} from "../../../src/config/config.js";
|
||||
import { __testing as sessionBindingTesting } from "../../../src/infra/outbound/session-binding-service.js";
|
||||
import { resetPluginRuntimeStateForTest } from "../../../src/plugins/runtime.js";
|
||||
import { loadBundledPluginTestApiSync } from "../../../src/test-utils/bundled-plugin-public-surface.js";
|
||||
|
||||
type DiscordThreadBindingTesting = {
|
||||
resetThreadBindingsForTests: () => void;
|
||||
};
|
||||
|
||||
let discordThreadBindingTestingCache: DiscordThreadBindingTesting | undefined;
|
||||
let feishuApiPromise: Promise<typeof import("../../../extensions/feishu/api.js")> | undefined;
|
||||
let matrixApiPromise: Promise<typeof import("../../../extensions/matrix/api.js")> | undefined;
|
||||
|
||||
function getDiscordThreadBindingTesting(): DiscordThreadBindingTesting {
|
||||
if (!discordThreadBindingTestingCache) {
|
||||
({ discordThreadBindingTesting: discordThreadBindingTestingCache } =
|
||||
loadBundledPluginTestApiSync<{
|
||||
discordThreadBindingTesting: DiscordThreadBindingTesting;
|
||||
}>("discord"));
|
||||
}
|
||||
return discordThreadBindingTestingCache;
|
||||
}
|
||||
type ResetTelegramThreadBindingsForTests = () => Promise<void>;
|
||||
|
||||
let resetTelegramThreadBindingsForTestsCache: ResetTelegramThreadBindingsForTests | undefined;
|
||||
|
||||
function getResetTelegramThreadBindingsForTests(): ResetTelegramThreadBindingsForTests {
|
||||
if (!resetTelegramThreadBindingsForTestsCache) {
|
||||
({ resetTelegramThreadBindingsForTests: resetTelegramThreadBindingsForTestsCache } =
|
||||
loadBundledPluginTestApiSync<{
|
||||
resetTelegramThreadBindingsForTests: ResetTelegramThreadBindingsForTests;
|
||||
}>("telegram"));
|
||||
}
|
||||
return resetTelegramThreadBindingsForTestsCache;
|
||||
}
|
||||
|
||||
async function getFeishuThreadBindingTesting() {
|
||||
feishuApiPromise ??= import("../../../extensions/feishu/api.js");
|
||||
return (await feishuApiPromise).feishuThreadBindingTesting;
|
||||
}
|
||||
|
||||
async function getResetMatrixThreadBindingsForTests() {
|
||||
matrixApiPromise ??= import("../../../extensions/matrix/api.js");
|
||||
return (await matrixApiPromise).resetMatrixThreadBindingsForTests;
|
||||
}
|
||||
|
||||
function hasEntries<T extends { id: string }>(
|
||||
entries: readonly T[],
|
||||
@@ -76,21 +25,6 @@ function hasEntries<T extends { id: string }>(
|
||||
return entries.some((entry) => entry.id === id);
|
||||
}
|
||||
|
||||
function resolveSessionBindingContractRuntimeConfig(id: string) {
|
||||
if (id !== "discord" && id !== "matrix") {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
plugins: {
|
||||
entries: {
|
||||
[id]: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function describeChannelRegistryBackedContracts(id: string) {
|
||||
if (hasEntries(pluginContractRegistry, id)) {
|
||||
const entry = pluginContractRegistry.find((item) => item.id === id)!;
|
||||
@@ -164,40 +98,3 @@ export function describeChannelRegistryBackedContracts(id: string) {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function describeSessionBindingRegistryBackedContract(id: string) {
|
||||
const entry = sessionBindingContractRegistry.find((item) => item.id === id);
|
||||
if (!entry) {
|
||||
throw new Error(`missing session binding contract entry for ${id}`);
|
||||
}
|
||||
|
||||
describe(`${entry.id} session binding contract`, () => {
|
||||
beforeEach(async () => {
|
||||
resetPluginRuntimeStateForTest();
|
||||
clearRuntimeConfigSnapshot();
|
||||
const runtimeConfig = resolveSessionBindingContractRuntimeConfig(entry.id);
|
||||
if (runtimeConfig) {
|
||||
// These registry-backed contract suites intentionally exercise bundled runtime facades.
|
||||
// Opt those specific plugins in so the activation boundary behaves like real runtime usage.
|
||||
setRuntimeConfigSnapshot(runtimeConfig);
|
||||
}
|
||||
setDefaultChannelPluginRegistryForTests();
|
||||
sessionBindingTesting.resetSessionBindingAdaptersForTests();
|
||||
getDiscordThreadBindingTesting().resetThreadBindingsForTests();
|
||||
(await getFeishuThreadBindingTesting()).resetFeishuThreadBindingsForTests();
|
||||
(await getResetMatrixThreadBindingsForTests())();
|
||||
await getResetTelegramThreadBindingsForTests()();
|
||||
});
|
||||
afterEach(() => {
|
||||
clearRuntimeConfigSnapshot();
|
||||
});
|
||||
|
||||
installSessionBindingContractSuite({
|
||||
expectedCapabilities: entry.expectedCapabilities,
|
||||
getCapabilities: entry.getCapabilities,
|
||||
bindAndResolve: entry.bindAndResolve,
|
||||
unbindAndVerify: entry.unbindAndVerify,
|
||||
cleanup: entry.cleanup,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
import { afterEach, beforeEach, describe } from "vitest";
|
||||
import { sessionBindingContractRegistry } from "../../../src/channels/plugins/contracts/registry-session-binding.js";
|
||||
import { installSessionBindingContractSuite } from "../../../src/channels/plugins/contracts/suites.js";
|
||||
import { setDefaultChannelPluginRegistryForTests } from "../../../src/commands/channel-test-registry.js";
|
||||
import {
|
||||
clearRuntimeConfigSnapshot,
|
||||
setRuntimeConfigSnapshot,
|
||||
} from "../../../src/config/config.js";
|
||||
import { __testing as sessionBindingTesting } from "../../../src/infra/outbound/session-binding-service.js";
|
||||
import { resetPluginRuntimeStateForTest } from "../../../src/plugins/runtime.js";
|
||||
import { loadBundledPluginTestApiSync } from "../../../src/test-utils/bundled-plugin-public-surface.js";
|
||||
|
||||
type DiscordThreadBindingTesting = {
|
||||
resetThreadBindingsForTests: () => void;
|
||||
};
|
||||
|
||||
type ResetTelegramThreadBindingsForTests = () => Promise<void>;
|
||||
|
||||
let discordThreadBindingTestingCache: DiscordThreadBindingTesting | undefined;
|
||||
let resetTelegramThreadBindingsForTestsCache: ResetTelegramThreadBindingsForTests | undefined;
|
||||
let feishuApiPromise: Promise<typeof import("../../../extensions/feishu/api.js")> | undefined;
|
||||
let matrixApiPromise: Promise<typeof import("../../../extensions/matrix/api.js")> | undefined;
|
||||
|
||||
function getDiscordThreadBindingTesting(): DiscordThreadBindingTesting {
|
||||
if (!discordThreadBindingTestingCache) {
|
||||
({ discordThreadBindingTesting: discordThreadBindingTestingCache } =
|
||||
loadBundledPluginTestApiSync<{
|
||||
discordThreadBindingTesting: DiscordThreadBindingTesting;
|
||||
}>("discord"));
|
||||
}
|
||||
return discordThreadBindingTestingCache;
|
||||
}
|
||||
|
||||
function getResetTelegramThreadBindingsForTests(): ResetTelegramThreadBindingsForTests {
|
||||
if (!resetTelegramThreadBindingsForTestsCache) {
|
||||
({ resetTelegramThreadBindingsForTests: resetTelegramThreadBindingsForTestsCache } =
|
||||
loadBundledPluginTestApiSync<{
|
||||
resetTelegramThreadBindingsForTests: ResetTelegramThreadBindingsForTests;
|
||||
}>("telegram"));
|
||||
}
|
||||
return resetTelegramThreadBindingsForTestsCache;
|
||||
}
|
||||
|
||||
async function getFeishuThreadBindingTesting() {
|
||||
feishuApiPromise ??= import("../../../extensions/feishu/api.js");
|
||||
return (await feishuApiPromise).feishuThreadBindingTesting;
|
||||
}
|
||||
|
||||
async function getResetMatrixThreadBindingsForTests() {
|
||||
matrixApiPromise ??= import("../../../extensions/matrix/api.js");
|
||||
return (await matrixApiPromise).resetMatrixThreadBindingsForTests;
|
||||
}
|
||||
|
||||
function resolveSessionBindingContractRuntimeConfig(id: string) {
|
||||
if (id !== "discord" && id !== "matrix") {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
plugins: {
|
||||
entries: {
|
||||
[id]: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function describeSessionBindingRegistryBackedContract(id: string) {
|
||||
const entry = sessionBindingContractRegistry.find((item) => item.id === id);
|
||||
if (!entry) {
|
||||
throw new Error(`missing session binding contract entry for ${id}`);
|
||||
}
|
||||
|
||||
describe(`${entry.id} session binding contract`, () => {
|
||||
beforeEach(async () => {
|
||||
resetPluginRuntimeStateForTest();
|
||||
clearRuntimeConfigSnapshot();
|
||||
const runtimeConfig = resolveSessionBindingContractRuntimeConfig(entry.id);
|
||||
if (runtimeConfig) {
|
||||
// These registry-backed contract suites intentionally exercise bundled runtime facades.
|
||||
// Opt those specific plugins in so the activation boundary behaves like real runtime usage.
|
||||
setRuntimeConfigSnapshot(runtimeConfig);
|
||||
}
|
||||
setDefaultChannelPluginRegistryForTests();
|
||||
sessionBindingTesting.resetSessionBindingAdaptersForTests();
|
||||
getDiscordThreadBindingTesting().resetThreadBindingsForTests();
|
||||
(await getFeishuThreadBindingTesting()).resetFeishuThreadBindingsForTests();
|
||||
(await getResetMatrixThreadBindingsForTests())();
|
||||
await getResetTelegramThreadBindingsForTests()();
|
||||
});
|
||||
afterEach(() => {
|
||||
clearRuntimeConfigSnapshot();
|
||||
});
|
||||
|
||||
installSessionBindingContractSuite({
|
||||
expectedCapabilities: entry.expectedCapabilities,
|
||||
getCapabilities: entry.getCapabilities,
|
||||
bindAndResolve: entry.bindAndResolve,
|
||||
unbindAndVerify: entry.unbindAndVerify,
|
||||
cleanup: entry.cleanup,
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user