QA: add Matrix transport substrate

This commit is contained in:
Gustavo Madeira Santana
2026-04-14 19:41:42 -04:00
parent 711b1a8f64
commit 5e77cbd9ec
7 changed files with 723 additions and 22 deletions

View File

@@ -5,6 +5,7 @@ import {
provisionMatrixQaRoom,
type MatrixQaObservedEvent,
} from "./client.js";
import { buildDefaultMatrixQaTopologySpec } from "./topology.js";
function resolveRequestUrl(input: RequestInfo | URL) {
if (typeof input === "string") {
@@ -308,6 +309,57 @@ describe("matrix driver client", () => {
);
});
it("issues Matrix room membership control requests for QA topology changes", async () => {
const requests: Array<{ body: Record<string, unknown>; url: string }> = [];
const fetchImpl: typeof fetch = async (input, init) => {
requests.push({
body: parseJsonRequestBody(init),
url: resolveRequestUrl(input),
});
return new Response(JSON.stringify({}), {
status: 200,
headers: { "content-type": "application/json" },
});
};
const client = createMatrixQaClient({
accessToken: "token",
baseUrl: "http://127.0.0.1:28008/",
fetchImpl,
});
await client.inviteUserToRoom({
roomId: "!room:matrix-qa.test",
userId: "@observer:matrix-qa.test",
});
await client.kickUserFromRoom({
reason: "topology reset",
roomId: "!room:matrix-qa.test",
userId: "@observer:matrix-qa.test",
});
await client.leaveRoom("!room:matrix-qa.test");
expect(requests).toEqual([
{
url: "http://127.0.0.1:28008/_matrix/client/v3/rooms/!room%3Amatrix-qa.test/invite",
body: {
user_id: "@observer:matrix-qa.test",
},
},
{
url: "http://127.0.0.1:28008/_matrix/client/v3/rooms/!room%3Amatrix-qa.test/kick",
body: {
reason: "topology reset",
user_id: "@observer:matrix-qa.test",
},
},
{
url: "http://127.0.0.1:28008/_matrix/client/v3/rooms/!room%3Amatrix-qa.test/leave",
body: {},
},
]);
});
it("sends Matrix reactions through the protocol send endpoint", async () => {
const fetchImpl: typeof fetch = async (input, init) => {
expect(resolveRequestUrl(input)).toContain(
@@ -401,16 +453,117 @@ describe("matrix driver client", () => {
roomName: "OpenClaw Matrix QA",
sutLocalpart: "qa-sut",
fetchImpl,
topology: buildDefaultMatrixQaTopologySpec({
defaultRoomName: "OpenClaw Matrix QA",
}),
});
expect(result.roomId).toBe("!room:matrix-qa.test");
expect(result.topology).toMatchObject({
defaultRoomId: "!room:matrix-qa.test",
defaultRoomKey: "main",
rooms: [
{
key: "main",
kind: "group",
memberRoles: ["driver", "observer", "sut"],
memberUserIds: [
"@qa-driver:matrix-qa.test",
"@qa-observer:matrix-qa.test",
"@qa-sut:matrix-qa.test",
],
requireMention: true,
roomId: "!room:matrix-qa.test",
},
],
});
expect(result.observer.userId).toBe("@qa-observer:matrix-qa.test");
expect(createRoomBodies).toEqual([
expect.objectContaining({
invite: ["@qa-sut:matrix-qa.test", "@qa-observer:matrix-qa.test"],
invite: ["@qa-observer:matrix-qa.test", "@qa-sut:matrix-qa.test"],
is_direct: false,
preset: "private_chat",
}),
]);
});
it("provisions direct-message topology rooms with Matrix direct-room flags", async () => {
const createRoomBodies: Array<Record<string, unknown>> = [];
const roomIds = ["!group:matrix-qa.test", "!dm:matrix-qa.test"];
let registerCount = 0;
const fetchImpl: typeof fetch = async (input, init) => {
const url = resolveRequestUrl(input);
const body = parseJsonRequestBody(init);
if (url.endsWith("/_matrix/client/v3/register")) {
registerCount += 1;
const role = ["driver", "sut", "observer"][registerCount - 1];
return new Response(
JSON.stringify({
access_token: `token-${role}`,
user_id: `@qa-${role}:matrix-qa.test`,
}),
{ status: 200, headers: { "content-type": "application/json" } },
);
}
if (url.endsWith("/_matrix/client/v3/createRoom")) {
createRoomBodies.push(body);
return new Response(JSON.stringify({ room_id: roomIds.shift() }), {
status: 200,
headers: { "content-type": "application/json" },
});
}
if (url.includes("/_matrix/client/v3/join/")) {
return new Response(JSON.stringify({ room_id: "!joined:matrix-qa.test" }), {
status: 200,
headers: { "content-type": "application/json" },
});
}
throw new Error(`unexpected fetch ${url}`);
};
const result = await provisionMatrixQaRoom({
baseUrl: "http://127.0.0.1:28008/",
driverLocalpart: "qa-driver",
observerLocalpart: "qa-observer",
registrationToken: "reg-token",
roomName: "unused",
sutLocalpart: "qa-sut",
fetchImpl,
topology: {
defaultRoomKey: "group",
rooms: [
{
key: "group",
kind: "group",
members: ["driver", "observer", "sut"],
name: "Matrix Group",
requireMention: true,
},
{
key: "sut-dm",
kind: "dm",
members: ["driver", "sut"],
name: "Matrix Driver/SUT DM",
},
],
},
});
expect(result.topology.rooms).toMatchObject([
{ key: "group", kind: "group", roomId: "!group:matrix-qa.test", requireMention: true },
{ key: "sut-dm", kind: "dm", roomId: "!dm:matrix-qa.test", requireMention: false },
]);
expect(createRoomBodies).toEqual([
expect.objectContaining({
invite: ["@qa-observer:matrix-qa.test", "@qa-sut:matrix-qa.test"],
is_direct: false,
name: "Matrix Group",
}),
expect.objectContaining({
invite: ["@qa-sut:matrix-qa.test"],
is_direct: true,
name: "Matrix Driver/SUT DM",
}),
]);
});
});

View File

@@ -1,5 +1,12 @@
import { randomUUID } from "node:crypto";
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
import {
findMatrixQaProvisionedRoom,
type MatrixQaParticipantRole,
type MatrixQaProvisionedTopology,
type MatrixQaTopologyRoomSpec,
type MatrixQaTopologySpec,
} from "./topology.js";
type FetchLike = typeof fetch;
@@ -115,6 +122,7 @@ export type MatrixQaProvisionResult = {
observer: MatrixQaRegisteredAccount;
roomId: string;
sut: MatrixQaRegisteredAccount;
topology: MatrixQaProvisionedTopology;
};
export type MatrixQaRoomEventWaitResult =
@@ -488,7 +496,7 @@ export function createMatrixQaClient(params: {
}
return {
async createPrivateRoom(opts: { inviteUserIds: string[]; name: string }) {
async createPrivateRoom(opts: { inviteUserIds: string[]; isDirect?: boolean; name: string }) {
const result = await requestMatrixJson<MatrixQaRoomCreateResponse>({
accessToken: params.accessToken,
baseUrl: params.baseUrl,
@@ -502,7 +510,7 @@ export function createMatrixQaClient(params: {
},
],
invite: opts.inviteUserIds,
is_direct: false,
is_direct: opts.isDirect === true,
name: opts.name,
preset: "private_chat",
},
@@ -618,6 +626,41 @@ export function createMatrixQaClient(params: {
});
return result.body.room_id?.trim() || roomId;
},
async inviteUserToRoom(opts: { roomId: string; userId: string }) {
await requestMatrixJson<Record<string, never>>({
accessToken: params.accessToken,
baseUrl: params.baseUrl,
body: {
user_id: opts.userId,
},
endpoint: `/_matrix/client/v3/rooms/${encodeURIComponent(opts.roomId)}/invite`,
fetchImpl,
method: "POST",
});
},
async kickUserFromRoom(opts: { reason?: string; roomId: string; userId: string }) {
await requestMatrixJson<Record<string, never>>({
accessToken: params.accessToken,
baseUrl: params.baseUrl,
body: {
user_id: opts.userId,
...(opts.reason?.trim() ? { reason: opts.reason.trim() } : {}),
},
endpoint: `/_matrix/client/v3/rooms/${encodeURIComponent(opts.roomId)}/kick`,
fetchImpl,
method: "POST",
});
},
async leaveRoom(roomId: string) {
await requestMatrixJson<Record<string, never>>({
accessToken: params.accessToken,
baseUrl: params.baseUrl,
body: {},
endpoint: `/_matrix/client/v3/rooms/${encodeURIComponent(roomId)}/leave`,
fetchImpl,
method: "POST",
});
},
waitForOptionalRoomEvent,
async waitForRoomEvent(opts: {
observedEvents: MatrixQaObservedEvent[];
@@ -659,9 +702,85 @@ async function joinRoomWithRetry(params: {
throw new Error(`Matrix join retry failed: ${formatErrorMessage(lastError)}`);
}
function resolveProvisionedRoomRequireMention(room: MatrixQaTopologyRoomSpec) {
return room.kind === "group" ? room.requireMention !== false : false;
}
function resolveTopologyMemberAccounts(
accounts: Record<MatrixQaParticipantRole, MatrixQaRegisteredAccount>,
memberRoles: MatrixQaParticipantRole[],
) {
const uniqueRoles = [...new Set(memberRoles)];
if (uniqueRoles.length === 0) {
throw new Error("Matrix QA room provisioning requires at least one member");
}
return uniqueRoles.map((role) => ({
role,
account: accounts[role],
}));
}
async function provisionMatrixQaTopology(params: {
accounts: Record<MatrixQaParticipantRole, MatrixQaRegisteredAccount>;
baseUrl: string;
fetchImpl?: FetchLike;
spec: MatrixQaTopologySpec;
}): Promise<MatrixQaProvisionedTopology> {
const rooms = [];
for (const room of params.spec.rooms) {
const members = resolveTopologyMemberAccounts(params.accounts, room.members);
const creator = members[0];
const invitees = members.slice(1);
const creatorClient = createMatrixQaClient({
accessToken: creator.account.accessToken,
baseUrl: params.baseUrl,
fetchImpl: params.fetchImpl,
});
const roomId = await creatorClient.createPrivateRoom({
inviteUserIds: invitees.map((entry) => entry.account.userId),
isDirect: room.kind === "dm",
name: room.name,
});
for (const invitee of invitees) {
await joinRoomWithRetry({
accessToken: invitee.account.accessToken,
baseUrl: params.baseUrl,
fetchImpl: params.fetchImpl,
roomId,
});
}
rooms.push({
key: room.key,
kind: room.kind,
memberRoles: members.map((entry) => entry.role),
memberUserIds: members.map((entry) => entry.account.userId),
name: room.name,
requireMention: resolveProvisionedRoomRequireMention(room),
roomId,
});
}
const defaultRoom = findMatrixQaProvisionedRoom(
{
defaultRoomId: "",
defaultRoomKey: params.spec.defaultRoomKey,
rooms,
},
params.spec.defaultRoomKey,
);
return {
defaultRoomId: defaultRoom.roomId,
defaultRoomKey: params.spec.defaultRoomKey,
rooms,
};
}
export async function provisionMatrixQaRoom(params: {
baseUrl: string;
fetchImpl?: FetchLike;
topology?: MatrixQaTopologySpec;
roomName: string;
driverLocalpart: string;
observerLocalpart: string;
@@ -690,32 +809,35 @@ export async function provisionMatrixQaRoom(params: {
password: `observer-${randomUUID()}`,
registrationToken: params.registrationToken,
});
const driverClient = createMatrixQaClient({
accessToken: driver.accessToken,
const topology = await provisionMatrixQaTopology({
accounts: {
driver,
observer,
sut,
},
baseUrl: params.baseUrl,
fetchImpl: params.fetchImpl,
});
const roomId = await driverClient.createPrivateRoom({
inviteUserIds: [sut.userId, observer.userId],
name: params.roomName,
});
await joinRoomWithRetry({
accessToken: sut.accessToken,
baseUrl: params.baseUrl,
fetchImpl: params.fetchImpl,
roomId,
});
await joinRoomWithRetry({
accessToken: observer.accessToken,
baseUrl: params.baseUrl,
fetchImpl: params.fetchImpl,
roomId,
spec:
params.topology ??
({
defaultRoomKey: "main",
rooms: [
{
key: "main",
kind: "group",
members: ["driver", "observer", "sut"],
name: params.roomName,
requireMention: true,
},
],
} satisfies MatrixQaTopologySpec),
});
return {
driver,
observer,
roomId,
roomId: topology.defaultRoomId,
sut,
topology,
} satisfies MatrixQaProvisionResult;
}

View File

@@ -0,0 +1,125 @@
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
import { describe, expect, it } from "vitest";
import { buildMatrixQaConfig } from "./config.js";
describe("matrix qa config", () => {
const topology = {
defaultRoomId: "!main:matrix-qa.test",
defaultRoomKey: "main",
rooms: [
{
key: "main",
kind: "group" as const,
memberRoles: ["driver", "observer", "sut"],
memberUserIds: [
"@driver:matrix-qa.test",
"@observer:matrix-qa.test",
"@sut:matrix-qa.test",
],
name: "Main",
requireMention: true,
roomId: "!main:matrix-qa.test",
},
{
key: "secondary",
kind: "group" as const,
memberRoles: ["driver", "observer", "sut"],
memberUserIds: [
"@driver:matrix-qa.test",
"@observer:matrix-qa.test",
"@sut:matrix-qa.test",
],
name: "Secondary",
requireMention: true,
roomId: "!secondary:matrix-qa.test",
},
{
key: "driver-dm",
kind: "dm" as const,
memberRoles: ["driver", "sut"],
memberUserIds: ["@driver:matrix-qa.test", "@sut:matrix-qa.test"],
name: "DM",
requireMention: false,
roomId: "!dm:matrix-qa.test",
},
],
};
it("builds default Matrix QA config from provisioned topology", () => {
const next = buildMatrixQaConfig({} as OpenClawConfig, {
driverUserId: "@driver:matrix-qa.test",
homeserver: "http://127.0.0.1:28008/",
sutAccessToken: "sut-token",
sutAccountId: "sut",
sutUserId: "@sut:matrix-qa.test",
topology,
});
expect(next.channels?.matrix?.accounts?.sut).toMatchObject({
dm: {
allowFrom: ["@driver:matrix-qa.test"],
enabled: true,
policy: "allowlist",
},
groupAllowFrom: ["@driver:matrix-qa.test"],
groupPolicy: "allowlist",
groups: {
"!main:matrix-qa.test": { enabled: true, requireMention: true },
"!secondary:matrix-qa.test": { enabled: true, requireMention: true },
},
replyToMode: "off",
threadReplies: "inbound",
});
});
it("applies room-keyed Matrix QA config overrides", () => {
const next = buildMatrixQaConfig({} as OpenClawConfig, {
driverUserId: "@driver:matrix-qa.test",
homeserver: "http://127.0.0.1:28008/",
overrides: {
groupAllowFrom: ["@driver:matrix-qa.test", "@observer:matrix-qa.test"],
groupsByKey: {
secondary: {
requireMention: false,
},
},
replyToMode: "all",
threadReplies: "always",
},
sutAccessToken: "sut-token",
sutAccountId: "sut",
sutUserId: "@sut:matrix-qa.test",
topology,
});
expect(next.channels?.matrix?.accounts?.sut).toMatchObject({
groupAllowFrom: ["@driver:matrix-qa.test", "@observer:matrix-qa.test"],
groups: {
"!main:matrix-qa.test": { enabled: true, requireMention: true },
"!secondary:matrix-qa.test": { enabled: true, requireMention: false },
},
replyToMode: "all",
threadReplies: "always",
});
});
it("rejects unknown room-key overrides", () => {
expect(() =>
buildMatrixQaConfig({} as OpenClawConfig, {
driverUserId: "@driver:matrix-qa.test",
homeserver: "http://127.0.0.1:28008/",
overrides: {
groupsByKey: {
ghost: {
requireMention: false,
},
},
},
sutAccessToken: "sut-token",
sutAccountId: "sut",
sutUserId: "@sut:matrix-qa.test",
topology,
}),
).toThrow('Matrix QA group override references unknown room key "ghost"');
});
});

View File

@@ -0,0 +1,169 @@
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
import type { MatrixQaProvisionedTopology } from "./topology.js";
type MatrixQaReplyToMode = "off" | "first" | "all" | "batched";
type MatrixQaThreadRepliesMode = "off" | "inbound" | "always";
type MatrixQaDmPolicy = "allowlist" | "disabled" | "open" | "pairing";
type MatrixQaGroupPolicy = "allowlist" | "disabled" | "open";
export type MatrixQaGroupConfigOverrides = {
enabled?: boolean;
requireMention?: boolean;
};
export type MatrixQaDmConfigOverrides = {
allowFrom?: string[];
enabled?: boolean;
policy?: MatrixQaDmPolicy;
sessionScope?: "per-room" | "per-user";
threadReplies?: MatrixQaThreadRepliesMode;
};
export type MatrixQaConfigOverrides = {
autoJoin?: "allowlist" | "always" | "off";
blockStreaming?: boolean;
dm?: MatrixQaDmConfigOverrides;
encryption?: boolean;
groupAllowFrom?: string[];
groupPolicy?: MatrixQaGroupPolicy;
groupsByKey?: Record<string, MatrixQaGroupConfigOverrides>;
replyToMode?: MatrixQaReplyToMode;
streaming?: "off" | "partial" | "quiet" | boolean;
threadReplies?: MatrixQaThreadRepliesMode;
};
function resolveMatrixQaGroupEntries(params: {
overrides?: MatrixQaConfigOverrides;
topology: MatrixQaProvisionedTopology;
}) {
const groupRooms = params.topology.rooms.filter((room) => room.kind === "group");
const groupsByKey = params.overrides?.groupsByKey ?? {};
const knownGroupKeys = new Set(groupRooms.map((room) => room.key));
for (const key of Object.keys(groupsByKey)) {
if (!knownGroupKeys.has(key)) {
throw new Error(`Matrix QA group override references unknown room key "${key}"`);
}
}
return Object.fromEntries(
groupRooms.map((room) => {
const override = groupsByKey[room.key];
return [
room.roomId,
{
enabled: override?.enabled ?? true,
requireMention: override?.requireMention ?? room.requireMention,
},
];
}),
);
}
function resolveMatrixQaDmAllowFrom(params: {
driverUserId: string;
overrides?: MatrixQaConfigOverrides;
sutUserId: string;
topology: MatrixQaProvisionedTopology;
}) {
if (params.overrides?.dm?.allowFrom) {
return [...params.overrides.dm.allowFrom];
}
const dmAllowFrom = [
...new Set(
params.topology.rooms
.filter((room) => room.kind === "dm")
.flatMap((room) => room.memberUserIds.filter((userId) => userId !== params.sutUserId)),
),
];
return dmAllowFrom.length > 0 ? dmAllowFrom : [params.driverUserId];
}
function resolveMatrixQaDmConfig(params: {
driverUserId: string;
overrides?: MatrixQaConfigOverrides;
sutUserId: string;
topology: MatrixQaProvisionedTopology;
}) {
const hasDmRooms = params.topology.rooms.some((room) => room.kind === "dm");
const dmOverrides = params.overrides?.dm;
if (!hasDmRooms && dmOverrides?.enabled !== true) {
return { enabled: false };
}
return {
allowFrom: resolveMatrixQaDmAllowFrom(params),
enabled: dmOverrides?.enabled ?? true,
policy: dmOverrides?.policy ?? "allowlist",
...(dmOverrides?.sessionScope ? { sessionScope: dmOverrides.sessionScope } : {}),
...(dmOverrides?.threadReplies ? { threadReplies: dmOverrides.threadReplies } : {}),
};
}
export function buildMatrixQaConfig(
baseCfg: OpenClawConfig,
params: {
driverUserId: string;
homeserver: string;
overrides?: MatrixQaConfigOverrides;
sutAccessToken: string;
sutAccountId: string;
sutDeviceId?: string;
sutUserId: string;
topology: MatrixQaProvisionedTopology;
},
): OpenClawConfig {
const pluginAllow = [...new Set([...(baseCfg.plugins?.allow ?? []), "matrix"])];
const groups = resolveMatrixQaGroupEntries({
overrides: params.overrides,
topology: params.topology,
});
return {
...baseCfg,
plugins: {
...baseCfg.plugins,
allow: pluginAllow,
entries: {
...baseCfg.plugins?.entries,
matrix: { enabled: true },
},
},
channels: {
...baseCfg.channels,
matrix: {
...baseCfg.channels?.matrix,
enabled: true,
defaultAccount: params.sutAccountId,
accounts: {
...baseCfg.channels?.matrix?.accounts,
[params.sutAccountId]: {
accessToken: params.sutAccessToken,
...(params.sutDeviceId ? { deviceId: params.sutDeviceId } : {}),
dm: resolveMatrixQaDmConfig(params),
enabled: true,
encryption: params.overrides?.encryption ?? false,
groupAllowFrom: params.overrides?.groupAllowFrom ?? [params.driverUserId],
groupPolicy: params.overrides?.groupPolicy ?? "allowlist",
...(Object.keys(groups).length > 0 ? { groups } : {}),
homeserver: params.homeserver,
network: {
dangerouslyAllowPrivateNetwork: true,
},
replyToMode: params.overrides?.replyToMode ?? "off",
threadReplies: params.overrides?.threadReplies ?? "inbound",
userId: params.sutUserId,
...(params.overrides?.autoJoin ? { autoJoin: params.overrides.autoJoin } : {}),
...(params.overrides?.blockStreaming !== undefined
? { blockStreaming: params.overrides.blockStreaming }
: {}),
...(params.overrides?.streaming !== undefined
? { streaming: params.overrides.streaming }
: {}),
},
},
},
},
};
}

View File

@@ -84,6 +84,10 @@ describe("matrix harness runtime", () => {
expect(result.stopCommand).toBe(
`docker compose -f ${outputDir}/docker-compose.matrix-qa.yml down --remove-orphans`,
);
await result.restartService();
expect(calls).toContain(
`docker compose -f ${outputDir}/docker-compose.matrix-qa.yml restart matrix-qa-homeserver @/repo/openclaw`,
);
} finally {
await rm(outputDir, { recursive: true, force: true });
}

View File

@@ -39,6 +39,7 @@ export type MatrixQaHarnessFiles = {
export type MatrixQaHarness = MatrixQaHarnessFiles & {
baseUrl: string;
restartService(): Promise<void>;
stopCommand: string;
stop(): Promise<void>;
};
@@ -248,9 +249,34 @@ export async function startMatrixQaHarness(
sleepImpl,
});
const waitForReady = async () => {
await sleepImpl(1_000);
await waitForDockerServiceHealth(
MATRIX_QA_SERVICE,
files.composeFile,
repoRoot,
runCommand,
sleepImpl,
);
await waitForHealth(buildVersionsUrl(baseUrl), {
label: "Matrix homeserver",
composeFile: files.composeFile,
fetchImpl,
sleepImpl,
});
};
return {
...files,
baseUrl,
async restartService() {
await runCommand(
"docker",
["compose", "-f", files.composeFile, "restart", MATRIX_QA_SERVICE],
repoRoot,
);
await waitForReady();
},
stopCommand: `docker compose -f ${files.composeFile} down --remove-orphans`,
async stop() {
await runCommand(

View File

@@ -0,0 +1,102 @@
export type MatrixQaParticipantRole = "driver" | "observer" | "sut";
export type MatrixQaRoomKind = "dm" | "group";
export type MatrixQaTopologyRoomSpec = {
key: string;
kind: MatrixQaRoomKind;
members: MatrixQaParticipantRole[];
name: string;
requireMention?: boolean;
};
export type MatrixQaTopologySpec = {
defaultRoomKey: string;
rooms: MatrixQaTopologyRoomSpec[];
};
export type MatrixQaProvisionedRoom = {
key: string;
kind: MatrixQaRoomKind;
memberRoles: MatrixQaParticipantRole[];
memberUserIds: string[];
name: string;
requireMention: boolean;
roomId: string;
};
export type MatrixQaProvisionedTopology = {
defaultRoomId: string;
defaultRoomKey: string;
rooms: MatrixQaProvisionedRoom[];
};
function matrixQaRoomSpecsEqual(left: MatrixQaTopologyRoomSpec, right: MatrixQaTopologyRoomSpec) {
return (
left.key === right.key &&
left.kind === right.kind &&
left.name === right.name &&
left.requireMention === right.requireMention &&
left.members.length === right.members.length &&
left.members.every((member, index) => member === right.members[index])
);
}
export function buildDefaultMatrixQaTopologySpec(params: {
defaultRoomName: string;
}): MatrixQaTopologySpec {
return {
defaultRoomKey: "main",
rooms: [
{
key: "main",
kind: "group",
members: ["driver", "observer", "sut"],
name: params.defaultRoomName,
requireMention: true,
},
],
};
}
export function findMatrixQaProvisionedRoom(
topology: MatrixQaProvisionedTopology,
key: string,
): MatrixQaProvisionedRoom {
const room = topology.rooms.find((entry) => entry.key === key);
if (!room) {
throw new Error(`Matrix QA topology is missing room "${key}"`);
}
return room;
}
export function mergeMatrixQaTopologySpecs(specs: MatrixQaTopologySpec[]): MatrixQaTopologySpec {
const first = specs[0];
if (!first) {
throw new Error("Matrix QA topology merge requires at least one spec");
}
const roomByKey = new Map<string, MatrixQaTopologyRoomSpec>();
for (const spec of specs) {
if (spec.defaultRoomKey !== first.defaultRoomKey) {
throw new Error(
`Matrix QA topology default room mismatch: ${spec.defaultRoomKey} !== ${first.defaultRoomKey}`,
);
}
for (const room of spec.rooms) {
const existing = roomByKey.get(room.key);
if (!existing) {
roomByKey.set(room.key, room);
continue;
}
if (!matrixQaRoomSpecsEqual(existing, room)) {
throw new Error(`Matrix QA topology room "${room.key}" has conflicting definitions`);
}
}
}
return {
defaultRoomKey: first.defaultRoomKey,
rooms: [...roomByKey.values()],
};
}