@@ -1045,6 +1045,64 @@ describe("deliverOutboundPayloads", () => {
|
1045 | 1045 | expect(queueMocks.ackDelivery).not.toHaveBeenCalled(); |
1046 | 1046 | }); |
1047 | 1047 | |
| 1048 | +it("marks queued delivery as unknown-after-send (not failed) when a later payload fails after an earlier one succeeded", async () => { |
| 1049 | +// Regression: required-mode batch send where an earlier payload succeeded |
| 1050 | +// (results.length > 0, OutboundDeliveryError.sentBeforeError === true) but a |
| 1051 | +// later payload throws. Previously the wrapper catch called failDelivery, |
| 1052 | +// leaving the entry in `send_attempt_started` so reconnect drain later |
| 1053 | +// replayed it as "not yet sent" — producing duplicate messages when the |
| 1054 | +// adapter's unknown-send reconciliation misreported `not_sent`. The fix |
| 1055 | +// advances to `unknown_after_send` so drain routes the entry through |
| 1056 | +// reconcileUnknownQueuedDelivery instead of blind replay. |
| 1057 | +const sendMatrix = vi |
| 1058 | +.fn() |
| 1059 | +.mockResolvedValueOnce({ messageId: "m1" }) |
| 1060 | +.mockRejectedValueOnce(new Error("second payload send failed")); |
| 1061 | + |
| 1062 | +await expect( |
| 1063 | +deliverOutboundPayloads({ |
| 1064 | +cfg: {}, |
| 1065 | +channel: "matrix", |
| 1066 | +to: "!room:example", |
| 1067 | +payloads: [{ text: "first" }, { text: "second" }], |
| 1068 | +deps: { matrix: sendMatrix }, |
| 1069 | +queuePolicy: "required", |
| 1070 | +}), |
| 1071 | +).rejects.toThrow("second payload send failed"); |
| 1072 | + |
| 1073 | +expect(sendMatrix).toHaveBeenCalledTimes(2); |
| 1074 | +expect(queueMocks.markDeliveryPlatformOutcomeUnknown).toHaveBeenCalledWith("mock-queue-id"); |
| 1075 | +// Must NOT failDelivery — that would leave send_attempt_started for drain to replay. |
| 1076 | +expect(queueMocks.failDelivery).not.toHaveBeenCalled(); |
| 1077 | +// Must NOT ack — the batch did not fully succeed; reconcile must confirm the |
| 1078 | +// partial send rather than silently dropping the queue entry. |
| 1079 | +expect(queueMocks.ackDelivery).not.toHaveBeenCalled(); |
| 1080 | +}); |
| 1081 | + |
| 1082 | +it("still calls failDelivery when a payload fails before any send succeeded", async () => { |
| 1083 | +// No send evidence (sentBeforeError === false): failDelivery is correct — |
| 1084 | +// nothing reached the channel, so leaving the entry for retry is safe. |
| 1085 | +const sendMatrix = vi.fn().mockRejectedValueOnce(new Error("first payload send failed")); |
| 1086 | + |
| 1087 | +await expect( |
| 1088 | +deliverOutboundPayloads({ |
| 1089 | +cfg: {}, |
| 1090 | +channel: "matrix", |
| 1091 | +to: "!room:example", |
| 1092 | +payloads: [{ text: "first" }], |
| 1093 | +deps: { matrix: sendMatrix }, |
| 1094 | +queuePolicy: "required", |
| 1095 | +}), |
| 1096 | +).rejects.toThrow("first payload send failed"); |
| 1097 | + |
| 1098 | +expect(queueMocks.failDelivery).toHaveBeenCalledWith( |
| 1099 | +"mock-queue-id", |
| 1100 | +expect.stringContaining("first payload send failed"), |
| 1101 | +); |
| 1102 | +expect(queueMocks.markDeliveryPlatformOutcomeUnknown).not.toHaveBeenCalled(); |
| 1103 | +expect(queueMocks.ackDelivery).not.toHaveBeenCalled(); |
| 1104 | +}); |
| 1105 | + |
1048 | 1106 | it("fails required delivery when the post-send unknown marker cannot be written", async () => { |
1049 | 1107 | queueMocks.markDeliveryPlatformOutcomeUnknown.mockRejectedValueOnce( |
1050 | 1108 | new Error("unknown marker offline"), |
|