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:
Patrick Erichsen
2026-05-05 23:20:08 -07:00
committed by GitHub
parent eb4d654796
commit 5107384e67
11 changed files with 639 additions and 94 deletions

View File

@@ -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()

View File

@@ -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