






















@@ -6,10 +6,27 @@
66// reconcileUnknownQueuedDelivery instead of blind replay.
77import { describe, expect, it, vi, beforeAll, beforeEach } from "vitest";
88import type { OpenClawConfig } from "../../config/config.js";
9-import { installDeliveryQueueTmpDirHooks } from "./delivery-queue.test-helpers.js";
9+import { drainPendingDeliveries, type DeliverFn, loadPendingDeliveries } from "./delivery-queue.js";
10+import {
11+createRecoveryLog,
12+installDeliveryQueueTmpDirHooks,
13+} from "./delivery-queue.test-helpers.js";
10141115let deliverOutboundPayloads: typeof import("./deliver.js").deliverOutboundPayloads;
121617+// Minimal reconnect drain helper (no adapter → reconcileUnknownQueuedDelivery returns null).
18+async function drainMatrixReconnect(opts: { deliver: DeliverFn; stateDir: string }): Promise<void> {
19+await drainPendingDeliveries({
20+drainKey: "matrix:reconnect-test",
21+logLabel: "Matrix reconnect drain",
22+cfg: {} as OpenClawConfig,
23+log: createRecoveryLog(),
24+stateDir: opts.stateDir,
25+deliver: opts.deliver,
26+selectEntry: (entry) => ({ match: entry.channel === "matrix" }),
27+});
28+}
29+1330describe("deliverOutboundPayloads queue integration: mid-batch failure with send evidence", () => {
1431const fixtures = installDeliveryQueueTmpDirHooks();
1532let tmpDir: string;
@@ -54,6 +71,45 @@ describe("deliverOutboundPayloads queue integration: mid-batch failure with send
5471expect(sendMatrix).toHaveBeenCalledTimes(2);
5572});
567374+it("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"));
87+88+// 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");
99+100+const beforeDrain = await loadPendingDeliveries(tmpDir);
101+expect(beforeDrain[0]?.recoveryState).toBe("unknown_after_send");
102+103+// Reconnect drain with no adapter (cfg={}) — reconcileUnknownQueuedDelivery
104+// returns null → "refusing blind replay" branch → deliver is never called.
105+const deliver = vi.fn<DeliverFn>(async () => {});
106+await drainMatrixReconnect({ deliver, stateDir: tmpDir });
107+108+expect(deliver).not.toHaveBeenCalled();
109+// The entry is moved to failed (not re-queued as pending), closing the drain loop.
110+expect(await loadPendingDeliveries(tmpDir)).toHaveLength(0);
111+});
112+57113it("leaves entry for retry (failDelivery, recovery_state stays null) when no send evidence", async () => {
58114process.env.OPENCLAW_STATE_DIR = tmpDir;
59115// First (and only) payload fails immediately — no send evidence.
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。