Skip to content

Commit 7d7dc75

Browse files
committed
QA: speed up Matrix live lane
1 parent 2e2cbdd commit 7d7dc75

3 files changed

Lines changed: 128 additions & 74 deletions

File tree

extensions/qa-matrix/src/runners/contract/runtime.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,26 @@ describe("matrix live qa runtime", () => {
325325
});
326326
});
327327

328+
it("batches Matrix scenarios by config key while preserving stable in-group order", () => {
329+
const scenarios = liveTesting.findMatrixQaScenarios([
330+
"matrix-top-level-reply-shape",
331+
"matrix-room-thread-reply-override",
332+
"matrix-thread-follow-up",
333+
"matrix-room-quiet-streaming-preview",
334+
"matrix-reaction-notification",
335+
]);
336+
337+
expect(
338+
liveTesting.scheduleMatrixQaScenariosByConfig(scenarios).map(({ scenario }) => scenario.id),
339+
).toEqual([
340+
"matrix-thread-follow-up",
341+
"matrix-top-level-reply-shape",
342+
"matrix-reaction-notification",
343+
"matrix-room-thread-reply-override",
344+
"matrix-room-quiet-streaming-preview",
345+
]);
346+
});
347+
328348
it("treats only connected, healthy Matrix accounts as ready", () => {
329349
expect(liveTesting.isMatrixAccountReady({ running: true, connected: true })).toBe(true);
330350
expect(liveTesting.isMatrixAccountReady({ running: true, connected: false })).toBe(false);

extensions/qa-matrix/src/runners/contract/runtime.ts

Lines changed: 53 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,11 @@ type MatrixQaScenarioResult = {
6161
title: string;
6262
};
6363

64+
type MatrixQaScheduledScenario = {
65+
originalIndex: number;
66+
scenario: (typeof MATRIX_QA_SCENARIOS)[number];
67+
};
68+
6469
type MatrixQaScenarioConfigEntry = MatrixQaSummary["config"]["scenarios"][number];
6570

6671
type MatrixQaSummary = {
@@ -178,6 +183,25 @@ function buildMatrixQaScenarioResult(params: {
178183
};
179184
}
180185

186+
function scheduleMatrixQaScenariosByConfig(
187+
scenarios: readonly (typeof MATRIX_QA_SCENARIOS)[number][],
188+
): MatrixQaScheduledScenario[] {
189+
const grouped = new Map<string, MatrixQaScheduledScenario[]>();
190+
191+
scenarios.forEach((scenario, originalIndex) => {
192+
const configKey = buildMatrixQaGatewayConfigKey(scenario.configOverrides);
193+
const existing = grouped.get(configKey);
194+
const scheduled = { originalIndex, scenario };
195+
if (existing) {
196+
existing.push(scheduled);
197+
return;
198+
}
199+
grouped.set(configKey, [scheduled]);
200+
});
201+
202+
return [...grouped.values()].flat();
203+
}
204+
181205
export type MatrixQaRunResult = {
182206
observedEventsPath: string;
183207
outputDir: string;
@@ -368,7 +392,9 @@ export async function runMatrixQaLive(params: {
368392
].join("\n"),
369393
},
370394
];
371-
const scenarioResults: MatrixQaScenarioResult[] = [];
395+
const scenarioResults: Array<MatrixQaScenarioResult | undefined> = Array.from({
396+
length: scenarios.length,
397+
});
372398
const cleanupErrors: string[] = [];
373399
let canaryArtifact: MatrixQaCanaryArtifact | undefined;
374400
let gatewayHarness: MatrixQaLiveLaneGatewayHarness | null = null;
@@ -388,6 +414,8 @@ export async function runMatrixQaLive(params: {
388414
const defaultConfigSnapshot = buildMatrixQaConfigSnapshot(gatewayConfigParams);
389415
const scenarioConfigSnapshots: MatrixQaScenarioConfigEntry[] = [];
390416

417+
const scheduledScenarios = scheduleMatrixQaScenariosByConfig(scenarios);
418+
391419
try {
392420
const ensureGatewayHarness = async (overrides?: MatrixQaConfigOverrides) => {
393421
const nextKey = buildMatrixQaGatewayConfigKey(overrides);
@@ -460,13 +488,13 @@ export async function runMatrixQaLive(params: {
460488
}
461489

462490
if (!canaryFailed) {
463-
for (const scenario of scenarios) {
491+
for (const { scenario, originalIndex } of scheduledScenarios) {
464492
const { entry: scenarioConfigEntry, summary: scenarioConfigSummary } =
465493
buildMatrixQaScenarioConfigEntry({
466494
gatewayConfigParams,
467495
scenario,
468496
});
469-
scenarioConfigSnapshots.push(scenarioConfigEntry);
497+
scenarioConfigSnapshots[originalIndex] = scenarioConfigEntry;
470498
try {
471499
const scenarioGateway = await ensureGatewayHarness(scenario.configOverrides);
472500
const result = await runMatrixQaScenario(scenario, {
@@ -497,24 +525,20 @@ export async function runMatrixQaLive(params: {
497525
timeoutMs: scenario.timeoutMs,
498526
topology: provisioning.topology,
499527
});
500-
scenarioResults.push(
501-
buildMatrixQaScenarioResult({
502-
artifacts: result.artifacts,
503-
configSummary: scenarioConfigSummary,
504-
details: result.details,
505-
scenario,
506-
status: "pass",
507-
}),
508-
);
528+
scenarioResults[originalIndex] = buildMatrixQaScenarioResult({
529+
artifacts: result.artifacts,
530+
configSummary: scenarioConfigSummary,
531+
details: result.details,
532+
scenario,
533+
status: "pass",
534+
});
509535
} catch (error) {
510-
scenarioResults.push(
511-
buildMatrixQaScenarioResult({
512-
configSummary: scenarioConfigSummary,
513-
details: formatErrorMessage(error),
514-
scenario,
515-
status: "fail",
516-
}),
517-
);
536+
scenarioResults[originalIndex] = buildMatrixQaScenarioResult({
537+
configSummary: scenarioConfigSummary,
538+
details: formatErrorMessage(error),
539+
scenario,
540+
status: "fail",
541+
});
518542
}
519543
}
520544
}
@@ -532,6 +556,9 @@ export async function runMatrixQaLive(params: {
532556
appendLiveLaneIssue(cleanupErrors, "Matrix harness cleanup", error);
533557
}
534558
}
559+
const completedScenarioResults = scenarioResults.filter(
560+
(scenario): scenario is MatrixQaScenarioResult => scenario !== undefined,
561+
);
535562
if (cleanupErrors.length > 0) {
536563
checks.push({
537564
name: "Matrix cleanup",
@@ -555,7 +582,7 @@ export async function runMatrixQaLive(params: {
555582
startedAt: startedAtDate,
556583
finishedAt: finishedAtDate,
557584
checks,
558-
scenarios: scenarioResults.map((scenario) => ({
585+
scenarios: completedScenarioResults.map((scenario) => ({
559586
details: scenario.details,
560587
name: scenario.title,
561588
status: scenario.status,
@@ -592,7 +619,7 @@ export async function runMatrixQaLive(params: {
592619
serverName: harness.serverName,
593620
},
594621
observedEventCount: observedEvents.length,
595-
scenarios: scenarioResults,
622+
scenarios: completedScenarioResults,
596623
startedAt,
597624
sutAccountId,
598625
userIds: {
@@ -623,7 +650,7 @@ export async function runMatrixQaLive(params: {
623650
const failedChecks = checks.filter(
624651
(check) => check.status === "fail" && check.name !== "Matrix cleanup",
625652
);
626-
const failedScenarios = scenarioResults.filter((scenario) => scenario.status === "fail");
653+
const failedScenarios = completedScenarioResults.filter((scenario) => scenario.status === "fail");
627654
if (failedChecks.length > 0 || failedScenarios.length > 0) {
628655
throw new Error(
629656
buildLiveLaneArtifactsError({
@@ -651,16 +678,18 @@ export async function runMatrixQaLive(params: {
651678
observedEventsPath,
652679
outputDir,
653680
reportPath,
654-
scenarios: scenarioResults,
681+
scenarios: completedScenarioResults,
655682
summaryPath,
656683
};
657684
}
658685

659686
export const __testing = {
660687
buildMatrixQaSummary,
688+
scheduleMatrixQaScenariosByConfig,
661689
MATRIX_QA_SCENARIOS,
662690
buildMatrixQaConfig,
663691
buildMatrixQaConfigSnapshot,
692+
findMatrixQaScenarios,
664693
isMatrixAccountReady,
665694
resolveMatrixQaModels,
666695
summarizeMatrixQaConfigSnapshot,

extensions/qa-matrix/src/substrate/client.ts

Lines changed: 55 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { randomUUID } from "node:crypto";
2+
import { setTimeout as sleep } from "node:timers/promises";
23
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
34
import type { MatrixQaObservedEvent } from "./events.js";
45
import { requestMatrixJson, type MatrixQaFetchLike } from "./request.js";
@@ -474,7 +475,7 @@ async function joinRoomWithRetry(params: {
474475
return;
475476
} catch (error) {
476477
lastError = error;
477-
await new Promise((resolve) => setTimeout(resolve, 300 * attempt));
478+
await sleep(300 * attempt);
478479
}
479480
}
480481
throw new Error(`Matrix join retry failed: ${formatErrorMessage(lastError)}`);
@@ -504,40 +505,42 @@ async function provisionMatrixQaTopology(params: {
504505
fetchImpl?: MatrixQaFetchLike;
505506
spec: MatrixQaTopologySpec;
506507
}): Promise<MatrixQaProvisionedTopology> {
507-
const rooms = [];
508-
509-
for (const room of params.spec.rooms) {
510-
const members = resolveTopologyMemberAccounts(params.accounts, room.members);
511-
const creator = members[0];
512-
const invitees = members.slice(1);
513-
const creatorClient = createMatrixQaClient({
514-
accessToken: creator.account.accessToken,
515-
baseUrl: params.baseUrl,
516-
fetchImpl: params.fetchImpl,
517-
});
518-
const roomId = await creatorClient.createPrivateRoom({
519-
inviteUserIds: invitees.map((entry) => entry.account.userId),
520-
isDirect: room.kind === "dm",
521-
name: room.name,
522-
});
523-
for (const invitee of invitees) {
524-
await joinRoomWithRetry({
525-
accessToken: invitee.account.accessToken,
508+
const rooms = await Promise.all(
509+
params.spec.rooms.map(async (room) => {
510+
const members = resolveTopologyMemberAccounts(params.accounts, room.members);
511+
const creator = members[0];
512+
const invitees = members.slice(1);
513+
const creatorClient = createMatrixQaClient({
514+
accessToken: creator.account.accessToken,
526515
baseUrl: params.baseUrl,
527516
fetchImpl: params.fetchImpl,
528-
roomId,
529517
});
530-
}
531-
rooms.push({
532-
key: room.key,
533-
kind: room.kind,
534-
memberRoles: members.map((entry) => entry.role),
535-
memberUserIds: members.map((entry) => entry.account.userId),
536-
name: room.name,
537-
requireMention: resolveProvisionedRoomRequireMention(room),
538-
roomId,
539-
});
540-
}
518+
const roomId = await creatorClient.createPrivateRoom({
519+
inviteUserIds: invitees.map((entry) => entry.account.userId),
520+
isDirect: room.kind === "dm",
521+
name: room.name,
522+
});
523+
await Promise.all(
524+
invitees.map((invitee) =>
525+
joinRoomWithRetry({
526+
accessToken: invitee.account.accessToken,
527+
baseUrl: params.baseUrl,
528+
fetchImpl: params.fetchImpl,
529+
roomId,
530+
}),
531+
),
532+
);
533+
return {
534+
key: room.key,
535+
kind: room.kind,
536+
memberRoles: members.map((entry) => entry.role),
537+
memberUserIds: members.map((entry) => entry.account.userId),
538+
name: room.name,
539+
requireMention: resolveProvisionedRoomRequireMention(room),
540+
roomId,
541+
};
542+
}),
543+
);
541544

542545
const defaultRoom = findMatrixQaProvisionedRoom(
543546
{
@@ -569,24 +572,26 @@ export async function provisionMatrixQaRoom(params: {
569572
baseUrl: params.baseUrl,
570573
fetchImpl: params.fetchImpl,
571574
});
572-
const driver = await anonClient.registerWithToken({
573-
deviceName: "OpenClaw Matrix QA Driver",
574-
localpart: params.driverLocalpart,
575-
password: `driver-${randomUUID()}`,
576-
registrationToken: params.registrationToken,
577-
});
578-
const sut = await anonClient.registerWithToken({
579-
deviceName: "OpenClaw Matrix QA SUT",
580-
localpart: params.sutLocalpart,
581-
password: `sut-${randomUUID()}`,
582-
registrationToken: params.registrationToken,
583-
});
584-
const observer = await anonClient.registerWithToken({
585-
deviceName: "OpenClaw Matrix QA Observer",
586-
localpart: params.observerLocalpart,
587-
password: `observer-${randomUUID()}`,
588-
registrationToken: params.registrationToken,
589-
});
575+
const [driver, sut, observer] = await Promise.all([
576+
anonClient.registerWithToken({
577+
deviceName: "OpenClaw Matrix QA Driver",
578+
localpart: params.driverLocalpart,
579+
password: `driver-${randomUUID()}`,
580+
registrationToken: params.registrationToken,
581+
}),
582+
anonClient.registerWithToken({
583+
deviceName: "OpenClaw Matrix QA SUT",
584+
localpart: params.sutLocalpart,
585+
password: `sut-${randomUUID()}`,
586+
registrationToken: params.registrationToken,
587+
}),
588+
anonClient.registerWithToken({
589+
deviceName: "OpenClaw Matrix QA Observer",
590+
localpart: params.observerLocalpart,
591+
password: `observer-${randomUUID()}`,
592+
registrationToken: params.registrationToken,
593+
}),
594+
]);
590595
const topology = await provisionMatrixQaTopology({
591596
accounts: {
592597
driver,

0 commit comments

Comments
 (0)