From a46d41156d23ba85208b773fd9a5267b01caf861 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 23 Apr 2026 06:26:51 +0100 Subject: [PATCH] fix(matrix): ignore stale no-reply events --- .../contract/scenario-runtime-shared.ts | 21 ++++-- .../src/runners/contract/scenarios.test.ts | 67 +++++++++++++++++++ 2 files changed, 83 insertions(+), 5 deletions(-) diff --git a/extensions/qa-matrix/src/runners/contract/scenario-runtime-shared.ts b/extensions/qa-matrix/src/runners/contract/scenario-runtime-shared.ts index ec89f2b06d4..d4c38496ca4 100644 --- a/extensions/qa-matrix/src/runners/contract/scenario-runtime-shared.ts +++ b/extensions/qa-matrix/src/runners/contract/scenario-runtime-shared.ts @@ -583,13 +583,24 @@ export async function runNoReplyExpectedScenario(params: { ...(params.mentionUserIds ? { mentionUserIds: params.mentionUserIds } : {}), roomId: params.roomId, }); + let observedTriggerEvent = false; const result = await client.waitForOptionalRoomEvent({ observedEvents: params.observedEvents, - predicate: (event) => - event.roomId === params.roomId && - event.sender === params.sutUserId && - event.type === "m.room.message" && - (params.replyPredicate?.(event, { driverEventId, token: params.token }) ?? true), + predicate: (event) => { + if (event.roomId !== params.roomId) { + return false; + } + if (event.eventId === driverEventId) { + observedTriggerEvent = true; + return false; + } + return ( + observedTriggerEvent && + event.sender === params.sutUserId && + event.type === "m.room.message" && + (params.replyPredicate?.(event, { driverEventId, token: params.token }) ?? true) + ); + }, roomId: params.roomId, since: startSince, timeoutMs: params.timeoutMs, diff --git a/extensions/qa-matrix/src/runners/contract/scenarios.test.ts b/extensions/qa-matrix/src/runners/contract/scenarios.test.ts index db4afe9bb3b..90930e40970 100644 --- a/extensions/qa-matrix/src/runners/contract/scenarios.test.ts +++ b/extensions/qa-matrix/src/runners/contract/scenarios.test.ts @@ -590,6 +590,73 @@ describe("matrix live qa scenarios", () => { ); }); + it("ignores stale Matrix SUT replies before a no-reply trigger", async () => { + const primeRoom = vi.fn().mockResolvedValue("observer-sync-start"); + const sendTextMessage = vi.fn().mockResolvedValue("$observer-command-trigger"); + const waitForOptionalRoomEvent = vi.fn().mockImplementation(async (params) => { + expect( + params.predicate({ + eventId: "$previous-reply", + kind: "message", + relatesTo: { + eventId: "$previous-trigger", + inReplyToId: "$previous-trigger", + isFallingBack: true, + relType: "m.thread", + }, + roomId: "!main:matrix-qa.test", + sender: "@sut:matrix-qa.test", + type: "m.room.message", + }), + ).toBe(false); + expect( + params.predicate({ + eventId: "$observer-command-trigger", + kind: "message", + roomId: "!main:matrix-qa.test", + sender: "@observer:matrix-qa.test", + type: "m.room.message", + }), + ).toBe(false); + expect( + params.predicate({ + eventId: "$current-reply", + kind: "message", + relatesTo: { + eventId: "$observer-command-trigger", + inReplyToId: "$observer-command-trigger", + isFallingBack: true, + relType: "m.thread", + }, + roomId: "!main:matrix-qa.test", + sender: "@sut:matrix-qa.test", + type: "m.room.message", + }), + ).toBe(true); + return { + matched: false, + since: "observer-sync-next", + }; + }); + + createMatrixQaClient.mockReturnValue({ + primeRoom, + sendTextMessage, + waitForOptionalRoomEvent, + }); + + const scenario = MATRIX_QA_SCENARIOS.find( + (entry) => entry.id === "matrix-mxid-prefixed-command-block", + ); + expect(scenario).toBeDefined(); + + await expect(runMatrixQaScenario(scenario!, matrixQaScenarioContext())).resolves.toMatchObject({ + artifacts: { + driverEventId: "$observer-command-trigger", + }, + }); + }); + it("hot-reloads group allowlist removals inside one running Matrix gateway", async () => { const patchGatewayConfig = vi.fn(async () => {}); const primeRoom = vi.fn().mockResolvedValue("sync-start");