test(qa): wait for Matrix approval reaction echo

This commit is contained in:
Vincent Koc
2026-05-03 17:01:50 -07:00
parent 4dc2aedb76
commit e782f47eca
3 changed files with 115 additions and 0 deletions

View File

@@ -55,6 +55,7 @@ Docs: https://docs.openclaw.ai
- QA/Slack: fail the live mention-gating scenario on any unexpected SUT reply, even when the reply does not echo the expected marker. Thanks @vincentkoc.
- QA/Matrix: steer the live tool-progress preview check away from `HEARTBEAT.md` and report final preview candidates when the live marker reply misses the exact token. Thanks @vincentkoc.
- QA/Matrix: let the live tool-progress preview check verify progress replacement events without depending on the preview saying `Working`. Thanks @vincentkoc.
- QA/Matrix: wait for live approval reactions to echo before starting the threaded approval decision timeout. Thanks @vincentkoc.
- Tlon: expose `groupInviteAllowlist` in the channel config schema and clarify that group invite auto-accept fails closed without an invite allowlist. Thanks @vincentkoc.
- Control UI/WebChat: collapse duplicate in-flight internal text sends onto the active Gateway run so rapid repeat submits do not start fresh `agent:main:main` dispatches. Fixes #75737. Thanks @dsdsddd1 and @BunsDev.
- Mattermost: accept the documented `channels.mattermost.streaming` config and honor `streaming: "off"` by disabling draft preview posts. Thanks @vincentkoc.

View File

@@ -179,6 +179,23 @@ async function reactToApproval(params: {
messageId: params.targetEventId,
roomId: params.roomId,
});
await client
.waitForRoomEvent({
observedEvents: params.context.observedEvents,
predicate: (event) =>
event.roomId === params.roomId &&
event.sender === params.context.driverUserId &&
event.type === "m.reaction" &&
event.reaction?.eventId === params.targetEventId &&
event.reaction.key === emoji,
roomId: params.roomId,
timeoutMs: params.context.timeoutMs,
})
.catch((err: unknown) => {
throw new Error(
`Matrix approval reaction ${eventId} was not observed before waiting for the gateway decision: ${String(err)}`,
);
});
return {
eventId,
reaction: {

View File

@@ -369,6 +369,103 @@ describe("matrix live qa scenarios", () => {
expect(shardIds.toSorted()).toEqual(allIds.toSorted());
});
it("waits for the driver Matrix approval reaction echo before awaiting the decision", async () => {
const context = matrixQaScenarioContext();
let approvalId = "";
const gatewayCall = vi.fn().mockImplementation(async (method: string, ...args: unknown[]) => {
if (method === "exec.approval.request") {
const params = args.find(
(arg): arg is { id?: unknown } => typeof arg === "object" && arg !== null && "id" in arg,
);
const payload =
typeof params === "object" && params !== null ? (params as { id?: unknown }) : undefined;
approvalId = String(payload?.id ?? "approval-missing");
return { id: approvalId, status: "accepted" };
}
if (method === "exec.approval.waitDecision") {
return { decision: "allow-once", id: approvalId };
}
throw new Error(`unexpected gateway method ${method}`);
});
context.gatewayCall = gatewayCall;
const rootEventId = "$approval-thread-root";
const approvalEventId = "$approval-thread-event";
const sendReaction = vi.fn().mockResolvedValue("$driver-approval-reaction");
const waitForRoomEvent = vi
.fn()
.mockImplementationOnce(async () => ({
event: matrixQaMessageEvent({
approval: {
allowedDecisions: ["allow-once", "deny"],
hasCommandText: true,
id: approvalId,
kind: "exec",
state: "pending",
type: "approval.request",
version: 1,
},
body: "approval requested",
eventId: approvalEventId,
kind: "message",
relatesTo: {
eventId: rootEventId,
inReplyToId: rootEventId,
isFallingBack: true,
relType: "m.thread",
},
}),
since: "driver-sync-approval",
}))
.mockImplementationOnce(async () => ({
event: {
eventId: "$bot-approval-option",
kind: "reaction",
reaction: {
eventId: approvalEventId,
key: "✅",
},
roomId: "!main:matrix-qa.test",
sender: "@sut:matrix-qa.test",
type: "m.reaction",
} satisfies MatrixQaObservedEvent,
since: "driver-sync-option",
}))
.mockImplementationOnce(async () => ({
event: {
eventId: "$driver-approval-reaction",
kind: "reaction",
reaction: {
eventId: approvalEventId,
key: "✅",
},
roomId: "!main:matrix-qa.test",
sender: "@driver:matrix-qa.test",
type: "m.reaction",
} satisfies MatrixQaObservedEvent,
since: "driver-sync-driver-reaction",
}));
createMatrixQaClient.mockReturnValue({
primeRoom: vi.fn().mockResolvedValue("driver-sync-start"),
sendReaction,
sendTextMessage: vi.fn().mockResolvedValue(rootEventId),
waitForRoomEvent,
});
const scenario = MATRIX_QA_SCENARIOS.find(
(entry) => entry.id === "matrix-approval-thread-target",
);
expect(scenario).toBeDefined();
await expect(runMatrixQaScenario(scenario!, context)).resolves.toMatchObject({
artifacts: {
reactionEventId: "$driver-approval-reaction",
reactionTargetEventId: approvalEventId,
},
});
expect(waitForRoomEvent).toHaveBeenCalledTimes(3);
expect(gatewayCall.mock.calls.at(-1)?.[0]).toBe("exec.approval.waitDecision");
});
it("lets explicit Matrix scenario ids override the selected profile", () => {
expect(
scenarioTesting