test(contracts): cover matrix session binding adapters (#50369)

Merged via squash.

Prepared head SHA: 25412dbc2ca91876882de1854da1f0e9c0640543
Co-authored-by: ChroniCat <220139611+ChroniCat@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
This commit is contained in:
Liu Ricardo 2026-03-19 22:26:37 +08:00 committed by GitHub
parent c7cbc8cc0b
commit 8c01347989
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 111 additions and 5 deletions

View File

@ -44,6 +44,7 @@ Docs: https://docs.openclaw.ai
- Control UI/chat: add an expand-to-canvas button on assistant chat bubbles and in-app session navigation from Sessions and Cron views. Thanks @BunsDev.
- Plugins/context engines: expose `delegateCompactionToRuntime(...)` on the public plugin SDK, refactor the legacy engine to use the shared helper, and clarify `ownsCompaction` delegation semantics for non-owning engines. (#49061) Thanks @jalehman.
- Plugins/MiniMax: add MiniMax-M2.7 and MiniMax-M2.7-highspeed models and update the default model from M2.5 to M2.7. (#49691) Thanks @liyuan97.
- Contracts/Matrix: validate Matrix session binding coverage through the real manager, expose the manager on the Matrix runtime API, and let tests pass an explicit state directory for isolated contract setup. (#50369) thanks @ChroniCat.
### Fixes

View File

@ -1,3 +1,8 @@
export * from "./src/setup-core.js";
export * from "./src/setup-surface.js";
export {
createMatrixThreadBindingManager,
getMatrixThreadBindingManager,
resetMatrixThreadBindingsForTests,
} from "./src/matrix/thread-bindings.js";
export { matrixOnboardingAdapter as matrixSetupWizard } from "./src/onboarding.js";

View File

@ -173,6 +173,7 @@ function resolveBindingsPath(params: {
auth: MatrixAuth;
accountId: string;
env?: NodeJS.ProcessEnv;
stateDir?: string;
}): string {
const storagePaths = resolveMatrixStoragePaths({
homeserver: params.auth.homeserver,
@ -181,6 +182,7 @@ function resolveBindingsPath(params: {
accountId: params.accountId,
deviceId: params.auth.deviceId,
env: params.env,
stateDir: params.stateDir,
});
return path.join(storagePaths.rootDir, "thread-bindings.json");
}
@ -341,6 +343,7 @@ export async function createMatrixThreadBindingManager(params: {
auth: MatrixAuth;
client: MatrixClient;
env?: NodeJS.ProcessEnv;
stateDir?: string;
idleTimeoutMs: number;
maxAgeMs: number;
enableSweeper?: boolean;
@ -360,6 +363,7 @@ export async function createMatrixThreadBindingManager(params: {
auth: params.auth,
accountId: params.accountId,
env: params.env,
stateDir: params.stateDir,
});
const loaded = await loadBindingsFromDisk(filePath, params.accountId);
for (const record of loaded) {

View File

@ -1,9 +1,13 @@
import fs from "node:fs/promises";
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 } from "../../../../extensions/matrix/api.js";
import { createTelegramThreadBindingManager } from "../../../../extensions/telegram/runtime-api.js";
import type { OpenClawConfig } from "../../../config/config.js";
import {
@ -126,7 +130,7 @@ type DirectoryContractEntry = {
type SessionBindingContractEntry = {
id: string;
expectedCapabilities: SessionBindingCapabilities;
getCapabilities: () => SessionBindingCapabilities;
getCapabilities: () => SessionBindingCapabilities | Promise<SessionBindingCapabilities>;
bindAndResolve: () => Promise<SessionBindingRecord>;
unbindAndVerify: (binding: SessionBindingRecord) => Promise<void>;
cleanup: () => Promise<void> | void;
@ -136,6 +140,7 @@ function expectResolvedSessionBinding(params: {
channel: string;
accountId: string;
conversationId: string;
parentConversationId?: string;
targetSessionKey: string;
}) {
expect(
@ -143,6 +148,7 @@ function expectResolvedSessionBinding(params: {
channel: params.channel,
accountId: params.accountId,
conversationId: params.conversationId,
parentConversationId: params.parentConversationId,
}),
)?.toMatchObject({
targetSessionKey: params.targetSessionKey,
@ -589,6 +595,24 @@ const baseSessionBindingCfg = {
session: { mainKey: "main", scope: "per-sender" },
} satisfies OpenClawConfig;
async function createContractMatrixThreadBindingManager() {
const stateDir = await fs.mkdtemp(path.join(os.tmpdir(), "matrix-contract-thread-bindings-"));
return await createMatrixThreadBindingManager({
accountId: "ops",
auth: {
accountId: "ops",
homeserver: "https://matrix.example.org",
userId: "@bot:example.org",
accessToken: "token",
},
client: {} as never,
stateDir,
idleTimeoutMs: 24 * 60 * 60 * 1000,
maxAgeMs: 0,
enableSweeper: false,
});
}
export const sessionBindingContractRegistry: SessionBindingContractEntry[] = [
{
id: "discord",
@ -708,6 +732,61 @@ export const sessionBindingContractRegistry: SessionBindingContractEntry[] = [
});
},
},
{
id: "matrix",
expectedCapabilities: {
adapterAvailable: true,
bindSupported: true,
unbindSupported: true,
placements: ["current", "child"],
},
getCapabilities: async () => {
await createContractMatrixThreadBindingManager();
return getSessionBindingService().getCapabilities({
channel: "matrix",
accountId: "ops",
});
},
bindAndResolve: async () => {
await createContractMatrixThreadBindingManager();
const service = getSessionBindingService();
const binding = await service.bind({
targetSessionKey: "agent:matrix:subagent:child-1",
targetKind: "subagent",
conversation: {
channel: "matrix",
accountId: "ops",
conversationId: "!room:example",
},
placement: "child",
metadata: {
label: "codex-matrix",
introText: "intro root",
},
});
expectResolvedSessionBinding({
channel: "matrix",
accountId: "ops",
conversationId: "$root",
parentConversationId: "!room:example",
targetSessionKey: "agent:matrix:subagent:child-1",
});
return binding;
},
unbindAndVerify: unbindAndExpectClearedSessionBinding,
cleanup: async () => {
const manager = await createContractMatrixThreadBindingManager();
manager.stop();
expect(
getSessionBindingService().resolveByConversation({
channel: "matrix",
accountId: "ops",
conversationId: "$root",
parentConversationId: "!room:example",
}),
).toBeNull();
},
},
{
id: "telegram",
expectedCapabilities: {

View File

@ -1,15 +1,32 @@
import { beforeEach, describe } from "vitest";
import { beforeEach, describe, vi } from "vitest";
import { __testing as discordThreadBindingTesting } from "../../../../extensions/discord/src/monitor/thread-bindings.manager.js";
import { __testing as feishuThreadBindingTesting } from "../../../../extensions/feishu/src/thread-bindings.js";
import { resetMatrixThreadBindingsForTests } from "../../../../extensions/matrix/api.js";
import { __testing as telegramThreadBindingTesting } from "../../../../extensions/telegram/src/thread-bindings.js";
import { __testing as sessionBindingTesting } from "../../../infra/outbound/session-binding-service.js";
import { sessionBindingContractRegistry } from "./registry.js";
import { installSessionBindingContractSuite } from "./suites.js";
vi.mock("../../../../extensions/matrix/src/matrix/send.js", async () => {
const actual = await vi.importActual<
typeof import("../../../../extensions/matrix/src/matrix/send.js")
>("../../../../extensions/matrix/src/matrix/send.js");
return {
...actual,
sendMessageMatrix: vi.fn(
async (_to: string, _message: string, opts?: { threadId?: string }) => ({
messageId: opts?.threadId ? "$reply" : "$root",
roomId: "!room:example",
}),
),
};
});
beforeEach(() => {
sessionBindingTesting.resetSessionBindingAdaptersForTests();
discordThreadBindingTesting.resetThreadBindingsForTests();
feishuThreadBindingTesting.resetFeishuThreadBindingsForTests();
resetMatrixThreadBindingsForTests();
telegramThreadBindingTesting.resetTelegramThreadBindingsForTests();
});

View File

@ -478,14 +478,14 @@ export function installChannelDirectoryContractSuite(params: {
}
export function installSessionBindingContractSuite(params: {
getCapabilities: () => SessionBindingCapabilities;
getCapabilities: () => SessionBindingCapabilities | Promise<SessionBindingCapabilities>;
bindAndResolve: () => Promise<SessionBindingRecord>;
unbindAndVerify: (binding: SessionBindingRecord) => Promise<void>;
cleanup: () => Promise<void> | void;
expectedCapabilities: SessionBindingCapabilities;
}) {
it("registers the expected session binding capabilities", () => {
expect(params.getCapabilities()).toEqual(params.expectedCapabilities);
it("registers the expected session binding capabilities", async () => {
expect(await params.getCapabilities()).toEqual(params.expectedCapabilities);
});
it("binds and resolves a session binding through the shared service", async () => {