mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 07:40:44 +00:00
test(qa): wait for Matrix approval reaction echo
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user