|
5 | 5 | type ChannelBotLoopProtectionFacts, |
6 | 6 | } from "openclaw/plugin-sdk/inbound-reply-dispatch"; |
7 | 7 | import type { ReplyPayload } from "openclaw/plugin-sdk/reply-dispatch-runtime"; |
| 8 | +import * as runtimeEnvModule from "openclaw/plugin-sdk/runtime-env"; |
8 | 9 | import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; |
9 | 10 | import type { DiscordMessagePreflightContext } from "./message-handler.preflight.js"; |
10 | 11 |
|
@@ -211,6 +212,7 @@ let createDiscordDirectMessageContextOverrides: typeof import("./message-handler |
211 | 212 | let threadBindingTesting: typeof import("./thread-bindings.js").testing; |
212 | 213 | let createThreadBindingManager: typeof import("./thread-bindings.js").createThreadBindingManager; |
213 | 214 | let processDiscordMessage: typeof import("./message-handler.process.js").processDiscordMessage; |
| 215 | +let formatDiscordReplySkip: typeof import("./message-handler.process.js").formatDiscordReplySkip; |
214 | 216 | let notifyDiscordInboundEventOutboundSuccess: typeof import("../inbound-event-delivery.js").notifyDiscordInboundEventOutboundSuccess; |
215 | 217 |
|
216 | 218 | vi.mock("openclaw/plugin-sdk/reply-runtime", () => ({ |
@@ -373,7 +375,8 @@ beforeAll(async () => { |
373 | 375 | await import("./message-handler.test-harness.js")); |
374 | 376 | ({ testing: threadBindingTesting, createThreadBindingManager } = |
375 | 377 | await import("./thread-bindings.js")); |
376 | | - ({ processDiscordMessage } = await import("./message-handler.process.js")); |
| 378 | + ({ processDiscordMessage, formatDiscordReplySkip } = |
| 379 | + await import("./message-handler.process.js")); |
377 | 380 | ({ notifyDiscordInboundEventOutboundSuccess } = await import("../inbound-event-delivery.js")); |
378 | 381 | }); |
379 | 382 |
|
@@ -2745,3 +2748,55 @@ describe("processDiscordMessage draft streaming", () => { |
2745 | 2748 | expect(draftStream.update).not.toHaveBeenCalled(); |
2746 | 2749 | }); |
2747 | 2750 | }); |
| 2751 | + |
| 2752 | +describe("processDiscordMessage deliver-lambda abort logging", () => { |
| 2753 | + it("emits logVerbose with formatDiscordReplySkip when deliver fires on a pre-aborted signal", async () => { |
| 2754 | + // Capture logVerbose calls via the ESM namespace binding. We rely on the |
| 2755 | + // same vi.spyOn pattern used in native-command.model-picker.test.ts so the |
| 2756 | + // production module keeps its real logVerbose import while the test still |
| 2757 | + // sees every invocation that the deliver lambda surfaces. |
| 2758 | + const verboseSpy = vi.spyOn(runtimeEnvModule, "logVerbose").mockImplementation(() => {}); |
| 2759 | + |
| 2760 | + const abortController = new AbortController(); |
| 2761 | + // Drive the dispatcher so deliver actually runs: abort the signal inside |
| 2762 | + // the dispatch mock and then queue a single block reply via the captured |
| 2763 | + // dispatcher. The mocked createReplyDispatcherWithTyping (see line ~229) |
| 2764 | + // routes sendBlockReply straight into the deliver lambda, where the very |
| 2765 | + // first gate is `if (isProcessAborted(abortSignal)) return;` — the line |
| 2766 | + // the PR added the logVerbose call to. |
| 2767 | + dispatchInboundMessage.mockImplementationOnce(async (params?: DispatchInboundParams) => { |
| 2768 | + abortController.abort(); |
| 2769 | + await params?.dispatcher.sendBlockReply({ text: "post-abort block payload" }); |
| 2770 | + return { queuedFinal: false, counts: { final: 0, tool: 0, block: 1 } }; |
| 2771 | + }); |
| 2772 | + |
| 2773 | + const ctx = await createAutomaticSourceDeliveryContext({ |
| 2774 | + abortSignal: abortController.signal, |
| 2775 | + cfg: { |
| 2776 | + messages: { |
| 2777 | + ackReaction: "👀", |
| 2778 | + }, |
| 2779 | + session: { store: "/tmp/openclaw-discord-process-test-sessions.json" }, |
| 2780 | + }, |
| 2781 | + }); |
| 2782 | + |
| 2783 | + await runProcessDiscordMessage(ctx); |
| 2784 | + |
| 2785 | + // The base test harness routes through guild g1 / channel c1 (see |
| 2786 | + // createBaseDiscordMessageContext) so the deliver lambda receives the |
| 2787 | + // matching deliver target and session key from ctxPayload.SessionKey. |
| 2788 | + const dispatchedSessionKey = getLastDispatchCtx()?.SessionKey; |
| 2789 | + expect(dispatchedSessionKey).toBeTypeOf("string"); |
| 2790 | + const expectedLog = formatDiscordReplySkip({ |
| 2791 | + kind: "block", |
| 2792 | + reason: "aborted before delivery", |
| 2793 | + target: "channel:c1", |
| 2794 | + sessionKey: dispatchedSessionKey, |
| 2795 | + }); |
| 2796 | + const verboseCalls = verboseSpy.mock.calls.map((call) => call[0]); |
| 2797 | + expect(verboseCalls).toContain(expectedLog); |
| 2798 | + // Restore so other tests sharing this worker (isolate=false) keep the |
| 2799 | + // real logVerbose binding. |
| 2800 | + verboseSpy.mockRestore(); |
| 2801 | + }); |
| 2802 | +}); |
0 commit comments