Add matrix session binding contracts

This commit is contained in:
Tak Hoffman 2026-03-19 01:59:01 -05:00
parent e63bedb74b
commit 46aa10c04a
No known key found for this signature in database
3 changed files with 112 additions and 5 deletions

View File

@ -1,9 +1,17 @@
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/src/matrix/thread-bindings.js";
import { setMatrixRuntime } from "../../../../extensions/matrix/src/runtime.js";
import { createTelegramThreadBindingManager } from "../../../../extensions/telegram/runtime-api.js";
import type { OpenClawConfig } from "../../../config/config.js";
import {
@ -126,12 +134,39 @@ 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;
};
const matrixSessionBindingAuth = {
accountId: "default",
homeserver: "https://matrix.example.org",
userId: "@bot:example.org",
accessToken: "token",
} as const;
function createMatrixSessionBindingStateDir() {
return fs.mkdtempSync(path.join(os.tmpdir(), "matrix-session-binding-contract-"));
}
async function setupMatrixSessionBindingManager() {
setMatrixRuntime({
state: {
resolveStateDir: () => createMatrixSessionBindingStateDir(),
},
} as never);
return await createMatrixThreadBindingManager({
accountId: "default",
auth: matrixSessionBindingAuth,
client: {} as never,
idleTimeoutMs: 24 * 60 * 60 * 1000,
maxAgeMs: 0,
enableSweeper: false,
});
}
function expectResolvedSessionBinding(params: {
channel: string;
accountId: string;
@ -708,6 +743,58 @@ export const sessionBindingContractRegistry: SessionBindingContractEntry[] = [
});
},
},
{
id: "matrix",
expectedCapabilities: {
adapterAvailable: true,
bindSupported: true,
unbindSupported: true,
placements: ["current", "child"],
},
getCapabilities: async () => {
await setupMatrixSessionBindingManager();
return getSessionBindingService().getCapabilities({
channel: "matrix",
accountId: "default",
});
},
bindAndResolve: async () => {
await setupMatrixSessionBindingManager();
const service = getSessionBindingService();
const binding = await service.bind({
targetSessionKey: "agent:matrix:child:thread-1",
targetKind: "subagent",
conversation: {
channel: "matrix",
accountId: "default",
conversationId: "!room:example",
},
placement: "child",
metadata: {
label: "codex-matrix",
introText: "matrix contract intro",
},
});
expectResolvedSessionBinding({
channel: "matrix",
accountId: "default",
conversationId: "$root",
targetSessionKey: "agent:matrix:child:thread-1",
});
return binding;
},
unbindAndVerify: unbindAndExpectClearedSessionBinding,
cleanup: async () => {
const manager = await setupMatrixSessionBindingManager();
manager.stop();
resetMatrixThreadBindingsForTests();
expectClearedSessionBinding({
channel: "matrix",
accountId: "default",
conversationId: "$root",
});
},
},
{
id: "telegram",
expectedCapabilities: {

View File

@ -1,16 +1,36 @@
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/src/matrix/thread-bindings.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";
const sendMessageMatrixMock = vi.hoisted(() =>
vi.fn(async (_to: string, _message: string, opts?: { threadId?: string }) => ({
messageId: opts?.threadId ? "$reply" : "$root",
roomId: "!room:example",
})),
);
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: sendMessageMatrixMock,
};
});
beforeEach(() => {
sessionBindingTesting.resetSessionBindingAdaptersForTests();
discordThreadBindingTesting.resetThreadBindingsForTests();
feishuThreadBindingTesting.resetFeishuThreadBindingsForTests();
resetMatrixThreadBindingsForTests();
telegramThreadBindingTesting.resetTelegramThreadBindingsForTests();
sendMessageMatrixMock.mockClear();
});
for (const entry of sessionBindingContractRegistry) {

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 () => {