

















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.
71import { describe, expect, it, vi, beforeAll, beforeEach } from "vitest";
82import type { OpenClawConfig } from "../../config/config.js";
93import { drainPendingDeliveries, type DeliverFn, loadPendingDeliveries } from "./delivery-queue.js";
@@ -14,7 +8,6 @@ import {
148159let deliverOutboundPayloads: typeof import("./deliver.js").deliverOutboundPayloads;
161017-// Minimal reconnect drain helper (no adapter → reconcileUnknownQueuedDelivery returns null).
1811async function drainMatrixReconnect(opts: { deliver: DeliverFn; stateDir: string }): Promise<void> {
1912await drainPendingDeliveries({
2013drainKey: "matrix:reconnect-test",
@@ -27,6 +20,27 @@ async function drainMatrixReconnect(opts: { deliver: DeliverFn; stateDir: string
2720});
2821}
292223+function createPartialSendFailure() {
24+return vi
25+.fn()
26+.mockResolvedValueOnce({ messageId: "m1" })
27+.mockRejectedValueOnce(new Error("second payload send failed"));
28+}
29+30+async function deliverPartialMatrixBatch(sendMatrix: ReturnType<typeof vi.fn>, tmpDir: string) {
31+process.env.OPENCLAW_STATE_DIR = tmpDir;
32+await expect(
33+deliverOutboundPayloads({
34+cfg: {} as OpenClawConfig,
35+channel: "matrix",
36+to: "!room:example",
37+payloads: [{ text: "first" }, { text: "second" }],
38+deps: { matrix: sendMatrix },
39+queuePolicy: "required",
40+}),
41+).rejects.toThrow("second payload send failed");
42+}
43+3044describe("deliverOutboundPayloads queue integration: mid-batch failure with send evidence", () => {
3145const fixtures = installDeliveryQueueTmpDirHooks();
3246let tmpDir: string;
@@ -40,79 +54,36 @@ describe("deliverOutboundPayloads queue integration: mid-batch failure with send
4054});
41554256it("advances queued entry to unknown_after_send when a later payload fails after an earlier one succeeded", async () => {
43-process.env.OPENCLAW_STATE_DIR = tmpDir;
44-// First payload succeeds (send evidence), second payload throws.
45-const sendMatrix = vi
46-.fn()
47-.mockResolvedValueOnce({ messageId: "m1" })
48-.mockRejectedValueOnce(new Error("second payload send failed"));
57+const sendMatrix = createPartialSendFailure();
495850-await expect(
51-deliverOutboundPayloads({
52-cfg: {} as OpenClawConfig,
53-channel: "matrix",
54-to: "!room:example",
55-payloads: [{ text: "first" }, { text: "second" }],
56-deps: { matrix: sendMatrix },
57-queuePolicy: "required",
58-}),
59-).rejects.toThrow("second payload send failed");
59+await deliverPartialMatrixBatch(sendMatrix, tmpDir);
606061-// The entry must exist in the real SQLite queue and be in unknown_after_send.
62-const entries = await import("./delivery-queue.js").then((m) =>
63-m.loadPendingDeliveries(tmpDir),
64-);
61+const entries = await loadPendingDeliveries(tmpDir);
6562expect(entries).toHaveLength(1);
6663const entry = entries[0];
6764expect(entry.recoveryState).toBe("unknown_after_send");
6865expect(entry.retryCount).toBe(0);
6966expect(entry.lastError).toBeUndefined();
70-// Sanity: the send actually happened for the first payload.
7167expect(sendMatrix).toHaveBeenCalledTimes(2);
7268});
73697470it("drain does not replay an unknown_after_send entry when no adapter reconciliation is available", async () => {
75-// Regression guard for the recovery/drain semantics: an entry in
76-// unknown_after_send (written by the patch above) must NOT be blindly
77-// replayed when the channel adapter cannot reconcile the unknown send.
78-// Without the patch the entry would stay in send_attempt_started, which
79-// has the same drain behaviour — but this test pins the contract so that
80-// any future regression that accidentally advances the state in a way that
81-// re-enables blind replay is caught.
82-process.env.OPENCLAW_STATE_DIR = tmpDir;
83-const sendMatrix = vi
84-.fn()
85-.mockResolvedValueOnce({ messageId: "m1" })
86-.mockRejectedValueOnce(new Error("second payload send failed"));
71+const sendMatrix = createPartialSendFailure();
877288-// Drive the patch: entry lands in unknown_after_send.
89-await expect(
90-deliverOutboundPayloads({
91-cfg: {} as OpenClawConfig,
92-channel: "matrix",
93-to: "!room:example",
94-payloads: [{ text: "first" }, { text: "second" }],
95-deps: { matrix: sendMatrix },
96-queuePolicy: "required",
97-}),
98-).rejects.toThrow("second payload send failed");
73+await deliverPartialMatrixBatch(sendMatrix, tmpDir);
997410075const beforeDrain = await loadPendingDeliveries(tmpDir);
10176expect(beforeDrain[0]?.recoveryState).toBe("unknown_after_send");
10277103-// Reconnect drain with no adapter (cfg={}) — reconcileUnknownQueuedDelivery
104-// returns null → "refusing blind replay" branch → deliver is never called.
10578const deliver = vi.fn<DeliverFn>(async () => {});
10679await drainMatrixReconnect({ deliver, stateDir: tmpDir });
1078010881expect(deliver).not.toHaveBeenCalled();
109-// The entry is moved to failed (not re-queued as pending), closing the drain loop.
11082expect(await loadPendingDeliveries(tmpDir)).toHaveLength(0);
11183});
11284113-it("leaves entry for retry (failDelivery, recovery_state stays null) when no send evidence", async () => {
85+it("leaves entry for retry in send_attempt_started when no send evidence exists", async () => {
11486process.env.OPENCLAW_STATE_DIR = tmpDir;
115-// First (and only) payload fails immediately — no send evidence.
11687const sendMatrix = vi.fn().mockRejectedValueOnce(new Error("first payload send failed"));
1178811889await expect(
@@ -131,7 +102,6 @@ describe("deliverOutboundPayloads queue integration: mid-batch failure with send
131102);
132103expect(entries).toHaveLength(1);
133104const entry = entries[0];
134-// No send evidence -> failDelivery path: retryCount bumped, recovery_state not advanced.
135105expect(entry.retryCount).toBe(1);
136106expect(entry.recoveryState).toBe("send_attempt_started");
137107expect(entry.lastError).toContain("first payload send failed");
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。