mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 17:40:44 +00:00
fix: stabilize Matrix tool progress QA (#78179)
* fix: stabilize matrix tool progress QA * fix: handle backtick matrix progress previews * fix: reuse observed matrix approvals * fix: retry matrix generated image QA * fix: wait for matrix sas trust propagation * fix: resolve matrix target both approvals by reaction * fix: avoid matrix target both approval echo wait * fix: reuse observed matrix target both dm approval * fix: retry matrix approval delivery * fix: accept active matrix approval dm * test: align matrix approval retry receipt * test: include matrix approval view in retry fixture
This commit is contained in:
@@ -335,6 +335,94 @@ describe("matrixApprovalNativeRuntime", () => {
|
||||
expect(reactMessage).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("retries transient Matrix approval send failures", async () => {
|
||||
const sendSingleTextMessage = vi
|
||||
.fn()
|
||||
.mockRejectedValueOnce(new Error("transient Matrix send failure"))
|
||||
.mockResolvedValue({
|
||||
messageId: "$approval",
|
||||
primaryMessageId: "$approval",
|
||||
receipt: buildMatrixReceipt(["$approval"]),
|
||||
roomId: "!room:example.org",
|
||||
});
|
||||
const reactMessage = vi.fn().mockResolvedValue(undefined);
|
||||
const view = buildExecApprovalView();
|
||||
const pendingPayload = await buildPendingPayload(view);
|
||||
|
||||
const entry = await matrixApprovalNativeRuntime.transport.deliverPending({
|
||||
cfg: {} as never,
|
||||
accountId: "default",
|
||||
context: {
|
||||
client: {} as never,
|
||||
deps: {
|
||||
sendSingleTextMessage,
|
||||
reactMessage,
|
||||
},
|
||||
},
|
||||
request: {} as never,
|
||||
approvalKind: "exec",
|
||||
plannedTarget: buildMatrixApprovalRoomTarget("!room:example.org"),
|
||||
preparedTarget: {
|
||||
to: "room:!room:example.org",
|
||||
roomId: "!room:example.org",
|
||||
},
|
||||
view,
|
||||
pendingPayload,
|
||||
});
|
||||
|
||||
expect(sendSingleTextMessage).toHaveBeenCalledTimes(2);
|
||||
expect(entry).toMatchObject({
|
||||
roomId: "!room:example.org",
|
||||
platformMessageIds: ["$approval"],
|
||||
});
|
||||
});
|
||||
|
||||
it("retries transient Matrix direct-room repair failures before preparing approval DMs", async () => {
|
||||
const repairDirectRooms = vi
|
||||
.fn()
|
||||
.mockRejectedValueOnce(new Error("direct account data not ready"))
|
||||
.mockResolvedValue({
|
||||
activeRoomId: "!dm:example.org",
|
||||
});
|
||||
|
||||
const prepared = await matrixApprovalNativeRuntime.transport.prepareTarget({
|
||||
cfg: {
|
||||
channels: {
|
||||
matrix: {
|
||||
encryption: false,
|
||||
},
|
||||
},
|
||||
} as never,
|
||||
accountId: "default",
|
||||
context: {
|
||||
client: {} as never,
|
||||
deps: {
|
||||
repairDirectRooms,
|
||||
},
|
||||
},
|
||||
request: {} as never,
|
||||
approvalKind: "exec",
|
||||
view: buildExecApprovalView(),
|
||||
pendingPayload: {} as never,
|
||||
plannedTarget: {
|
||||
surface: "approver-dm",
|
||||
target: {
|
||||
to: "user:@owner:example.org",
|
||||
},
|
||||
reason: "preferred",
|
||||
},
|
||||
});
|
||||
|
||||
expect(repairDirectRooms).toHaveBeenCalledTimes(2);
|
||||
expect(prepared).toMatchObject({
|
||||
target: {
|
||||
to: "room:!dm:example.org",
|
||||
roomId: "!dm:example.org",
|
||||
threadId: undefined,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("falls back to chunked Matrix delivery when approval content exceeds one event", async () => {
|
||||
const sendSingleTextMessage = vi
|
||||
.fn()
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { setTimeout as sleep } from "node:timers/promises";
|
||||
import type {
|
||||
ChannelApprovalCapabilityHandlerContext,
|
||||
PendingApprovalView,
|
||||
@@ -123,6 +124,9 @@ type MatrixPrepareTargetParams = {
|
||||
rawTarget: MatrixRawApprovalTarget;
|
||||
};
|
||||
|
||||
const MATRIX_APPROVAL_DELIVERY_ATTEMPTS = 3;
|
||||
const MATRIX_APPROVAL_DELIVERY_RETRY_DELAY_MS = 250;
|
||||
|
||||
export type MatrixApprovalHandlerDeps = {
|
||||
nowMs?: () => number;
|
||||
sendMessage?: typeof sendMessageMatrix;
|
||||
@@ -176,6 +180,25 @@ function isSingleMatrixMessageLimitError(error: unknown): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
async function retryMatrixApprovalDelivery<T>(
|
||||
operation: () => Promise<T>,
|
||||
params: { shouldRetry?: (error: unknown) => boolean } = {},
|
||||
): Promise<T> {
|
||||
let lastError: unknown;
|
||||
for (let attempt = 1; attempt <= MATRIX_APPROVAL_DELIVERY_ATTEMPTS; attempt += 1) {
|
||||
try {
|
||||
return await operation();
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
if (attempt === MATRIX_APPROVAL_DELIVERY_ATTEMPTS || params.shouldRetry?.(error) === false) {
|
||||
break;
|
||||
}
|
||||
await sleep(MATRIX_APPROVAL_DELIVERY_RETRY_DELAY_MS * attempt);
|
||||
}
|
||||
}
|
||||
throw lastError;
|
||||
}
|
||||
|
||||
async function prepareTarget(
|
||||
params: MatrixPrepareTargetParams,
|
||||
): Promise<PreparedMatrixTarget | null> {
|
||||
@@ -194,11 +217,14 @@ async function prepareTarget(
|
||||
accountId: resolved.accountId,
|
||||
});
|
||||
const repairDirectRooms = resolved.context.deps?.repairDirectRooms ?? repairMatrixDirectRooms;
|
||||
const repaired = await repairDirectRooms({
|
||||
client: resolved.context.client,
|
||||
remoteUserId: target.id,
|
||||
encrypted: account.config.encryption === true,
|
||||
});
|
||||
const repaired = await retryMatrixApprovalDelivery(
|
||||
async () =>
|
||||
await repairDirectRooms({
|
||||
client: resolved.context.client,
|
||||
remoteUserId: target.id,
|
||||
encrypted: account.config.encryption === true,
|
||||
}),
|
||||
);
|
||||
if (!repaired.activeRoomId) {
|
||||
return null;
|
||||
}
|
||||
@@ -424,25 +450,32 @@ export const matrixApprovalNativeRuntime = createChannelApprovalNativeRuntimeAda
|
||||
const reactMessage = resolved.context.deps?.reactMessage ?? reactMatrixMessage;
|
||||
let result;
|
||||
try {
|
||||
result = await sendSingleTextMessage(preparedTarget.to, pendingPayload.text, {
|
||||
cfg: cfg as CoreConfig,
|
||||
accountId: resolved.accountId,
|
||||
client: resolved.context.client,
|
||||
threadId: preparedTarget.threadId,
|
||||
extraContent: pendingPayload.extraContent,
|
||||
});
|
||||
result = await retryMatrixApprovalDelivery(
|
||||
async () =>
|
||||
await sendSingleTextMessage(preparedTarget.to, pendingPayload.text, {
|
||||
cfg: cfg as CoreConfig,
|
||||
accountId: resolved.accountId,
|
||||
client: resolved.context.client,
|
||||
threadId: preparedTarget.threadId,
|
||||
extraContent: pendingPayload.extraContent,
|
||||
}),
|
||||
{ shouldRetry: (error) => !isSingleMatrixMessageLimitError(error) },
|
||||
);
|
||||
} catch (error) {
|
||||
if (!isSingleMatrixMessageLimitError(error)) {
|
||||
throw error;
|
||||
}
|
||||
const sendMessage = resolved.context.deps?.sendMessage ?? sendMessageMatrix;
|
||||
result = await sendMessage(preparedTarget.to, pendingPayload.text, {
|
||||
cfg: cfg as CoreConfig,
|
||||
accountId: resolved.accountId,
|
||||
client: resolved.context.client,
|
||||
threadId: preparedTarget.threadId,
|
||||
extraContent: pendingPayload.extraContent,
|
||||
});
|
||||
result = await retryMatrixApprovalDelivery(
|
||||
async () =>
|
||||
await sendMessage(preparedTarget.to, pendingPayload.text, {
|
||||
cfg: cfg as CoreConfig,
|
||||
accountId: resolved.accountId,
|
||||
client: resolved.context.client,
|
||||
threadId: preparedTarget.threadId,
|
||||
extraContent: pendingPayload.extraContent,
|
||||
}),
|
||||
);
|
||||
}
|
||||
const receiptMessageIds = listMessageReceiptPlatformIds(result.receipt);
|
||||
const platformMessageIds = receiptMessageIds.length
|
||||
|
||||
Reference in New Issue
Block a user