






















@@ -1,4 +1,5 @@
1-import { describe, expect, it, vi } from "vitest";
1+import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types";
2+import { beforeEach, describe, expect, it, vi } from "vitest";
23import { DiscordRetryableInboundError } from "./inbound-dedupe.js";
34import {
45createDiscordMessageHandler,
@@ -10,6 +11,23 @@ import {
1011createDiscordPreflightContext,
1112} from "./message-handler.test-helpers.js";
121314+const earlyTypingMocks = vi.hoisted(() => ({
15+createDiscordRestClient: vi.fn(() => ({
16+token: "test-token",
17+rest: { kind: "discord-rest" },
18+account: { accountId: "default", config: {} },
19+})),
20+sendTyping: vi.fn(async () => {}),
21+}));
22+23+vi.mock("../client.js", () => ({
24+createDiscordRestClient: earlyTypingMocks.createDiscordRestClient,
25+}));
26+27+vi.mock("./typing.js", () => ({
28+sendTyping: earlyTypingMocks.sendTyping,
29+}));
30+1331type SetStatusFn = (patch: Record<string, unknown>) => void;
1432function createDeferred<T = void>() {
1533let resolve: (value: T | PromiseLike<T>) => void = () => {};
@@ -40,17 +58,40 @@ function createMessageData(messageId: string, channelId = "ch-1") {
4058}
41594260function createPreflightContext(channelId = "ch-1") {
61+const discordConfig = {
62+enabled: true,
63+token: "test-token",
64+groupPolicy: "allowlist" as const,
65+};
66+const cfg: OpenClawConfig = {
67+channels: {
68+discord: discordConfig,
69+},
70+messages: {
71+inbound: {
72+debounceMs: 0,
73+},
74+},
75+};
4376return {
4477 ...createDiscordPreflightContext(channelId),
78+ cfg,
4579accountId: "default",
4680token: "test-token",
4781textLimit: 2_000,
4882replyToMode: "off" as const,
49-discordConfig: {
50-enabled: true,
51-token: "test-token",
52-groupPolicy: "allowlist" as const,
53-},
83+ discordConfig,
84+};
85+}
86+87+function createAcceptedDmPreflightContext(overrides: Record<string, unknown> = {}) {
88+return {
89+ ...createPreflightContext("dm-1"),
90+isDirectMessage: true,
91+isGuildMessage: false,
92+isGroupDm: false,
93+messageText: "hello",
94+ ...overrides,
5495};
5596}
5697@@ -104,6 +145,128 @@ async function createLifecycleStopScenario(params: {
104145}
105146106147describe("createDiscordMessageHandler queue behavior", () => {
148+beforeEach(() => {
149+earlyTypingMocks.createDiscordRestClient.mockReset().mockReturnValue({
150+token: "test-token",
151+rest: { kind: "discord-rest" },
152+account: { accountId: "default", config: {} },
153+});
154+earlyTypingMocks.sendTyping.mockReset().mockResolvedValue(undefined);
155+});
156+157+it("sends an accepted DM typing cue before queued processing starts", async () => {
158+preflightDiscordMessageMock.mockReset();
159+processDiscordMessageMock.mockReset();
160+preflightDiscordMessageMock.mockResolvedValue(createAcceptedDmPreflightContext());
161+processDiscordMessageMock.mockResolvedValue(undefined);
162+163+const handler = createDiscordMessageHandler(createDiscordHandlerParams());
164+await expect(
165+handler(createMessageData("m-typing", "dm-1") as never, {} as never),
166+).resolves.toBeUndefined();
167+168+await flushQueueWork();
169+170+expect(earlyTypingMocks.createDiscordRestClient).toHaveBeenCalledWith(
171+expect.objectContaining({
172+accountId: "default",
173+token: "test-token",
174+}),
175+);
176+expect(earlyTypingMocks.sendTyping).toHaveBeenCalledWith({
177+rest: { kind: "discord-rest" },
178+channelId: "dm-1",
179+});
180+expect(earlyTypingMocks.sendTyping.mock.invocationCallOrder[0]).toBeLessThan(
181+processDiscordMessageMock.mock.invocationCallOrder[0],
182+);
183+});
184+185+it("keeps accepted DM dispatch running when the early typing cue fails", async () => {
186+preflightDiscordMessageMock.mockReset();
187+processDiscordMessageMock.mockReset();
188+earlyTypingMocks.sendTyping.mockRejectedValueOnce(new Error("typing failed"));
189+preflightDiscordMessageMock.mockResolvedValue(createAcceptedDmPreflightContext());
190+processDiscordMessageMock.mockResolvedValue(undefined);
191+192+const handler = createDiscordMessageHandler(createDiscordHandlerParams());
193+await expect(
194+handler(createMessageData("m-typing-fails", "dm-1") as never, {} as never),
195+).resolves.toBeUndefined();
196+197+await flushQueueWork();
198+199+expect(earlyTypingMocks.sendTyping).toHaveBeenCalledTimes(1);
200+expect(processDiscordMessageMock).toHaveBeenCalledTimes(1);
201+});
202+203+it("does not send early typing when preflight rejects the message", async () => {
204+preflightDiscordMessageMock.mockReset();
205+processDiscordMessageMock.mockReset();
206+preflightDiscordMessageMock.mockResolvedValue(null);
207+208+const handler = createDiscordMessageHandler(createDiscordHandlerParams());
209+await expect(
210+handler(createMessageData("m-rejected", "dm-1") as never, {} as never),
211+).resolves.toBeUndefined();
212+213+await flushQueueWork();
214+215+expect(earlyTypingMocks.sendTyping).not.toHaveBeenCalled();
216+expect(processDiscordMessageMock).not.toHaveBeenCalled();
217+});
218+219+it("does not send early typing when typing mode is not instant", async () => {
220+preflightDiscordMessageMock.mockReset();
221+processDiscordMessageMock.mockReset();
222+preflightDiscordMessageMock.mockResolvedValue(
223+createAcceptedDmPreflightContext({
224+cfg: {
225+ ...createPreflightContext().cfg,
226+agents: {
227+defaults: {
228+typingMode: "message",
229+},
230+},
231+},
232+}),
233+);
234+processDiscordMessageMock.mockResolvedValue(undefined);
235+236+const handler = createDiscordMessageHandler(createDiscordHandlerParams());
237+await expect(
238+handler(createMessageData("m-message-mode", "dm-1") as never, {} as never),
239+).resolves.toBeUndefined();
240+241+await flushQueueWork();
242+243+expect(earlyTypingMocks.sendTyping).not.toHaveBeenCalled();
244+expect(processDiscordMessageMock).toHaveBeenCalledTimes(1);
245+});
246+247+it("does not send early typing for guild messages", async () => {
248+preflightDiscordMessageMock.mockReset();
249+processDiscordMessageMock.mockReset();
250+preflightDiscordMessageMock.mockResolvedValue(
251+createAcceptedDmPreflightContext({
252+isDirectMessage: false,
253+isGuildMessage: true,
254+messageChannelId: "guild-channel",
255+}),
256+);
257+processDiscordMessageMock.mockResolvedValue(undefined);
258+259+const handler = createDiscordMessageHandler(createDiscordHandlerParams());
260+await expect(
261+handler(createMessageData("m-guild", "guild-channel") as never, {} as never),
262+).resolves.toBeUndefined();
263+264+await flushQueueWork();
265+266+expect(earlyTypingMocks.sendTyping).not.toHaveBeenCalled();
267+expect(processDiscordMessageMock).toHaveBeenCalledTimes(1);
268+});
269+107270it("resets busy counters when the handler is created", () => {
108271preflightDiscordMessageMock.mockReset();
109272processDiscordMessageMock.mockReset();
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。