























@@ -266,6 +266,9 @@ describe("deliverOutboundPayloads", () => {
266266queueMocks.ackDelivery.mockResolvedValue(undefined);
267267queueMocks.failDelivery.mockClear();
268268queueMocks.failDelivery.mockResolvedValue(undefined);
269+queueMocks.tryClaimActiveDelivery.mockClear();
270+queueMocks.tryClaimActiveDelivery.mockReturnValue(true);
271+queueMocks.releaseActiveDelivery.mockClear();
269272logMocks.warn.mockClear();
270273});
271274@@ -958,6 +961,30 @@ describe("deliverOutboundPayloads", () => {
958961expect(sendMatrix).not.toHaveBeenCalled();
959962});
960963964+it("bails out without sending when a concurrent drain already claimed the queue entry", async () => {
965+// Regression for openclaw/openclaw#70386: if a reconnect or startup drain
966+// observes the newly enqueued entry and claims it before the live send
967+// path claims it, the live path must not send. The drain already owns
968+// ack/fail for that id; sending here would duplicate the outbound and
969+// race queue cleanup.
970+queueMocks.tryClaimActiveDelivery.mockReturnValueOnce(false);
971+const sendMatrix = vi.fn().mockResolvedValue({ messageId: "m1", roomId: "!room:example" });
972+973+const results = await deliverOutboundPayloads({
974+cfg: {},
975+channel: "matrix",
976+to: "!room:example",
977+payloads: [{ text: "hi" }],
978+deps: { matrix: sendMatrix },
979+});
980+981+expect(results).toEqual([]);
982+expect(sendMatrix).not.toHaveBeenCalled();
983+expect(queueMocks.ackDelivery).not.toHaveBeenCalled();
984+expect(queueMocks.failDelivery).not.toHaveBeenCalled();
985+expect(queueMocks.releaseActiveDelivery).not.toHaveBeenCalled();
986+});
987+961988it("acks the queue entry when delivery is aborted", async () => {
962989const sendMatrix = vi.fn().mockResolvedValue({ messageId: "m1", roomId: "!room:example" });
963990const abortController = new AbortController();
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。