diff --git a/extensions/qa-matrix/src/runners/contract/scenario-catalog.ts b/extensions/qa-matrix/src/runners/contract/scenario-catalog.ts index 8e864032a29..ba33718867c 100644 --- a/extensions/qa-matrix/src/runners/contract/scenario-catalog.ts +++ b/extensions/qa-matrix/src/runners/contract/scenario-catalog.ts @@ -44,6 +44,7 @@ export type MatrixQaScenarioId = | "matrix-room-membership-loss" | "matrix-homeserver-restart-resume" | "matrix-mention-gating" + | "matrix-mxid-prefixed-command-block" | "matrix-mention-metadata-spoof-block" | "matrix-observer-allowlist-override" | "matrix-allowlist-block" @@ -449,6 +450,14 @@ export const MATRIX_QA_SCENARIOS: MatrixQaScenarioDefinition[] = [ timeoutMs: 8_000, title: "Matrix room message without mention does not trigger", }, + { + id: "matrix-mxid-prefixed-command-block", + timeoutMs: 8_000, + title: "Matrix MXID-prefixed control commands stay gated", + configOverrides: { + groupPolicy: "open", + }, + }, { id: "matrix-mention-metadata-spoof-block", timeoutMs: 8_000, diff --git a/extensions/qa-matrix/src/runners/contract/scenario-runtime.ts b/extensions/qa-matrix/src/runners/contract/scenario-runtime.ts index 91c869828dc..dc9147b9d13 100644 --- a/extensions/qa-matrix/src/runners/contract/scenario-runtime.ts +++ b/extensions/qa-matrix/src/runners/contract/scenario-runtime.ts @@ -247,6 +247,18 @@ export async function runMatrixQaScenario( token, }); } + case "matrix-mxid-prefixed-command-block": { + const token = buildMatrixQaToken("MATRIX_QA_MXID_COMMAND"); + return await runNoReplyScenario({ + accessToken: context.observerAccessToken, + actorId: "observer", + actorUserId: context.observerUserId, + body: `${context.sutUserId} /new`, + mentionUserIds: [context.sutUserId], + context, + token, + }); + } case "matrix-mention-metadata-spoof-block": { const token = buildMatrixQaToken("MATRIX_QA_METADATA_SPOOF"); return await runNoReplyScenario({ diff --git a/extensions/qa-matrix/src/runners/contract/scenarios.test.ts b/extensions/qa-matrix/src/runners/contract/scenarios.test.ts index 21ff8a7c89c..81686ef4c8d 100644 --- a/extensions/qa-matrix/src/runners/contract/scenarios.test.ts +++ b/extensions/qa-matrix/src/runners/contract/scenarios.test.ts @@ -106,6 +106,7 @@ describe("matrix live qa scenarios", () => { "matrix-room-membership-loss", "matrix-homeserver-restart-resume", "matrix-mention-gating", + "matrix-mxid-prefixed-command-block", "matrix-mention-metadata-spoof-block", "matrix-observer-allowlist-override", "matrix-allowlist-block", @@ -516,6 +517,51 @@ describe("matrix live qa scenarios", () => { }); }); + it("blocks MXID-prefixed Matrix control commands from non-allowlisted observers", 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.since).toBe("observer-sync-start"); + 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: { + actorUserId: "@observer:matrix-qa.test", + driverEventId: "$observer-command-trigger", + }, + }); + + expect(createMatrixQaClient).toHaveBeenCalledWith({ + accessToken: "observer-token", + baseUrl: "http://127.0.0.1:28008/", + }); + expect(sendTextMessage).toHaveBeenCalledWith({ + body: "@sut:matrix-qa.test /new", + mentionUserIds: ["@sut:matrix-qa.test"], + roomId: "!main:matrix-qa.test", + }); + expect(waitForOptionalRoomEvent).toHaveBeenCalledWith( + expect.objectContaining({ + roomId: "!main:matrix-qa.test", + }), + ); + }); + it("queues a Matrix trigger during restart before proving incremental sync continues", async () => { const callOrder: string[] = []; const primeRoom = vi.fn().mockResolvedValue("driver-sync-start");