






















1+// Integration test: real SQLite delivery queue + mock adapter.
2+// Verifies the patch end-to-end at the queue layer: when a required-mode
3+// batch send fails mid-batch after an earlier payload already succeeded,
4+// the queue entry advances to recovery_state=unknown_after_send (not left
5+// in send_attempt_started), so reconnect-drain routes it through
6+// reconcileUnknownQueuedDelivery instead of blind replay.
7+import { describe, expect, it, vi, beforeAll, beforeEach } from "vitest";
8+import type { OpenClawConfig } from "../../config/config.js";
9+import { installDeliveryQueueTmpDirHooks, readQueuedEntry } from "./delivery-queue.test-helpers.js";
10+11+let deliverOutboundPayloads: typeof import("./deliver.js").deliverOutboundPayloads;
12+13+describe("deliverOutboundPayloads queue integration: mid-batch failure with send evidence", () => {
14+const fixtures = installDeliveryQueueTmpDirHooks();
15+let tmpDir: string;
16+17+beforeAll(async () => {
18+({ deliverOutboundPayloads } = await import("./deliver.js"));
19+});
20+21+beforeEach(() => {
22+tmpDir = fixtures.tmpDir();
23+});
24+25+it("advances queued entry to unknown_after_send when a later payload fails after an earlier one succeeded", async () => {
26+process.env.OPENCLAW_STATE_DIR = tmpDir;
27+// First payload succeeds (send evidence), second payload throws.
28+const sendMatrix = vi
29+.fn()
30+.mockResolvedValueOnce({ messageId: "m1" })
31+.mockRejectedValueOnce(new Error("second payload send failed"));
32+33+await expect(
34+deliverOutboundPayloads({
35+cfg: {} as OpenClawConfig,
36+channel: "matrix",
37+to: "!room:example",
38+payloads: [{ text: "first" }, { text: "second" }],
39+deps: { matrix: sendMatrix },
40+queuePolicy: "required",
41+}),
42+).rejects.toThrow("second payload send failed");
43+44+// The entry must exist in the real SQLite queue and be in unknown_after_send.
45+const entries = await import("./delivery-queue.js").then((m) =>
46+m.loadPendingDeliveries(tmpDir),
47+);
48+expect(entries).toHaveLength(1);
49+const entry = entries[0]!;
50+expect(entry.recoveryState).toBe("unknown_after_send");
51+expect(entry.retryCount).toBe(0);
52+expect(entry.lastError).toBeUndefined();
53+// Sanity: the send actually happened for the first payload.
54+expect(sendMatrix).toHaveBeenCalledTimes(2);
55+});
56+57+it("leaves entry for retry (failDelivery, recovery_state stays null) when no send evidence", async () => {
58+process.env.OPENCLAW_STATE_DIR = tmpDir;
59+// First (and only) payload fails immediately — no send evidence.
60+const sendMatrix = vi.fn().mockRejectedValueOnce(new Error("first payload send failed"));
61+62+await expect(
63+deliverOutboundPayloads({
64+cfg: {} as OpenClawConfig,
65+channel: "matrix",
66+to: "!room:example",
67+payloads: [{ text: "first" }],
68+deps: { matrix: sendMatrix },
69+queuePolicy: "required",
70+}),
71+).rejects.toThrow("first payload send failed");
72+73+const entries = await import("./delivery-queue.js").then((m) =>
74+m.loadPendingDeliveries(tmpDir),
75+);
76+expect(entries).toHaveLength(1);
77+const entry = entries[0]!;
78+// No send evidence -> failDelivery path: retryCount bumped, recovery_state not advanced.
79+expect(entry.retryCount).toBe(1);
80+expect(entry.recoveryState).toBe("send_attempt_started");
81+expect(String(entry.lastError ?? "")).toContain("first payload send failed");
82+});
83+});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。