QA: expand Matrix config scenario coverage

This commit is contained in:
Gustavo Madeira Santana
2026-04-14 21:06:16 -04:00
parent 734bb9c2e7
commit 9463f1c498
8 changed files with 2356 additions and 1029 deletions

View File

@@ -144,6 +144,108 @@ describe("matrix live qa runtime", () => {
});
});
it("records default and per-scenario Matrix config snapshots in the summary", () => {
expect(
liveTesting.buildMatrixQaSummary({
artifactPaths: {
observedEvents: "/tmp/observed.json",
report: "/tmp/report.md",
summary: "/tmp/summary.json",
},
checks: [{ name: "Matrix harness ready", status: "pass" }],
config: {
default: liveTesting.buildMatrixQaConfigSnapshot({
driverUserId: "@driver:matrix-qa.test",
sutUserId: "@sut:matrix-qa.test",
topology: {
defaultRoomId: "!room:matrix-qa.test",
defaultRoomKey: "main",
rooms: [
{
key: "main",
kind: "group",
memberRoles: ["driver", "observer", "sut"],
memberUserIds: [
"@driver:matrix-qa.test",
"@observer:matrix-qa.test",
"@sut:matrix-qa.test",
],
name: "Matrix QA",
requireMention: true,
roomId: "!room:matrix-qa.test",
},
],
},
}),
scenarios: [
{
id: "matrix-room-thread-reply-override",
title: "Matrix threadReplies always keeps room replies threaded",
config: liveTesting.buildMatrixQaConfigSnapshot({
driverUserId: "@driver:matrix-qa.test",
overrides: {
threadReplies: "always",
},
sutUserId: "@sut:matrix-qa.test",
topology: {
defaultRoomId: "!room:matrix-qa.test",
defaultRoomKey: "main",
rooms: [
{
key: "main",
kind: "group",
memberRoles: ["driver", "observer", "sut"],
memberUserIds: [
"@driver:matrix-qa.test",
"@observer:matrix-qa.test",
"@sut:matrix-qa.test",
],
name: "Matrix QA",
requireMention: true,
roomId: "!room:matrix-qa.test",
},
],
},
}),
},
],
},
finishedAt: "2026-04-10T10:05:00.000Z",
harness: {
baseUrl: "http://127.0.0.1:28008/",
composeFile: "/tmp/docker-compose.yml",
dmRoomIds: [],
image: "ghcr.io/matrix-construct/tuwunel:v1.5.1",
roomId: "!room:matrix-qa.test",
roomIds: ["!room:matrix-qa.test"],
serverName: "matrix-qa.test",
},
observedEventCount: 0,
scenarios: [],
startedAt: "2026-04-10T10:00:00.000Z",
sutAccountId: "sut",
userIds: {
driver: "@driver:matrix-qa.test",
observer: "@observer:matrix-qa.test",
sut: "@sut:matrix-qa.test",
},
}).config,
).toMatchObject({
default: {
replyToMode: "off",
threadReplies: "inbound",
},
scenarios: [
{
id: "matrix-room-thread-reply-override",
config: {
threadReplies: "always",
},
},
],
});
});
it("preserves negative-scenario artifacts in the Matrix summary", () => {
expect(
liveTesting.buildMatrixQaSummary({
@@ -153,6 +255,18 @@ describe("matrix live qa runtime", () => {
summary: "/tmp/summary.json",
},
checks: [{ name: "Matrix harness ready", status: "pass" }],
config: {
default: liveTesting.buildMatrixQaConfigSnapshot({
driverUserId: "@driver:matrix-qa.test",
sutUserId: "@sut:matrix-qa.test",
topology: {
defaultRoomId: "!room:matrix-qa.test",
defaultRoomKey: "main",
rooms: [],
},
}),
scenarios: [],
},
finishedAt: "2026-04-10T10:05:00.000Z",
harness: {
baseUrl: "http://127.0.0.1:28008/",

View File

@@ -14,7 +14,13 @@ import {
} from "../../shared/live-lane-helpers.js";
import { buildMatrixQaObservedEventsArtifact } from "../../substrate/artifacts.js";
import { provisionMatrixQaRoom, type MatrixQaProvisionResult } from "../../substrate/client.js";
import { buildMatrixQaConfig, type MatrixQaConfigOverrides } from "../../substrate/config.js";
import {
buildMatrixQaConfig,
buildMatrixQaConfigSnapshot,
summarizeMatrixQaConfigSnapshot,
type MatrixQaConfigOverrides,
type MatrixQaConfigSnapshot,
} from "../../substrate/config.js";
import type { MatrixQaObservedEvent } from "../../substrate/events.js";
import { startMatrixQaHarness } from "../../substrate/harness.runtime.js";
import { resolveMatrixQaModels } from "./model-selection.js";
@@ -57,6 +63,14 @@ type MatrixQaScenarioResult = {
type MatrixQaSummary = {
checks: QaReportCheck[];
config: {
default: MatrixQaConfigSnapshot;
scenarios: Array<{
config: MatrixQaConfigSnapshot;
id: string;
title: string;
}>;
};
counts: {
failed: number;
passed: number;
@@ -93,6 +107,20 @@ type MatrixQaArtifactPaths = {
summary: string;
};
function countMatrixQaStatuses<T extends { status: "fail" | "pass" }>(entries: T[]) {
return {
failed: entries.filter((entry) => entry.status === "fail").length,
passed: entries.filter((entry) => entry.status === "pass").length,
};
}
function formatMatrixQaScenarioDetails(params: { details: string; configSummary?: string }) {
if (!params.configSummary) {
return params.details;
}
return [`effective config: ${params.configSummary}`, params.details].join("\n");
}
export type MatrixQaRunResult = {
observedEventsPath: string;
outputDir: string;
@@ -105,6 +133,7 @@ function buildMatrixQaSummary(params: {
artifactPaths: MatrixQaArtifactPaths;
canary?: MatrixQaCanaryArtifact;
checks: QaReportCheck[];
config: MatrixQaSummary["config"];
finishedAt: string;
harness: MatrixQaSummary["harness"];
observedEventCount: number;
@@ -113,16 +142,16 @@ function buildMatrixQaSummary(params: {
sutAccountId: string;
userIds: MatrixQaSummary["userIds"];
}): MatrixQaSummary {
const checkCounts = countMatrixQaStatuses(params.checks);
const scenarioCounts = countMatrixQaStatuses(params.scenarios);
return {
checks: params.checks,
config: params.config,
counts: {
total: params.checks.length + params.scenarios.length,
passed:
params.checks.filter((check) => check.status === "pass").length +
params.scenarios.filter((scenario) => scenario.status === "pass").length,
failed:
params.checks.filter((check) => check.status === "fail").length +
params.scenarios.filter((scenario) => scenario.status === "fail").length,
passed: checkCounts.passed + scenarioCounts.passed,
failed: checkCounts.failed + scenarioCounts.failed,
},
finishedAt: params.finishedAt,
harness: params.harness,
@@ -298,6 +327,8 @@ export async function runMatrixQaLive(params: {
sutUserId: provisioning.sut.userId,
topology: provisioning.topology,
};
const defaultConfigSnapshot = buildMatrixQaConfigSnapshot(gatewayConfigParams);
const scenarioConfigSnapshots: MatrixQaSummary["config"]["scenarios"] = [];
try {
const ensureGatewayHarness = async (overrides?: MatrixQaConfigOverrides) => {
@@ -372,6 +403,19 @@ export async function runMatrixQaLive(params: {
if (!canaryFailed) {
for (const scenario of scenarios) {
const scenarioConfigSnapshot = buildMatrixQaConfigSnapshot({
...gatewayConfigParams,
overrides: scenario.configOverrides,
});
const scenarioConfigSummary =
scenario.configOverrides === undefined
? undefined
: summarizeMatrixQaConfigSnapshot(scenarioConfigSnapshot);
scenarioConfigSnapshots.push({
config: scenarioConfigSnapshot,
id: scenario.id,
title: scenario.title,
});
try {
const scenarioGateway = await ensureGatewayHarness(scenario.configOverrides);
const result = await runMatrixQaScenario(scenario, {
@@ -407,14 +451,20 @@ export async function runMatrixQaLive(params: {
id: scenario.id,
title: scenario.title,
status: "pass",
details: result.details,
details: formatMatrixQaScenarioDetails({
details: result.details,
configSummary: scenarioConfigSummary,
}),
});
} catch (error) {
scenarioResults.push({
id: scenario.id,
title: scenario.title,
status: "fail",
details: formatErrorMessage(error),
details: formatMatrixQaScenarioDetails({
details: formatErrorMessage(error),
configSummary: scenarioConfigSummary,
}),
});
}
}
@@ -464,6 +514,7 @@ export async function runMatrixQaLive(params: {
notes: [
`roomId: ${provisioning.roomId}`,
`roomIds: ${provisioning.topology.rooms.map((room) => room.roomId).join(", ")}`,
`default config: ${summarizeMatrixQaConfigSnapshot(defaultConfigSnapshot)}`,
`driver: ${provisioning.driver.userId}`,
`observer: ${provisioning.observer.userId}`,
`sut: ${provisioning.sut.userId}`,
@@ -475,6 +526,10 @@ export async function runMatrixQaLive(params: {
artifactPaths,
canary: canaryArtifact,
checks,
config: {
default: defaultConfigSnapshot,
scenarios: scenarioConfigSnapshots,
},
finishedAt,
harness: {
baseUrl: harness.baseUrl,
@@ -556,7 +611,9 @@ export const __testing = {
buildMatrixQaSummary,
MATRIX_QA_SCENARIOS,
buildMatrixQaConfig,
buildMatrixQaConfigSnapshot,
isMatrixAccountReady,
resolveMatrixQaModels,
summarizeMatrixQaConfigSnapshot,
waitForMatrixChannelReady,
};

View File

@@ -0,0 +1,270 @@
import {
collectLiveTransportStandardScenarioCoverage,
selectLiveTransportScenarios,
type LiveTransportScenarioDefinition,
} from "../../shared/live-transport-scenarios.js";
import { type MatrixQaConfigOverrides } from "../../substrate/config.js";
import {
buildDefaultMatrixQaTopologySpec,
findMatrixQaProvisionedRoom,
mergeMatrixQaTopologySpecs,
type MatrixQaProvisionedTopology,
type MatrixQaTopologySpec,
} from "../../substrate/topology.js";
export type MatrixQaScenarioId =
| "matrix-thread-follow-up"
| "matrix-thread-isolation"
| "matrix-top-level-reply-shape"
| "matrix-room-thread-reply-override"
| "matrix-dm-reply-shape"
| "matrix-dm-shared-session-notice"
| "matrix-dm-thread-reply-override"
| "matrix-dm-per-room-session-override"
| "matrix-room-autojoin-invite"
| "matrix-secondary-room-reply"
| "matrix-secondary-room-open-trigger"
| "matrix-reaction-notification"
| "matrix-restart-resume"
| "matrix-room-membership-loss"
| "matrix-homeserver-restart-resume"
| "matrix-mention-gating"
| "matrix-allowlist-block";
export type MatrixQaScenarioDefinition = LiveTransportScenarioDefinition<MatrixQaScenarioId> & {
configOverrides?: MatrixQaConfigOverrides;
topology?: MatrixQaTopologySpec;
};
export const MATRIX_QA_DRIVER_DM_ROOM_KEY = "driver-dm";
export const MATRIX_QA_DRIVER_DM_SHARED_ROOM_KEY = "driver-dm-shared";
export const MATRIX_QA_MEMBERSHIP_ROOM_KEY = "membership";
export const MATRIX_QA_SECONDARY_ROOM_KEY = "secondary";
function buildMatrixQaDmTopology(
rooms: Array<{
key: string;
name: string;
}>,
): MatrixQaTopologySpec {
return {
defaultRoomKey: "main",
rooms: rooms.map((room) => ({
key: room.key,
kind: "dm" as const,
members: ["driver", "sut"],
name: room.name,
})),
};
}
function buildMatrixQaSingleGroupTopology(params: {
key: string;
name: string;
requireMention: boolean;
}): MatrixQaTopologySpec {
return {
defaultRoomKey: "main",
rooms: [
{
key: params.key,
kind: "group",
members: ["driver", "observer", "sut"],
name: params.name,
requireMention: params.requireMention,
},
],
};
}
const MATRIX_QA_DRIVER_DM_TOPOLOGY = buildMatrixQaDmTopology([
{
key: MATRIX_QA_DRIVER_DM_ROOM_KEY,
name: "Matrix QA Driver/SUT DM",
},
]);
const MATRIX_QA_SHARED_DM_TOPOLOGY = buildMatrixQaDmTopology([
{
key: MATRIX_QA_DRIVER_DM_ROOM_KEY,
name: "Matrix QA Driver/SUT DM",
},
{
key: MATRIX_QA_DRIVER_DM_SHARED_ROOM_KEY,
name: "Matrix QA Driver/SUT Shared DM",
},
]);
const MATRIX_QA_SECONDARY_ROOM_TOPOLOGY = buildMatrixQaSingleGroupTopology({
key: MATRIX_QA_SECONDARY_ROOM_KEY,
name: "Matrix QA Secondary Room",
requireMention: true,
});
const MATRIX_QA_MEMBERSHIP_ROOM_TOPOLOGY = buildMatrixQaSingleGroupTopology({
key: MATRIX_QA_MEMBERSHIP_ROOM_KEY,
name: "Matrix QA Membership Room",
requireMention: true,
});
export const MATRIX_QA_SCENARIOS: MatrixQaScenarioDefinition[] = [
{
id: "matrix-thread-follow-up",
standardId: "thread-follow-up",
timeoutMs: 60_000,
title: "Matrix thread follow-up reply",
},
{
id: "matrix-thread-isolation",
standardId: "thread-isolation",
timeoutMs: 75_000,
title: "Matrix top-level reply stays out of prior thread",
},
{
id: "matrix-top-level-reply-shape",
standardId: "top-level-reply-shape",
timeoutMs: 45_000,
title: "Matrix top-level reply keeps replyToMode off",
},
{
id: "matrix-room-thread-reply-override",
timeoutMs: 45_000,
title: "Matrix threadReplies always keeps room replies threaded",
configOverrides: {
threadReplies: "always",
},
},
{
id: "matrix-dm-reply-shape",
timeoutMs: 45_000,
title: "Matrix DM reply stays top-level without a mention",
topology: MATRIX_QA_DRIVER_DM_TOPOLOGY,
},
{
id: "matrix-dm-shared-session-notice",
timeoutMs: 45_000,
title: "Matrix shared DM sessions emit a cross-room notice",
topology: MATRIX_QA_SHARED_DM_TOPOLOGY,
},
{
id: "matrix-dm-thread-reply-override",
timeoutMs: 45_000,
title: "Matrix DM thread override keeps DM replies threaded",
topology: MATRIX_QA_DRIVER_DM_TOPOLOGY,
configOverrides: {
dm: {
threadReplies: "always",
},
threadReplies: "off",
},
},
{
id: "matrix-dm-per-room-session-override",
timeoutMs: 45_000,
title: "Matrix DM per-room session override suppresses cross-room notices",
topology: MATRIX_QA_SHARED_DM_TOPOLOGY,
configOverrides: {
dm: {
sessionScope: "per-room",
},
},
},
{
id: "matrix-room-autojoin-invite",
timeoutMs: 60_000,
title: "Matrix invite auto-join accepts fresh group rooms",
configOverrides: {
autoJoin: "always",
groupPolicy: "open",
},
},
{
id: "matrix-secondary-room-reply",
timeoutMs: 45_000,
title: "Matrix secondary room reply stays scoped to that room",
topology: MATRIX_QA_SECONDARY_ROOM_TOPOLOGY,
},
{
id: "matrix-secondary-room-open-trigger",
timeoutMs: 45_000,
title: "Matrix secondary room can opt out of mention gating",
topology: MATRIX_QA_SECONDARY_ROOM_TOPOLOGY,
configOverrides: {
groupsByKey: {
[MATRIX_QA_SECONDARY_ROOM_KEY]: {
requireMention: false,
},
},
},
},
{
id: "matrix-reaction-notification",
standardId: "reaction-observation",
timeoutMs: 45_000,
title: "Matrix reactions on bot replies are observed",
},
{
id: "matrix-restart-resume",
standardId: "restart-resume",
timeoutMs: 60_000,
title: "Matrix lane resumes cleanly after gateway restart",
},
{
id: "matrix-room-membership-loss",
timeoutMs: 75_000,
title: "Matrix room membership loss recovers after re-invite",
topology: MATRIX_QA_MEMBERSHIP_ROOM_TOPOLOGY,
},
{
id: "matrix-homeserver-restart-resume",
timeoutMs: 75_000,
title: "Matrix lane resumes after homeserver restart",
},
{
id: "matrix-mention-gating",
standardId: "mention-gating",
timeoutMs: 8_000,
title: "Matrix room message without mention does not trigger",
},
{
id: "matrix-allowlist-block",
standardId: "allowlist-block",
timeoutMs: 8_000,
title: "Matrix allowlist blocks non-driver replies",
},
];
export const MATRIX_QA_STANDARD_SCENARIO_IDS = collectLiveTransportStandardScenarioCoverage({
alwaysOnStandardScenarioIds: ["canary"],
scenarios: MATRIX_QA_SCENARIOS,
});
export function findMatrixQaScenarios(ids?: string[]) {
return selectLiveTransportScenarios({
ids,
laneLabel: "Matrix",
scenarios: MATRIX_QA_SCENARIOS,
});
}
export function buildMatrixQaTopologyForScenarios(params: {
defaultRoomName: string;
scenarios: MatrixQaScenarioDefinition[];
}): MatrixQaTopologySpec {
return mergeMatrixQaTopologySpecs([
buildDefaultMatrixQaTopologySpec({
defaultRoomName: params.defaultRoomName,
}),
...params.scenarios.flatMap((scenario) => (scenario.topology ? [scenario.topology] : [])),
]);
}
export function resolveMatrixQaScenarioRoomId(
context: Pick<{ roomId: string; topology: MatrixQaProvisionedTopology }, "roomId" | "topology">,
roomKey?: string,
) {
if (!roomKey) {
return context.roomId;
}
return findMatrixQaProvisionedRoom(context.topology, roomKey).roomId;
}

File diff suppressed because it is too large Load Diff

View File

@@ -27,7 +27,12 @@ describe("matrix live qa scenarios", () => {
"matrix-thread-follow-up",
"matrix-thread-isolation",
"matrix-top-level-reply-shape",
"matrix-room-thread-reply-override",
"matrix-dm-reply-shape",
"matrix-dm-shared-session-notice",
"matrix-dm-thread-reply-override",
"matrix-dm-per-room-session-override",
"matrix-room-autojoin-invite",
"matrix-secondary-room-reply",
"matrix-secondary-room-open-trigger",
"matrix-reaction-notification",
@@ -405,6 +410,481 @@ describe("matrix live qa scenarios", () => {
);
});
it("uses room thread override scenarios against the main room", async () => {
const primeRoom = vi.fn().mockResolvedValue("driver-sync-start");
const sendTextMessage = vi.fn().mockResolvedValue("$room-thread-trigger");
const waitForRoomEvent = vi.fn().mockImplementation(async () => ({
event: {
kind: "message",
roomId: "!main:matrix-qa.test",
eventId: "$sut-reply",
sender: "@sut:matrix-qa.test",
type: "m.room.message",
body: String(sendTextMessage.mock.calls[0]?.[0]?.body).replace(
"@sut:matrix-qa.test reply with only this exact marker: ",
"",
),
relatesTo: {
relType: "m.thread",
eventId: "$room-thread-trigger",
inReplyToId: "$room-thread-trigger",
isFallingBack: true,
},
},
since: "driver-sync-next",
}));
createMatrixQaClient.mockReturnValue({
primeRoom,
sendTextMessage,
waitForRoomEvent,
});
const scenario = MATRIX_QA_SCENARIOS.find(
(entry) => entry.id === "matrix-room-thread-reply-override",
);
expect(scenario).toBeDefined();
await expect(
runMatrixQaScenario(scenario!, {
baseUrl: "http://127.0.0.1:28008/",
canary: undefined,
driverAccessToken: "driver-token",
driverUserId: "@driver:matrix-qa.test",
observedEvents: [],
observerAccessToken: "observer-token",
observerUserId: "@observer:matrix-qa.test",
roomId: "!main:matrix-qa.test",
restartGateway: undefined,
syncState: {},
sutAccessToken: "sut-token",
sutUserId: "@sut:matrix-qa.test",
timeoutMs: 8_000,
topology: {
defaultRoomId: "!main:matrix-qa.test",
defaultRoomKey: "main",
rooms: [],
},
}),
).resolves.toMatchObject({
artifacts: {
driverEventId: "$room-thread-trigger",
reply: {
relatesTo: {
relType: "m.thread",
eventId: "$room-thread-trigger",
},
},
},
});
});
it("uses DM thread override scenarios against the provisioned DM room", async () => {
const primeRoom = vi.fn().mockResolvedValue("driver-sync-start");
const sendTextMessage = vi.fn().mockResolvedValue("$dm-thread-trigger");
const waitForRoomEvent = vi.fn().mockImplementation(async () => ({
event: {
kind: "message",
roomId: "!dm:matrix-qa.test",
eventId: "$sut-reply",
sender: "@sut:matrix-qa.test",
type: "m.room.message",
body: String(sendTextMessage.mock.calls[0]?.[0]?.body).replace(
"reply with only this exact marker: ",
"",
),
relatesTo: {
relType: "m.thread",
eventId: "$dm-thread-trigger",
inReplyToId: "$dm-thread-trigger",
isFallingBack: true,
},
},
since: "driver-sync-next",
}));
createMatrixQaClient.mockReturnValue({
primeRoom,
sendTextMessage,
waitForRoomEvent,
});
const scenario = MATRIX_QA_SCENARIOS.find(
(entry) => entry.id === "matrix-dm-thread-reply-override",
);
expect(scenario).toBeDefined();
await expect(
runMatrixQaScenario(scenario!, {
baseUrl: "http://127.0.0.1:28008/",
canary: undefined,
driverAccessToken: "driver-token",
driverUserId: "@driver:matrix-qa.test",
observedEvents: [],
observerAccessToken: "observer-token",
observerUserId: "@observer:matrix-qa.test",
roomId: "!main:matrix-qa.test",
restartGateway: undefined,
syncState: {},
sutAccessToken: "sut-token",
sutUserId: "@sut:matrix-qa.test",
timeoutMs: 8_000,
topology: {
defaultRoomId: "!main:matrix-qa.test",
defaultRoomKey: "main",
rooms: [
{
key: scenarioTesting.MATRIX_QA_DRIVER_DM_ROOM_KEY,
kind: "dm",
memberRoles: ["driver", "sut"],
memberUserIds: ["@driver:matrix-qa.test", "@sut:matrix-qa.test"],
name: "DM",
requireMention: false,
roomId: "!dm:matrix-qa.test",
},
],
},
}),
).resolves.toMatchObject({
artifacts: {
driverEventId: "$dm-thread-trigger",
reply: {
relatesTo: {
relType: "m.thread",
eventId: "$dm-thread-trigger",
},
},
},
});
});
it("surfaces the shared DM session notice in the secondary DM room", async () => {
const primePrimaryRoom = vi.fn().mockResolvedValue("driver-primary-sync-start");
const sendPrimaryTextMessage = vi.fn().mockResolvedValue("$dm-primary-trigger");
const waitPrimaryReply = vi.fn().mockImplementation(async () => ({
event: {
kind: "message",
roomId: "!dm:matrix-qa.test",
eventId: "$sut-primary-reply",
sender: "@sut:matrix-qa.test",
type: "m.room.message",
body: String(sendPrimaryTextMessage.mock.calls[0]?.[0]?.body).replace(
"reply with only this exact marker: ",
"",
),
},
since: "driver-primary-sync-next",
}));
const primeSecondaryReplyRoom = vi.fn().mockResolvedValue("driver-secondary-reply-sync-start");
const sendSecondaryTextMessage = vi.fn().mockResolvedValue("$dm-secondary-trigger");
const waitSecondaryReply = vi.fn().mockImplementation(async () => ({
event: {
kind: "message",
roomId: "!dm-shared:matrix-qa.test",
eventId: "$sut-secondary-reply",
sender: "@sut:matrix-qa.test",
type: "m.room.message",
body: String(sendSecondaryTextMessage.mock.calls[0]?.[0]?.body).replace(
"reply with only this exact marker: ",
"",
),
},
since: "driver-secondary-sync-next",
}));
const primeSecondaryNoticeRoom = vi
.fn()
.mockResolvedValue("driver-secondary-notice-sync-start");
const waitSecondaryNotice = vi.fn().mockImplementation(async () => ({
matched: true,
event: {
kind: "notice",
roomId: "!dm-shared:matrix-qa.test",
eventId: "$shared-notice",
sender: "@sut:matrix-qa.test",
type: "m.room.message",
body: "This Matrix DM is sharing a session with another Matrix DM room. Set channels.matrix.dm.sessionScope to per-room to isolate each Matrix DM room.",
},
since: "driver-secondary-notice-sync-next",
}));
createMatrixQaClient
.mockReturnValueOnce({
primeRoom: primePrimaryRoom,
sendTextMessage: sendPrimaryTextMessage,
waitForRoomEvent: waitPrimaryReply,
})
.mockReturnValueOnce({
primeRoom: primeSecondaryReplyRoom,
sendTextMessage: sendSecondaryTextMessage,
waitForRoomEvent: waitSecondaryReply,
})
.mockReturnValueOnce({
primeRoom: primeSecondaryNoticeRoom,
waitForOptionalRoomEvent: waitSecondaryNotice,
});
const scenario = MATRIX_QA_SCENARIOS.find(
(entry) => entry.id === "matrix-dm-shared-session-notice",
);
expect(scenario).toBeDefined();
await expect(
runMatrixQaScenario(scenario!, {
baseUrl: "http://127.0.0.1:28008/",
canary: undefined,
driverAccessToken: "driver-token",
driverUserId: "@driver:matrix-qa.test",
observedEvents: [],
observerAccessToken: "observer-token",
observerUserId: "@observer:matrix-qa.test",
roomId: "!main:matrix-qa.test",
restartGateway: undefined,
syncState: {},
sutAccessToken: "sut-token",
sutUserId: "@sut:matrix-qa.test",
timeoutMs: 8_000,
topology: {
defaultRoomId: "!main:matrix-qa.test",
defaultRoomKey: "main",
rooms: [
{
key: scenarioTesting.MATRIX_QA_DRIVER_DM_ROOM_KEY,
kind: "dm",
memberRoles: ["driver", "sut"],
memberUserIds: ["@driver:matrix-qa.test", "@sut:matrix-qa.test"],
name: "DM",
requireMention: false,
roomId: "!dm:matrix-qa.test",
},
{
key: scenarioTesting.MATRIX_QA_DRIVER_DM_SHARED_ROOM_KEY,
kind: "dm",
memberRoles: ["driver", "sut"],
memberUserIds: ["@driver:matrix-qa.test", "@sut:matrix-qa.test"],
name: "Shared DM",
requireMention: false,
roomId: "!dm-shared:matrix-qa.test",
},
],
},
}),
).resolves.toMatchObject({
artifacts: {
noticeEventId: "$shared-notice",
roomKey: scenarioTesting.MATRIX_QA_DRIVER_DM_SHARED_ROOM_KEY,
},
});
expect(sendPrimaryTextMessage).toHaveBeenCalledWith({
body: expect.stringContaining("reply with only this exact marker:"),
roomId: "!dm:matrix-qa.test",
});
expect(sendSecondaryTextMessage).toHaveBeenCalledWith({
body: expect.stringContaining("reply with only this exact marker:"),
roomId: "!dm-shared:matrix-qa.test",
});
expect(waitSecondaryNotice).toHaveBeenCalledWith(
expect.objectContaining({
roomId: "!dm-shared:matrix-qa.test",
}),
);
});
it("suppresses the shared DM notice when sessionScope is per-room", async () => {
const primePrimaryRoom = vi.fn().mockResolvedValue("driver-primary-sync-start");
const sendPrimaryTextMessage = vi.fn().mockResolvedValue("$dm-primary-trigger");
const waitPrimaryReply = vi.fn().mockImplementation(async () => ({
event: {
kind: "message",
roomId: "!dm:matrix-qa.test",
eventId: "$sut-primary-reply",
sender: "@sut:matrix-qa.test",
type: "m.room.message",
body: String(sendPrimaryTextMessage.mock.calls[0]?.[0]?.body).replace(
"reply with only this exact marker: ",
"",
),
},
since: "driver-primary-sync-next",
}));
const primeSecondaryReplyRoom = vi.fn().mockResolvedValue("driver-secondary-reply-sync-start");
const sendSecondaryTextMessage = vi.fn().mockResolvedValue("$dm-secondary-trigger");
const waitSecondaryReply = vi.fn().mockImplementation(async () => ({
event: {
kind: "message",
roomId: "!dm-shared:matrix-qa.test",
eventId: "$sut-secondary-reply",
sender: "@sut:matrix-qa.test",
type: "m.room.message",
body: String(sendSecondaryTextMessage.mock.calls[0]?.[0]?.body).replace(
"reply with only this exact marker: ",
"",
),
},
since: "driver-secondary-sync-next",
}));
const primeSecondaryNoticeRoom = vi
.fn()
.mockResolvedValue("driver-secondary-notice-sync-start");
const waitSecondaryNotice = vi.fn().mockImplementation(async () => ({
matched: false,
since: "driver-secondary-notice-sync-next",
}));
createMatrixQaClient
.mockReturnValueOnce({
primeRoom: primePrimaryRoom,
sendTextMessage: sendPrimaryTextMessage,
waitForRoomEvent: waitPrimaryReply,
})
.mockReturnValueOnce({
primeRoom: primeSecondaryReplyRoom,
sendTextMessage: sendSecondaryTextMessage,
waitForRoomEvent: waitSecondaryReply,
})
.mockReturnValueOnce({
primeRoom: primeSecondaryNoticeRoom,
waitForOptionalRoomEvent: waitSecondaryNotice,
});
const scenario = MATRIX_QA_SCENARIOS.find(
(entry) => entry.id === "matrix-dm-per-room-session-override",
);
expect(scenario).toBeDefined();
await expect(
runMatrixQaScenario(scenario!, {
baseUrl: "http://127.0.0.1:28008/",
canary: undefined,
driverAccessToken: "driver-token",
driverUserId: "@driver:matrix-qa.test",
observedEvents: [],
observerAccessToken: "observer-token",
observerUserId: "@observer:matrix-qa.test",
roomId: "!main:matrix-qa.test",
restartGateway: undefined,
syncState: {},
sutAccessToken: "sut-token",
sutUserId: "@sut:matrix-qa.test",
timeoutMs: 8_000,
topology: {
defaultRoomId: "!main:matrix-qa.test",
defaultRoomKey: "main",
rooms: [
{
key: scenarioTesting.MATRIX_QA_DRIVER_DM_ROOM_KEY,
kind: "dm",
memberRoles: ["driver", "sut"],
memberUserIds: ["@driver:matrix-qa.test", "@sut:matrix-qa.test"],
name: "DM",
requireMention: false,
roomId: "!dm:matrix-qa.test",
},
{
key: scenarioTesting.MATRIX_QA_DRIVER_DM_SHARED_ROOM_KEY,
kind: "dm",
memberRoles: ["driver", "sut"],
memberUserIds: ["@driver:matrix-qa.test", "@sut:matrix-qa.test"],
name: "Shared DM",
requireMention: false,
roomId: "!dm-shared:matrix-qa.test",
},
],
},
}),
).resolves.toMatchObject({
artifacts: {
roomKey: scenarioTesting.MATRIX_QA_DRIVER_DM_SHARED_ROOM_KEY,
},
});
expect(waitSecondaryNotice).toHaveBeenCalledTimes(1);
});
it("auto-joins a freshly invited Matrix group room before replying", async () => {
const primeRoom = vi.fn().mockResolvedValue("driver-sync-start");
const createPrivateRoom = vi.fn().mockResolvedValue("!autojoin:matrix-qa.test");
const sendTextMessage = vi.fn().mockResolvedValue("$autojoin-trigger");
const waitForRoomEvent = vi
.fn()
.mockImplementationOnce(async () => ({
event: {
kind: "membership",
roomId: "!autojoin:matrix-qa.test",
eventId: "$autojoin-join",
sender: "@sut:matrix-qa.test",
stateKey: "@sut:matrix-qa.test",
type: "m.room.member",
membership: "join",
},
since: "driver-sync-join",
}))
.mockImplementationOnce(async () => ({
event: {
kind: "message",
roomId: "!autojoin:matrix-qa.test",
eventId: "$sut-autojoin-reply",
sender: "@sut:matrix-qa.test",
type: "m.room.message",
body: String(sendTextMessage.mock.calls[0]?.[0]?.body).replace(
"@sut:matrix-qa.test reply with only this exact marker: ",
"",
),
},
since: "driver-sync-next",
}));
createMatrixQaClient.mockReturnValue({
primeRoom,
createPrivateRoom,
sendTextMessage,
waitForRoomEvent,
});
const scenario = MATRIX_QA_SCENARIOS.find(
(entry) => entry.id === "matrix-room-autojoin-invite",
);
expect(scenario).toBeDefined();
await expect(
runMatrixQaScenario(scenario!, {
baseUrl: "http://127.0.0.1:28008/",
canary: undefined,
driverAccessToken: "driver-token",
driverUserId: "@driver:matrix-qa.test",
observedEvents: [],
observerAccessToken: "observer-token",
observerUserId: "@observer:matrix-qa.test",
roomId: "!main:matrix-qa.test",
restartGateway: undefined,
syncState: {},
sutAccessToken: "sut-token",
sutUserId: "@sut:matrix-qa.test",
timeoutMs: 8_000,
topology: {
defaultRoomId: "!main:matrix-qa.test",
defaultRoomKey: "main",
rooms: [],
},
}),
).resolves.toMatchObject({
artifacts: {
joinedRoomId: "!autojoin:matrix-qa.test",
membershipJoinEventId: "$autojoin-join",
},
});
expect(createPrivateRoom).toHaveBeenCalledWith({
inviteUserIds: ["@observer:matrix-qa.test", "@sut:matrix-qa.test"],
name: expect.stringContaining("Matrix QA AutoJoin"),
});
expect(sendTextMessage).toHaveBeenCalledWith({
body: expect.stringContaining("@sut:matrix-qa.test reply with only this exact marker:"),
mentionUserIds: ["@sut:matrix-qa.test"],
roomId: "!autojoin:matrix-qa.test",
});
});
it("runs the secondary-room scenario against the provisioned secondary room", async () => {
const primeRoom = vi.fn().mockResolvedValue("driver-sync-start");
const sendTextMessage = vi.fn().mockResolvedValue("$secondary-trigger");

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,10 @@
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
import { describe, expect, it } from "vitest";
import { buildMatrixQaConfig } from "./config.js";
import {
buildMatrixQaConfig,
buildMatrixQaConfigSnapshot,
summarizeMatrixQaConfigSnapshot,
} from "./config.js";
import type { MatrixQaProvisionedTopology } from "./topology.js";
describe("matrix qa config", () => {
@@ -78,6 +82,14 @@ describe("matrix qa config", () => {
driverUserId: "@driver:matrix-qa.test",
homeserver: "http://127.0.0.1:28008/",
overrides: {
autoJoin: "allowlist",
autoJoinAllowlist: [" !dm:matrix-qa.test ", "#ops:matrix-qa.test"],
blockStreaming: true,
dm: {
sessionScope: "per-room",
threadReplies: "off",
},
encryption: true,
groupAllowFrom: ["@driver:matrix-qa.test", "@observer:matrix-qa.test"],
groupsByKey: {
secondary: {
@@ -85,6 +97,7 @@ describe("matrix qa config", () => {
},
},
replyToMode: "all",
streaming: "quiet",
threadReplies: "always",
},
sutAccessToken: "sut-token",
@@ -94,16 +107,76 @@ describe("matrix qa config", () => {
});
expect(next.channels?.matrix?.accounts?.sut).toMatchObject({
autoJoin: "allowlist",
autoJoinAllowlist: ["!dm:matrix-qa.test", "#ops:matrix-qa.test"],
blockStreaming: true,
dm: {
sessionScope: "per-room",
threadReplies: "off",
},
encryption: true,
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",
streaming: "quiet",
threadReplies: "always",
});
});
it("builds an effective Matrix QA config snapshot for reporting", () => {
const snapshot = buildMatrixQaConfigSnapshot({
driverUserId: "@driver:matrix-qa.test",
overrides: {
autoJoin: "allowlist",
autoJoinAllowlist: ["!ops:matrix-qa.test"],
blockStreaming: true,
dm: {
sessionScope: "per-room",
},
groupPolicy: "open",
streaming: true,
},
sutUserId: "@sut:matrix-qa.test",
topology,
});
expect(snapshot).toEqual({
autoJoin: "allowlist",
autoJoinAllowlist: ["!ops:matrix-qa.test"],
blockStreaming: true,
dm: {
allowFrom: ["@driver:matrix-qa.test"],
enabled: true,
policy: "allowlist",
sessionScope: "per-room",
threadReplies: "inbound",
},
encryption: false,
groupAllowFrom: ["@driver:matrix-qa.test"],
groupPolicy: "open",
groupsByKey: {
main: {
enabled: true,
requireMention: true,
roomId: "!main:matrix-qa.test",
},
secondary: {
enabled: true,
requireMention: true,
roomId: "!secondary:matrix-qa.test",
},
},
replyToMode: "off",
streaming: "partial",
threadReplies: "inbound",
});
expect(summarizeMatrixQaConfigSnapshot(snapshot)).toContain("autoJoin=allowlist");
expect(summarizeMatrixQaConfigSnapshot(snapshot)).toContain("streaming=partial");
});
it("rejects unknown room-key overrides", () => {
expect(() =>
buildMatrixQaConfig({} as OpenClawConfig, {

View File

@@ -1,10 +1,12 @@
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 MatrixQaReplyToMode = "off" | "first" | "all" | "batched";
export type MatrixQaThreadRepliesMode = "off" | "inbound" | "always";
export type MatrixQaDmPolicy = "allowlist" | "disabled" | "open" | "pairing";
export type MatrixQaGroupPolicy = "allowlist" | "disabled" | "open";
export type MatrixQaAutoJoinMode = "allowlist" | "always" | "off";
export type MatrixQaStreamingMode = "off" | "partial" | "quiet";
export type MatrixQaGroupConfigOverrides = {
enabled?: boolean;
@@ -20,7 +22,8 @@ export type MatrixQaDmConfigOverrides = {
};
export type MatrixQaConfigOverrides = {
autoJoin?: "allowlist" | "always" | "off";
autoJoin?: MatrixQaAutoJoinMode;
autoJoinAllowlist?: string[];
blockStreaming?: boolean;
dm?: MatrixQaDmConfigOverrides;
encryption?: boolean;
@@ -32,7 +35,48 @@ export type MatrixQaConfigOverrides = {
threadReplies?: MatrixQaThreadRepliesMode;
};
function resolveMatrixQaGroupEntries(params: {
export type MatrixQaConfigSnapshot = {
autoJoin: MatrixQaAutoJoinMode;
autoJoinAllowlist: string[];
blockStreaming: boolean;
dm: {
allowFrom: string[];
enabled: boolean;
policy: MatrixQaDmPolicy;
sessionScope: "per-room" | "per-user";
threadReplies: MatrixQaThreadRepliesMode;
};
encryption: boolean;
groupAllowFrom: string[];
groupPolicy: MatrixQaGroupPolicy;
groupsByKey: Record<
string,
{
enabled: boolean;
requireMention: boolean;
roomId: string;
}
>;
replyToMode: MatrixQaReplyToMode;
streaming: MatrixQaStreamingMode;
threadReplies: MatrixQaThreadRepliesMode;
};
type MatrixQaAccountDmConfig =
| { enabled: false }
| {
allowFrom: string[];
enabled: true;
policy: MatrixQaDmPolicy;
sessionScope?: "per-room" | "per-user";
threadReplies?: MatrixQaThreadRepliesMode;
};
function normalizeMatrixQaAllowlist(entries?: string[]) {
return [...new Set((entries ?? []).map((entry) => entry.trim()).filter(Boolean))];
}
function resolveMatrixQaGroupSnapshots(params: {
overrides?: MatrixQaConfigOverrides;
topology: MatrixQaProvisionedTopology;
}) {
@@ -50,8 +94,9 @@ function resolveMatrixQaGroupEntries(params: {
groupRooms.map((room) => {
const override = groupsByKey[room.key];
return [
room.roomId,
room.key,
{
roomId: room.roomId,
enabled: override?.enabled ?? true,
requireMention: override?.requireMention ?? room.requireMention,
},
@@ -60,6 +105,20 @@ function resolveMatrixQaGroupEntries(params: {
);
}
function buildMatrixQaGroupEntries(
groupsByKey: MatrixQaConfigSnapshot["groupsByKey"],
): Record<string, { enabled: boolean; requireMention: boolean }> {
return Object.fromEntries(
Object.values(groupsByKey).map((group) => [
group.roomId,
{
enabled: group.enabled,
requireMention: group.requireMention,
},
]),
);
}
function resolveMatrixQaDmAllowFrom(params: {
driverUserId: string;
overrides?: MatrixQaConfigOverrides;
@@ -67,19 +126,16 @@ function resolveMatrixQaDmAllowFrom(params: {
topology: MatrixQaProvisionedTopology;
}) {
if (params.overrides?.dm?.allowFrom) {
return [...params.overrides.dm.allowFrom];
return normalizeMatrixQaAllowlist(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)),
),
];
const dmParticipantUserIds = params.topology.rooms
.filter((room) => room.kind === "dm")
.flatMap((room) => room.memberUserIds.filter((userId) => userId !== params.sutUserId));
const dmAllowFrom = [...new Set(dmParticipantUserIds)];
return dmAllowFrom.length > 0 ? dmAllowFrom : [params.driverUserId];
}
function resolveMatrixQaDmConfig(params: {
function resolveMatrixQaDmConfigSnapshot(params: {
driverUserId: string;
overrides?: MatrixQaConfigOverrides;
sutUserId: string;
@@ -87,20 +143,99 @@ function resolveMatrixQaDmConfig(params: {
}) {
const hasDmRooms = params.topology.rooms.some((room) => room.kind === "dm");
const dmOverrides = params.overrides?.dm;
const enabled = hasDmRooms || dmOverrides?.enabled === true;
return {
allowFrom: enabled ? resolveMatrixQaDmAllowFrom(params) : [],
enabled,
policy: dmOverrides?.policy ?? "allowlist",
sessionScope: dmOverrides?.sessionScope ?? "per-user",
threadReplies: dmOverrides?.threadReplies ?? params.overrides?.threadReplies ?? "inbound",
};
}
if (!hasDmRooms && dmOverrides?.enabled !== true) {
function resolveMatrixQaStreamingMode(
value: MatrixQaConfigOverrides["streaming"],
): MatrixQaStreamingMode {
if (value === true || value === "partial") {
return "partial";
}
if (value === "quiet") {
return "quiet";
}
return "off";
}
function resolveMatrixQaAutoJoinAllowlist(params: { overrides?: MatrixQaConfigOverrides }) {
if (params.overrides?.autoJoin !== "allowlist") {
return [];
}
return normalizeMatrixQaAllowlist(params.overrides.autoJoinAllowlist);
}
function formatMatrixQaBoolean(value: boolean) {
return value ? "true" : "false";
}
function buildMatrixQaAccountDmConfig(params: {
dmOverrides?: MatrixQaConfigOverrides["dm"];
snapshot: MatrixQaConfigSnapshot;
}): MatrixQaAccountDmConfig {
if (!params.snapshot.dm.enabled) {
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 } : {}),
allowFrom: params.snapshot.dm.allowFrom,
enabled: true,
policy: params.snapshot.dm.policy,
...(params.dmOverrides?.sessionScope ? { sessionScope: params.snapshot.dm.sessionScope } : {}),
...(params.dmOverrides?.threadReplies
? { threadReplies: params.snapshot.dm.threadReplies }
: {}),
};
}
export function buildMatrixQaConfigSnapshot(params: {
driverUserId: string;
overrides?: MatrixQaConfigOverrides;
sutUserId: string;
topology: MatrixQaProvisionedTopology;
}): MatrixQaConfigSnapshot {
return {
autoJoin: params.overrides?.autoJoin ?? "off",
autoJoinAllowlist: resolveMatrixQaAutoJoinAllowlist(params),
blockStreaming: params.overrides?.blockStreaming ?? false,
dm: resolveMatrixQaDmConfigSnapshot(params),
encryption: params.overrides?.encryption ?? false,
groupAllowFrom: normalizeMatrixQaAllowlist(
params.overrides?.groupAllowFrom ?? [params.driverUserId],
),
groupPolicy: params.overrides?.groupPolicy ?? "allowlist",
groupsByKey: resolveMatrixQaGroupSnapshots({
overrides: params.overrides,
topology: params.topology,
}),
replyToMode: params.overrides?.replyToMode ?? "off",
streaming: resolveMatrixQaStreamingMode(params.overrides?.streaming),
threadReplies: params.overrides?.threadReplies ?? "inbound",
};
}
export function summarizeMatrixQaConfigSnapshot(snapshot: MatrixQaConfigSnapshot) {
return [
`replyToMode=${snapshot.replyToMode}`,
`threadReplies=${snapshot.threadReplies}`,
`dm.enabled=${formatMatrixQaBoolean(snapshot.dm.enabled)}`,
`dm.policy=${snapshot.dm.policy}`,
`dm.sessionScope=${snapshot.dm.sessionScope}`,
`dm.threadReplies=${snapshot.dm.threadReplies}`,
`streaming=${snapshot.streaming}`,
`blockStreaming=${formatMatrixQaBoolean(snapshot.blockStreaming)}`,
`autoJoin=${snapshot.autoJoin}`,
`encryption=${formatMatrixQaBoolean(snapshot.encryption)}`,
].join(", ");
}
export function buildMatrixQaConfig(
baseCfg: OpenClawConfig,
params: {
@@ -115,10 +250,18 @@ export function buildMatrixQaConfig(
},
): OpenClawConfig {
const pluginAllow = [...new Set([...(baseCfg.plugins?.allow ?? []), "matrix"])];
const groups = resolveMatrixQaGroupEntries({
const snapshot = buildMatrixQaConfigSnapshot({
driverUserId: params.driverUserId,
overrides: params.overrides,
sutUserId: params.sutUserId,
topology: params.topology,
});
const groups = buildMatrixQaGroupEntries(snapshot.groupsByKey);
const dmOverrides = params.overrides?.dm;
const dm = buildMatrixQaAccountDmConfig({
dmOverrides,
snapshot,
});
return {
...baseCfg,
@@ -141,22 +284,25 @@ export function buildMatrixQaConfig(
[params.sutAccountId]: {
accessToken: params.sutAccessToken,
...(params.sutDeviceId ? { deviceId: params.sutDeviceId } : {}),
dm: resolveMatrixQaDmConfig(params),
dm,
enabled: true,
encryption: params.overrides?.encryption ?? false,
groupAllowFrom: params.overrides?.groupAllowFrom ?? [params.driverUserId],
groupPolicy: params.overrides?.groupPolicy ?? "allowlist",
encryption: snapshot.encryption,
groupAllowFrom: snapshot.groupAllowFrom,
groupPolicy: snapshot.groupPolicy,
...(Object.keys(groups).length > 0 ? { groups } : {}),
homeserver: params.homeserver,
network: {
dangerouslyAllowPrivateNetwork: true,
},
replyToMode: params.overrides?.replyToMode ?? "off",
threadReplies: params.overrides?.threadReplies ?? "inbound",
replyToMode: snapshot.replyToMode,
threadReplies: snapshot.threadReplies,
userId: params.sutUserId,
...(params.overrides?.autoJoin ? { autoJoin: params.overrides.autoJoin } : {}),
...(snapshot.autoJoin !== "off" ? { autoJoin: snapshot.autoJoin } : {}),
...(snapshot.autoJoin === "allowlist" && snapshot.autoJoinAllowlist.length > 0
? { autoJoinAllowlist: snapshot.autoJoinAllowlist }
: {}),
...(params.overrides?.blockStreaming !== undefined
? { blockStreaming: params.overrides.blockStreaming }
? { blockStreaming: snapshot.blockStreaming }
: {}),
...(params.overrides?.streaming !== undefined
? { streaming: params.overrides.streaming }