@openclaw/feishu 2026.5.2 → 2026.5.3-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/accounts-Ba3-WP1z.js +423 -0
- package/dist/api.js +2280 -0
- package/dist/app-registration-B8qc1MCM.js +184 -0
- package/dist/audio-preflight.runtime-BPlzkO3l.js +7 -0
- package/dist/card-interaction-BfRLgvw_.js +96 -0
- package/dist/channel-CSD_Jt8I.js +1668 -0
- package/dist/channel-entry.js +22 -0
- package/dist/channel-plugin-api.js +2 -0
- package/dist/channel.runtime-DYsXcD36.js +700 -0
- package/dist/client-DBVoQL5w.js +157 -0
- package/dist/contract-api.js +9 -0
- package/dist/conversation-id-DWS3Ep2A.js +139 -0
- package/dist/directory.static-f3EeoRJd.js +44 -0
- package/dist/drive-C5eJLJr7.js +883 -0
- package/dist/index.js +68 -0
- package/dist/monitor-CT189QfR.js +60 -0
- package/dist/monitor.account-dJV2jO8C.js +4990 -0
- package/dist/monitor.state-DYM02ipp.js +100 -0
- package/dist/policy-D6c-wMPl.js +118 -0
- package/dist/probe-BNzzU_uR.js +149 -0
- package/dist/rolldown-runtime-DUslC3ob.js +14 -0
- package/dist/runtime-CG0DuRCy.js +8 -0
- package/dist/runtime-api.js +14 -0
- package/dist/secret-contract-Dm4Z_zQN.js +119 -0
- package/dist/secret-contract-api.js +2 -0
- package/dist/security-audit-DqJdocrN.js +11 -0
- package/dist/security-audit-shared-ByuMx9cJ.js +38 -0
- package/dist/security-contract-api.js +2 -0
- package/dist/send-DowxxbpH.js +1218 -0
- package/dist/session-conversation-B4nrW-vo.js +27 -0
- package/dist/session-key-api.js +2 -0
- package/dist/setup-api.js +2 -0
- package/dist/setup-entry.js +15 -0
- package/dist/subagent-hooks-C3UhPVLV.js +227 -0
- package/dist/subagent-hooks-api.js +23 -0
- package/dist/targets-JMFJRKSe.js +48 -0
- package/dist/thread-bindings-BmS6TLes.js +222 -0
- package/package.json +15 -6
- package/api.ts +0 -31
- package/channel-entry.ts +0 -20
- package/channel-plugin-api.ts +0 -1
- package/contract-api.ts +0 -16
- package/index.ts +0 -82
- package/runtime-api.ts +0 -55
- package/secret-contract-api.ts +0 -5
- package/security-contract-api.ts +0 -1
- package/session-key-api.ts +0 -1
- package/setup-api.ts +0 -3
- package/setup-entry.test.ts +0 -14
- package/setup-entry.ts +0 -13
- package/src/accounts.test.ts +0 -459
- package/src/accounts.ts +0 -326
- package/src/app-registration.ts +0 -331
- package/src/approval-auth.test.ts +0 -24
- package/src/approval-auth.ts +0 -25
- package/src/async.test.ts +0 -35
- package/src/async.ts +0 -104
- package/src/audio-preflight.runtime.ts +0 -9
- package/src/bitable.test.ts +0 -131
- package/src/bitable.ts +0 -762
- package/src/bot-content.ts +0 -474
- package/src/bot-group-name.test.ts +0 -108
- package/src/bot-runtime-api.ts +0 -12
- package/src/bot-sender-name.ts +0 -125
- package/src/bot.broadcast.test.ts +0 -463
- package/src/bot.card-action.test.ts +0 -577
- package/src/bot.checkBotMentioned.test.ts +0 -265
- package/src/bot.helpers.test.ts +0 -118
- package/src/bot.stripBotMention.test.ts +0 -126
- package/src/bot.test.ts +0 -3040
- package/src/bot.ts +0 -1559
- package/src/card-action.ts +0 -447
- package/src/card-interaction.test.ts +0 -129
- package/src/card-interaction.ts +0 -159
- package/src/card-test-helpers.ts +0 -47
- package/src/card-ux-approval.ts +0 -65
- package/src/card-ux-launcher.test.ts +0 -99
- package/src/card-ux-launcher.ts +0 -121
- package/src/card-ux-shared.ts +0 -33
- package/src/channel-runtime-api.ts +0 -16
- package/src/channel.runtime.ts +0 -47
- package/src/channel.test.ts +0 -959
- package/src/channel.ts +0 -1313
- package/src/chat-schema.ts +0 -25
- package/src/chat.test.ts +0 -196
- package/src/chat.ts +0 -188
- package/src/client.test.ts +0 -433
- package/src/client.ts +0 -290
- package/src/comment-dispatcher-runtime-api.ts +0 -6
- package/src/comment-dispatcher.test.ts +0 -169
- package/src/comment-dispatcher.ts +0 -107
- package/src/comment-handler-runtime-api.ts +0 -3
- package/src/comment-handler.test.ts +0 -486
- package/src/comment-handler.ts +0 -309
- package/src/comment-reaction.test.ts +0 -166
- package/src/comment-reaction.ts +0 -259
- package/src/comment-shared.test.ts +0 -182
- package/src/comment-shared.ts +0 -406
- package/src/comment-target.ts +0 -44
- package/src/config-schema.test.ts +0 -309
- package/src/config-schema.ts +0 -333
- package/src/conversation-id.test.ts +0 -18
- package/src/conversation-id.ts +0 -199
- package/src/dedup-runtime-api.ts +0 -1
- package/src/dedup.ts +0 -141
- package/src/directory.static.ts +0 -61
- package/src/directory.test.ts +0 -136
- package/src/directory.ts +0 -124
- package/src/doc-schema.ts +0 -182
- package/src/docx-batch-insert.test.ts +0 -91
- package/src/docx-batch-insert.ts +0 -223
- package/src/docx-color-text.ts +0 -154
- package/src/docx-table-ops.test.ts +0 -53
- package/src/docx-table-ops.ts +0 -316
- package/src/docx-types.ts +0 -38
- package/src/docx.account-selection.test.ts +0 -79
- package/src/docx.test.ts +0 -685
- package/src/docx.ts +0 -1616
- package/src/drive-schema.ts +0 -92
- package/src/drive.test.ts +0 -1219
- package/src/drive.ts +0 -829
- package/src/dynamic-agent.ts +0 -137
- package/src/event-types.ts +0 -45
- package/src/external-keys.test.ts +0 -20
- package/src/external-keys.ts +0 -19
- package/src/lifecycle.test-support.ts +0 -220
- package/src/media.test.ts +0 -900
- package/src/media.ts +0 -861
- package/src/mention-target.types.ts +0 -5
- package/src/mention.ts +0 -114
- package/src/message-action-contract.ts +0 -13
- package/src/monitor-state-runtime-api.ts +0 -7
- package/src/monitor-transport-runtime-api.ts +0 -7
- package/src/monitor.account.ts +0 -468
- package/src/monitor.acp-init-failure.lifecycle.test-support.ts +0 -219
- package/src/monitor.bot-identity.ts +0 -86
- package/src/monitor.bot-menu-handler.ts +0 -165
- package/src/monitor.bot-menu.lifecycle.test-support.ts +0 -224
- package/src/monitor.bot-menu.test.ts +0 -178
- package/src/monitor.broadcast.reply-once.lifecycle.test-support.ts +0 -264
- package/src/monitor.card-action.lifecycle.test-support.ts +0 -373
- package/src/monitor.cleanup.test.ts +0 -376
- package/src/monitor.comment-notice-handler.ts +0 -105
- package/src/monitor.comment.test.ts +0 -937
- package/src/monitor.comment.ts +0 -1386
- package/src/monitor.lifecycle.test.ts +0 -4
- package/src/monitor.message-handler.ts +0 -339
- package/src/monitor.reaction.lifecycle.test-support.ts +0 -68
- package/src/monitor.reaction.test.ts +0 -713
- package/src/monitor.startup.test.ts +0 -192
- package/src/monitor.startup.ts +0 -74
- package/src/monitor.state.defaults.test.ts +0 -46
- package/src/monitor.state.ts +0 -170
- package/src/monitor.synthetic-error.ts +0 -18
- package/src/monitor.test-mocks.ts +0 -45
- package/src/monitor.transport.ts +0 -424
- package/src/monitor.ts +0 -100
- package/src/monitor.webhook-e2e.test.ts +0 -272
- package/src/monitor.webhook-security.test.ts +0 -264
- package/src/monitor.webhook.test-helpers.ts +0 -116
- package/src/outbound-runtime-api.ts +0 -1
- package/src/outbound.test.ts +0 -935
- package/src/outbound.ts +0 -718
- package/src/perm-schema.ts +0 -52
- package/src/perm.ts +0 -170
- package/src/pins.ts +0 -108
- package/src/policy.test.ts +0 -334
- package/src/policy.ts +0 -236
- package/src/post.test.ts +0 -105
- package/src/post.ts +0 -275
- package/src/probe.test.ts +0 -275
- package/src/probe.ts +0 -166
- package/src/processing-claims.ts +0 -59
- package/src/qr-terminal.ts +0 -1
- package/src/reactions.ts +0 -123
- package/src/reasoning-preview.test.ts +0 -59
- package/src/reasoning-preview.ts +0 -20
- package/src/reply-dispatcher-runtime-api.ts +0 -7
- package/src/reply-dispatcher.test.ts +0 -1144
- package/src/reply-dispatcher.ts +0 -650
- package/src/runtime.ts +0 -9
- package/src/secret-contract.ts +0 -145
- package/src/secret-input.ts +0 -1
- package/src/security-audit-shared.ts +0 -69
- package/src/security-audit.test.ts +0 -61
- package/src/security-audit.ts +0 -1
- package/src/send-result.ts +0 -29
- package/src/send-target.test.ts +0 -80
- package/src/send-target.ts +0 -35
- package/src/send.reply-fallback.test.ts +0 -292
- package/src/send.test.ts +0 -550
- package/src/send.ts +0 -800
- package/src/sequential-key.test.ts +0 -72
- package/src/sequential-key.ts +0 -28
- package/src/sequential-queue.test.ts +0 -92
- package/src/sequential-queue.ts +0 -16
- package/src/session-conversation.ts +0 -42
- package/src/session-route.ts +0 -48
- package/src/setup-core.ts +0 -51
- package/src/setup-surface.test.ts +0 -174
- package/src/setup-surface.ts +0 -581
- package/src/streaming-card.test.ts +0 -190
- package/src/streaming-card.ts +0 -490
- package/src/subagent-hooks.test.ts +0 -603
- package/src/subagent-hooks.ts +0 -397
- package/src/targets.ts +0 -97
- package/src/test-support/lifecycle-test-support.ts +0 -453
- package/src/thread-bindings.test.ts +0 -143
- package/src/thread-bindings.ts +0 -330
- package/src/tool-account-routing.test.ts +0 -187
- package/src/tool-account.test.ts +0 -44
- package/src/tool-account.ts +0 -93
- package/src/tool-factory-test-harness.ts +0 -79
- package/src/tool-result.test.ts +0 -32
- package/src/tool-result.ts +0 -16
- package/src/tools-config.test.ts +0 -21
- package/src/tools-config.ts +0 -22
- package/src/types.ts +0 -104
- package/src/typing.test.ts +0 -144
- package/src/typing.ts +0 -214
- package/src/wiki-schema.ts +0 -55
- package/src/wiki.ts +0 -227
- package/subagent-hooks-api.ts +0 -31
- package/tsconfig.json +0 -16
|
@@ -1,713 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
createInboundDebouncer,
|
|
3
|
-
resolveInboundDebounceMs,
|
|
4
|
-
} from "openclaw/plugin-sdk/channel-inbound-debounce";
|
|
5
|
-
import { hasControlCommand } from "openclaw/plugin-sdk/command-detection";
|
|
6
|
-
import { createNonExitingRuntimeEnv } from "openclaw/plugin-sdk/plugin-test-runtime";
|
|
7
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
8
|
-
import type { ClawdbotConfig, PluginRuntime } from "../runtime-api.js";
|
|
9
|
-
import { parseFeishuMessageEvent, type FeishuMessageEvent } from "./bot.js";
|
|
10
|
-
import * as dedup from "./dedup.js";
|
|
11
|
-
import {
|
|
12
|
-
monitorSingleAccount,
|
|
13
|
-
resolveReactionSyntheticEvent,
|
|
14
|
-
type FeishuReactionCreatedEvent,
|
|
15
|
-
} from "./monitor.account.js";
|
|
16
|
-
import { setFeishuRuntime } from "./runtime.js";
|
|
17
|
-
import type { ResolvedFeishuAccount } from "./types.js";
|
|
18
|
-
|
|
19
|
-
const handleFeishuMessageMock = vi.hoisted(() => vi.fn(async (_params: { event?: unknown }) => {}));
|
|
20
|
-
const createEventDispatcherMock = vi.hoisted(() => vi.fn());
|
|
21
|
-
const monitorWebSocketMock = vi.hoisted(() => vi.fn(async () => {}));
|
|
22
|
-
const monitorWebhookMock = vi.hoisted(() => vi.fn(async () => {}));
|
|
23
|
-
const createFeishuThreadBindingManagerMock = vi.hoisted(() => vi.fn(() => ({ stop: vi.fn() })));
|
|
24
|
-
|
|
25
|
-
let handlers: Record<string, (data: unknown) => Promise<void>> = {};
|
|
26
|
-
|
|
27
|
-
vi.mock("./client.js", () => ({
|
|
28
|
-
createEventDispatcher: createEventDispatcherMock,
|
|
29
|
-
}));
|
|
30
|
-
|
|
31
|
-
vi.mock("./bot.js", async () => {
|
|
32
|
-
const actual = await vi.importActual<typeof import("./bot.js")>("./bot.js");
|
|
33
|
-
return {
|
|
34
|
-
...actual,
|
|
35
|
-
handleFeishuMessage: handleFeishuMessageMock,
|
|
36
|
-
};
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
vi.mock("./monitor.transport.js", () => ({
|
|
40
|
-
monitorWebSocket: monitorWebSocketMock,
|
|
41
|
-
monitorWebhook: monitorWebhookMock,
|
|
42
|
-
}));
|
|
43
|
-
|
|
44
|
-
vi.mock("./thread-bindings.js", () => ({
|
|
45
|
-
createFeishuThreadBindingManager: createFeishuThreadBindingManagerMock,
|
|
46
|
-
}));
|
|
47
|
-
|
|
48
|
-
const cfg = {} as ClawdbotConfig;
|
|
49
|
-
|
|
50
|
-
function makeReactionEvent(
|
|
51
|
-
overrides: Partial<FeishuReactionCreatedEvent> = {},
|
|
52
|
-
): FeishuReactionCreatedEvent {
|
|
53
|
-
return {
|
|
54
|
-
message_id: "om_msg1",
|
|
55
|
-
reaction_type: { emoji_type: "THUMBSUP" },
|
|
56
|
-
operator_type: "user",
|
|
57
|
-
user_id: { open_id: "ou_user1" },
|
|
58
|
-
...overrides,
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function createFetchedReactionMessage(chatId: string, chatType?: "p2p" | "group" | "private") {
|
|
63
|
-
return {
|
|
64
|
-
messageId: "om_msg1",
|
|
65
|
-
chatId,
|
|
66
|
-
chatType,
|
|
67
|
-
senderOpenId: "ou_bot",
|
|
68
|
-
content: "hello",
|
|
69
|
-
contentType: "text",
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
async function resolveReactionWithLookup(params: {
|
|
74
|
-
event?: FeishuReactionCreatedEvent;
|
|
75
|
-
lookupChatId: string;
|
|
76
|
-
lookupChatType?: "p2p" | "group" | "private";
|
|
77
|
-
}) {
|
|
78
|
-
return await resolveReactionSyntheticEvent({
|
|
79
|
-
cfg,
|
|
80
|
-
accountId: "default",
|
|
81
|
-
event: params.event ?? makeReactionEvent(),
|
|
82
|
-
botOpenId: "ou_bot",
|
|
83
|
-
fetchMessage: async () =>
|
|
84
|
-
createFetchedReactionMessage(params.lookupChatId, params.lookupChatType),
|
|
85
|
-
uuid: () => "fixed-uuid",
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
async function resolveNonBotReaction(params?: { cfg?: ClawdbotConfig; uuid?: () => string }) {
|
|
90
|
-
return await resolveReactionSyntheticEvent({
|
|
91
|
-
cfg: params?.cfg ?? cfg,
|
|
92
|
-
accountId: "default",
|
|
93
|
-
event: makeReactionEvent(),
|
|
94
|
-
botOpenId: "ou_bot",
|
|
95
|
-
fetchMessage: async () => ({
|
|
96
|
-
messageId: "om_msg1",
|
|
97
|
-
chatId: "oc_group",
|
|
98
|
-
chatType: "group",
|
|
99
|
-
senderOpenId: "ou_other",
|
|
100
|
-
senderType: "user",
|
|
101
|
-
content: "hello",
|
|
102
|
-
contentType: "text",
|
|
103
|
-
}),
|
|
104
|
-
...(params?.uuid ? { uuid: params.uuid } : {}),
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
type FeishuMention = NonNullable<FeishuMessageEvent["message"]["mentions"]>[number];
|
|
109
|
-
|
|
110
|
-
function buildDebounceConfig(): ClawdbotConfig {
|
|
111
|
-
return {
|
|
112
|
-
messages: {
|
|
113
|
-
inbound: {
|
|
114
|
-
debounceMs: 0,
|
|
115
|
-
byChannel: {
|
|
116
|
-
feishu: 20,
|
|
117
|
-
},
|
|
118
|
-
},
|
|
119
|
-
},
|
|
120
|
-
channels: {
|
|
121
|
-
feishu: {
|
|
122
|
-
enabled: true,
|
|
123
|
-
},
|
|
124
|
-
},
|
|
125
|
-
} as ClawdbotConfig;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function buildDebounceAccount(): ResolvedFeishuAccount {
|
|
129
|
-
return {
|
|
130
|
-
accountId: "default",
|
|
131
|
-
enabled: true,
|
|
132
|
-
configured: true,
|
|
133
|
-
appId: "cli_test",
|
|
134
|
-
appSecret: "secret_test", // pragma: allowlist secret
|
|
135
|
-
domain: "feishu",
|
|
136
|
-
config: {
|
|
137
|
-
enabled: true,
|
|
138
|
-
connectionMode: "websocket",
|
|
139
|
-
},
|
|
140
|
-
} as ResolvedFeishuAccount;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
function createTextEvent(params: {
|
|
144
|
-
messageId: string;
|
|
145
|
-
text: string;
|
|
146
|
-
senderId?: string;
|
|
147
|
-
mentions?: FeishuMention[];
|
|
148
|
-
}): FeishuMessageEvent {
|
|
149
|
-
const senderId = params.senderId ?? "ou_sender";
|
|
150
|
-
return {
|
|
151
|
-
sender: {
|
|
152
|
-
sender_id: { open_id: senderId },
|
|
153
|
-
sender_type: "user",
|
|
154
|
-
},
|
|
155
|
-
message: {
|
|
156
|
-
message_id: params.messageId,
|
|
157
|
-
chat_id: "oc_group_1",
|
|
158
|
-
chat_type: "group",
|
|
159
|
-
message_type: "text",
|
|
160
|
-
content: JSON.stringify({ text: params.text }),
|
|
161
|
-
mentions: params.mentions,
|
|
162
|
-
},
|
|
163
|
-
};
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
async function setupDebounceMonitor(params?: {
|
|
167
|
-
botOpenId?: string;
|
|
168
|
-
botName?: string;
|
|
169
|
-
}): Promise<(data: unknown) => Promise<void>> {
|
|
170
|
-
const register = vi.fn((registered: Record<string, (data: unknown) => Promise<void>>) => {
|
|
171
|
-
handlers = registered;
|
|
172
|
-
});
|
|
173
|
-
createEventDispatcherMock.mockReturnValue({ register });
|
|
174
|
-
|
|
175
|
-
await monitorSingleAccount({
|
|
176
|
-
cfg: buildDebounceConfig(),
|
|
177
|
-
account: buildDebounceAccount(),
|
|
178
|
-
runtime: createNonExitingRuntimeEnv(),
|
|
179
|
-
botOpenIdSource: {
|
|
180
|
-
kind: "prefetched",
|
|
181
|
-
botOpenId: params?.botOpenId ?? "ou_bot",
|
|
182
|
-
botName: params?.botName,
|
|
183
|
-
},
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
const onMessage = handlers["im.message.receive_v1"];
|
|
187
|
-
if (!onMessage) {
|
|
188
|
-
throw new Error("missing im.message.receive_v1 handler");
|
|
189
|
-
}
|
|
190
|
-
return onMessage;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
function getFirstDispatchedEvent(): FeishuMessageEvent {
|
|
194
|
-
const firstCall = handleFeishuMessageMock.mock.calls[0];
|
|
195
|
-
if (!firstCall) {
|
|
196
|
-
throw new Error("missing dispatch call");
|
|
197
|
-
}
|
|
198
|
-
const firstParams = firstCall[0] as { event?: FeishuMessageEvent } | undefined;
|
|
199
|
-
if (!firstParams?.event) {
|
|
200
|
-
throw new Error("missing dispatched event payload");
|
|
201
|
-
}
|
|
202
|
-
return firstParams.event;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
function expectSingleDispatchedEvent(): FeishuMessageEvent {
|
|
206
|
-
expect(handleFeishuMessageMock).toHaveBeenCalledTimes(1);
|
|
207
|
-
return getFirstDispatchedEvent();
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
function expectParsedFirstDispatchedEvent(botOpenId = "ou_bot") {
|
|
211
|
-
const dispatched = expectSingleDispatchedEvent();
|
|
212
|
-
return {
|
|
213
|
-
dispatched,
|
|
214
|
-
parsed: parseFeishuMessageEvent(dispatched, botOpenId),
|
|
215
|
-
};
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
function setDedupPassThroughMocks(): void {
|
|
219
|
-
vi.spyOn(dedup, "tryBeginFeishuMessageProcessing").mockReturnValue(true);
|
|
220
|
-
vi.spyOn(dedup, "recordProcessedFeishuMessage").mockResolvedValue(true);
|
|
221
|
-
vi.spyOn(dedup, "hasProcessedFeishuMessage").mockResolvedValue(false);
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
function createMention(params: { openId: string; name: string; key?: string }): FeishuMention {
|
|
225
|
-
return {
|
|
226
|
-
key: params.key ?? "@_user_1",
|
|
227
|
-
id: { open_id: params.openId },
|
|
228
|
-
name: params.name,
|
|
229
|
-
};
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
function createFeishuMonitorRuntime(params?: {
|
|
233
|
-
createInboundDebouncer?: PluginRuntime["channel"]["debounce"]["createInboundDebouncer"];
|
|
234
|
-
resolveInboundDebounceMs?: PluginRuntime["channel"]["debounce"]["resolveInboundDebounceMs"];
|
|
235
|
-
hasControlCommand?: PluginRuntime["channel"]["text"]["hasControlCommand"];
|
|
236
|
-
}): PluginRuntime {
|
|
237
|
-
return {
|
|
238
|
-
channel: {
|
|
239
|
-
debounce: {
|
|
240
|
-
createInboundDebouncer: params?.createInboundDebouncer ?? createInboundDebouncer,
|
|
241
|
-
resolveInboundDebounceMs: params?.resolveInboundDebounceMs ?? resolveInboundDebounceMs,
|
|
242
|
-
},
|
|
243
|
-
text: {
|
|
244
|
-
hasControlCommand: params?.hasControlCommand ?? hasControlCommand,
|
|
245
|
-
},
|
|
246
|
-
},
|
|
247
|
-
} as unknown as PluginRuntime;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
async function enqueueDebouncedMessage(
|
|
251
|
-
onMessage: (data: unknown) => Promise<void>,
|
|
252
|
-
event: FeishuMessageEvent,
|
|
253
|
-
): Promise<void> {
|
|
254
|
-
await onMessage(event);
|
|
255
|
-
await Promise.resolve();
|
|
256
|
-
await Promise.resolve();
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
function setStaleRetryMocks(messageId = "om_old") {
|
|
260
|
-
vi.spyOn(dedup, "hasProcessedFeishuMessage").mockImplementation(
|
|
261
|
-
async (currentMessageId) => currentMessageId === messageId,
|
|
262
|
-
);
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
describe("resolveReactionSyntheticEvent", () => {
|
|
266
|
-
it("filters app self-reactions", async () => {
|
|
267
|
-
const event = makeReactionEvent({ operator_type: "app" });
|
|
268
|
-
const result = await resolveReactionSyntheticEvent({
|
|
269
|
-
cfg,
|
|
270
|
-
accountId: "default",
|
|
271
|
-
event,
|
|
272
|
-
botOpenId: "ou_bot",
|
|
273
|
-
});
|
|
274
|
-
expect(result).toBeNull();
|
|
275
|
-
});
|
|
276
|
-
|
|
277
|
-
it("filters Typing reactions", async () => {
|
|
278
|
-
const event = makeReactionEvent({ reaction_type: { emoji_type: "Typing" } });
|
|
279
|
-
const result = await resolveReactionSyntheticEvent({
|
|
280
|
-
cfg,
|
|
281
|
-
accountId: "default",
|
|
282
|
-
event,
|
|
283
|
-
botOpenId: "ou_bot",
|
|
284
|
-
});
|
|
285
|
-
expect(result).toBeNull();
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
it("fails closed when bot open_id is unavailable", async () => {
|
|
289
|
-
const event = makeReactionEvent();
|
|
290
|
-
const result = await resolveReactionSyntheticEvent({
|
|
291
|
-
cfg,
|
|
292
|
-
accountId: "default",
|
|
293
|
-
event,
|
|
294
|
-
});
|
|
295
|
-
expect(result).toBeNull();
|
|
296
|
-
});
|
|
297
|
-
|
|
298
|
-
it("drops reactions when reactionNotifications is off", async () => {
|
|
299
|
-
const event = makeReactionEvent();
|
|
300
|
-
const result = await resolveReactionSyntheticEvent({
|
|
301
|
-
cfg: {
|
|
302
|
-
channels: {
|
|
303
|
-
feishu: {
|
|
304
|
-
reactionNotifications: "off",
|
|
305
|
-
},
|
|
306
|
-
},
|
|
307
|
-
} as ClawdbotConfig,
|
|
308
|
-
accountId: "default",
|
|
309
|
-
event,
|
|
310
|
-
botOpenId: "ou_bot",
|
|
311
|
-
fetchMessage: async () => ({
|
|
312
|
-
messageId: "om_msg1",
|
|
313
|
-
chatId: "oc_group",
|
|
314
|
-
senderOpenId: "ou_bot",
|
|
315
|
-
senderType: "app",
|
|
316
|
-
content: "hello",
|
|
317
|
-
contentType: "text",
|
|
318
|
-
}),
|
|
319
|
-
});
|
|
320
|
-
expect(result).toBeNull();
|
|
321
|
-
});
|
|
322
|
-
|
|
323
|
-
it("filters reactions on non-bot messages", async () => {
|
|
324
|
-
const result = await resolveNonBotReaction();
|
|
325
|
-
expect(result).toBeNull();
|
|
326
|
-
});
|
|
327
|
-
|
|
328
|
-
it("allows non-bot reactions when reactionNotifications is all", async () => {
|
|
329
|
-
const result = await resolveNonBotReaction({
|
|
330
|
-
cfg: {
|
|
331
|
-
channels: {
|
|
332
|
-
feishu: {
|
|
333
|
-
reactionNotifications: "all",
|
|
334
|
-
},
|
|
335
|
-
},
|
|
336
|
-
} as ClawdbotConfig,
|
|
337
|
-
uuid: () => "fixed-uuid",
|
|
338
|
-
});
|
|
339
|
-
expect(result?.message.message_id).toBe("om_msg1:reaction:THUMBSUP:fixed-uuid");
|
|
340
|
-
});
|
|
341
|
-
|
|
342
|
-
it("drops unverified reactions when sender verification times out", async () => {
|
|
343
|
-
const event = makeReactionEvent();
|
|
344
|
-
const result = await resolveReactionSyntheticEvent({
|
|
345
|
-
cfg,
|
|
346
|
-
accountId: "default",
|
|
347
|
-
event,
|
|
348
|
-
botOpenId: "ou_bot",
|
|
349
|
-
verificationTimeoutMs: 1,
|
|
350
|
-
fetchMessage: async () =>
|
|
351
|
-
await new Promise<never>(() => {
|
|
352
|
-
// Never resolves
|
|
353
|
-
}),
|
|
354
|
-
});
|
|
355
|
-
expect(result).toBeNull();
|
|
356
|
-
});
|
|
357
|
-
|
|
358
|
-
it("uses event chat context when provided", async () => {
|
|
359
|
-
const result = await resolveReactionWithLookup({
|
|
360
|
-
event: makeReactionEvent({
|
|
361
|
-
chat_id: "oc_group_from_event",
|
|
362
|
-
chat_type: "group",
|
|
363
|
-
}),
|
|
364
|
-
lookupChatId: "oc_group_from_lookup",
|
|
365
|
-
});
|
|
366
|
-
|
|
367
|
-
expect(result).toEqual({
|
|
368
|
-
sender: {
|
|
369
|
-
sender_id: { open_id: "ou_user1" },
|
|
370
|
-
sender_type: "user",
|
|
371
|
-
},
|
|
372
|
-
message: {
|
|
373
|
-
message_id: "om_msg1:reaction:THUMBSUP:fixed-uuid",
|
|
374
|
-
chat_id: "oc_group_from_event",
|
|
375
|
-
chat_type: "group",
|
|
376
|
-
message_type: "text",
|
|
377
|
-
content: JSON.stringify({
|
|
378
|
-
text: "[reacted with THUMBSUP to message om_msg1]",
|
|
379
|
-
}),
|
|
380
|
-
},
|
|
381
|
-
});
|
|
382
|
-
});
|
|
383
|
-
|
|
384
|
-
it("falls back to reacted message chat_id when event chat_id is absent", async () => {
|
|
385
|
-
const result = await resolveReactionWithLookup({
|
|
386
|
-
lookupChatId: "oc_group_from_lookup",
|
|
387
|
-
lookupChatType: "group",
|
|
388
|
-
});
|
|
389
|
-
|
|
390
|
-
expect(result?.message.chat_id).toBe("oc_group_from_lookup");
|
|
391
|
-
expect(result?.message.chat_type).toBe("group");
|
|
392
|
-
});
|
|
393
|
-
|
|
394
|
-
it("falls back to sender p2p chat when lookup returns empty chat_id", async () => {
|
|
395
|
-
const result = await resolveReactionWithLookup({
|
|
396
|
-
lookupChatId: "",
|
|
397
|
-
lookupChatType: "p2p",
|
|
398
|
-
});
|
|
399
|
-
|
|
400
|
-
expect(result?.message.chat_id).toBe("p2p:ou_user1");
|
|
401
|
-
expect(result?.message.chat_type).toBe("p2p");
|
|
402
|
-
});
|
|
403
|
-
|
|
404
|
-
it("drops reactions without chat context when lookup does not provide chat_type", async () => {
|
|
405
|
-
const result = await resolveReactionWithLookup({
|
|
406
|
-
lookupChatId: "oc_group_from_lookup",
|
|
407
|
-
});
|
|
408
|
-
|
|
409
|
-
expect(result).toBeNull();
|
|
410
|
-
});
|
|
411
|
-
|
|
412
|
-
it("drops reactions when event chat_type is invalid and lookup cannot recover it", async () => {
|
|
413
|
-
const result = await resolveReactionWithLookup({
|
|
414
|
-
event: makeReactionEvent({
|
|
415
|
-
chat_id: "oc_group_from_event",
|
|
416
|
-
chat_type: "bogus" as "group",
|
|
417
|
-
}),
|
|
418
|
-
lookupChatId: "oc_group_from_lookup",
|
|
419
|
-
});
|
|
420
|
-
|
|
421
|
-
expect(result).toBeNull();
|
|
422
|
-
});
|
|
423
|
-
|
|
424
|
-
it("logs and drops reactions when lookup throws", async () => {
|
|
425
|
-
const log = vi.fn();
|
|
426
|
-
const event = makeReactionEvent();
|
|
427
|
-
const result = await resolveReactionSyntheticEvent({
|
|
428
|
-
cfg,
|
|
429
|
-
accountId: "acct1",
|
|
430
|
-
event,
|
|
431
|
-
botOpenId: "ou_bot",
|
|
432
|
-
fetchMessage: async () => {
|
|
433
|
-
throw new Error("boom");
|
|
434
|
-
},
|
|
435
|
-
logger: log,
|
|
436
|
-
});
|
|
437
|
-
expect(result).toBeNull();
|
|
438
|
-
expect(log).toHaveBeenCalledWith(
|
|
439
|
-
expect.stringContaining("ignoring reaction on non-bot/unverified message om_msg1"),
|
|
440
|
-
);
|
|
441
|
-
});
|
|
442
|
-
});
|
|
443
|
-
|
|
444
|
-
describe("monitorSingleAccount lifecycle", () => {
|
|
445
|
-
beforeEach(() => {
|
|
446
|
-
createFeishuThreadBindingManagerMock.mockReset().mockImplementation(() => ({
|
|
447
|
-
stop: vi.fn(),
|
|
448
|
-
}));
|
|
449
|
-
createEventDispatcherMock.mockReset().mockReturnValue({
|
|
450
|
-
register: vi.fn(),
|
|
451
|
-
});
|
|
452
|
-
});
|
|
453
|
-
|
|
454
|
-
it("stops the Feishu thread binding manager when the monitor exits", async () => {
|
|
455
|
-
setFeishuRuntime(createFeishuMonitorRuntime());
|
|
456
|
-
|
|
457
|
-
await monitorSingleAccount({
|
|
458
|
-
cfg: buildDebounceConfig(),
|
|
459
|
-
account: buildDebounceAccount(),
|
|
460
|
-
runtime: createNonExitingRuntimeEnv(),
|
|
461
|
-
botOpenIdSource: {
|
|
462
|
-
kind: "prefetched",
|
|
463
|
-
botOpenId: "ou_bot",
|
|
464
|
-
},
|
|
465
|
-
});
|
|
466
|
-
|
|
467
|
-
const manager = createFeishuThreadBindingManagerMock.mock.results[0]?.value as
|
|
468
|
-
| { stop: ReturnType<typeof vi.fn> }
|
|
469
|
-
| undefined;
|
|
470
|
-
expect(manager?.stop).toHaveBeenCalledTimes(1);
|
|
471
|
-
});
|
|
472
|
-
|
|
473
|
-
it("stops the Feishu thread binding manager when setup fails before transport starts", async () => {
|
|
474
|
-
setFeishuRuntime(createFeishuMonitorRuntime());
|
|
475
|
-
createEventDispatcherMock.mockReturnValue({
|
|
476
|
-
get register() {
|
|
477
|
-
throw new Error("register failed");
|
|
478
|
-
},
|
|
479
|
-
});
|
|
480
|
-
|
|
481
|
-
await expect(
|
|
482
|
-
monitorSingleAccount({
|
|
483
|
-
cfg: buildDebounceConfig(),
|
|
484
|
-
account: buildDebounceAccount(),
|
|
485
|
-
runtime: createNonExitingRuntimeEnv(),
|
|
486
|
-
botOpenIdSource: {
|
|
487
|
-
kind: "prefetched",
|
|
488
|
-
botOpenId: "ou_bot",
|
|
489
|
-
},
|
|
490
|
-
}),
|
|
491
|
-
).rejects.toThrow("register failed");
|
|
492
|
-
|
|
493
|
-
const manager = createFeishuThreadBindingManagerMock.mock.results[0]?.value as
|
|
494
|
-
| { stop: ReturnType<typeof vi.fn> }
|
|
495
|
-
| undefined;
|
|
496
|
-
expect(manager?.stop).toHaveBeenCalledTimes(1);
|
|
497
|
-
});
|
|
498
|
-
});
|
|
499
|
-
|
|
500
|
-
describe("Feishu inbound debounce regressions", () => {
|
|
501
|
-
beforeEach(() => {
|
|
502
|
-
vi.useFakeTimers();
|
|
503
|
-
handlers = {};
|
|
504
|
-
handleFeishuMessageMock.mockClear();
|
|
505
|
-
setFeishuRuntime(createFeishuMonitorRuntime());
|
|
506
|
-
});
|
|
507
|
-
|
|
508
|
-
afterEach(() => {
|
|
509
|
-
vi.useRealTimers();
|
|
510
|
-
vi.restoreAllMocks();
|
|
511
|
-
});
|
|
512
|
-
|
|
513
|
-
it("keeps bot mention when per-message mention keys collide across non-forward messages", async () => {
|
|
514
|
-
setDedupPassThroughMocks();
|
|
515
|
-
const onMessage = await setupDebounceMonitor();
|
|
516
|
-
|
|
517
|
-
await enqueueDebouncedMessage(
|
|
518
|
-
onMessage,
|
|
519
|
-
createTextEvent({
|
|
520
|
-
messageId: "om_1",
|
|
521
|
-
text: "first",
|
|
522
|
-
mentions: [createMention({ openId: "ou_user_a", name: "user-a" })],
|
|
523
|
-
}),
|
|
524
|
-
);
|
|
525
|
-
await enqueueDebouncedMessage(
|
|
526
|
-
onMessage,
|
|
527
|
-
createTextEvent({
|
|
528
|
-
messageId: "om_2",
|
|
529
|
-
text: "@bot second",
|
|
530
|
-
mentions: [createMention({ openId: "ou_bot", name: "bot" })],
|
|
531
|
-
}),
|
|
532
|
-
);
|
|
533
|
-
await vi.advanceTimersByTimeAsync(25);
|
|
534
|
-
|
|
535
|
-
const dispatched = expectSingleDispatchedEvent();
|
|
536
|
-
const mergedMentions = dispatched.message.mentions ?? [];
|
|
537
|
-
expect(mergedMentions.some((mention) => mention.id.open_id === "ou_bot")).toBe(true);
|
|
538
|
-
expect(mergedMentions.some((mention) => mention.id.open_id === "ou_user_a")).toBe(false);
|
|
539
|
-
});
|
|
540
|
-
|
|
541
|
-
it("passes prefetched botName through to handleFeishuMessage", async () => {
|
|
542
|
-
vi.spyOn(dedup, "tryBeginFeishuMessageProcessing").mockReturnValue(true);
|
|
543
|
-
vi.spyOn(dedup, "recordProcessedFeishuMessage").mockResolvedValue(true);
|
|
544
|
-
vi.spyOn(dedup, "hasProcessedFeishuMessage").mockResolvedValue(false);
|
|
545
|
-
const onMessage = await setupDebounceMonitor({ botName: "OpenClaw Bot" });
|
|
546
|
-
|
|
547
|
-
await onMessage(
|
|
548
|
-
createTextEvent({
|
|
549
|
-
messageId: "om_name_passthrough",
|
|
550
|
-
text: "@bot hello",
|
|
551
|
-
mentions: [
|
|
552
|
-
{
|
|
553
|
-
key: "@_user_1",
|
|
554
|
-
id: { open_id: "ou_bot" },
|
|
555
|
-
name: "OpenClaw Bot",
|
|
556
|
-
},
|
|
557
|
-
],
|
|
558
|
-
}),
|
|
559
|
-
);
|
|
560
|
-
await Promise.resolve();
|
|
561
|
-
await Promise.resolve();
|
|
562
|
-
await vi.advanceTimersByTimeAsync(25);
|
|
563
|
-
|
|
564
|
-
expect(handleFeishuMessageMock).toHaveBeenCalledTimes(1);
|
|
565
|
-
const firstParams = handleFeishuMessageMock.mock.calls[0]?.[0] as
|
|
566
|
-
| { botName?: string }
|
|
567
|
-
| undefined;
|
|
568
|
-
expect(firstParams?.botName).toBe("OpenClaw Bot");
|
|
569
|
-
});
|
|
570
|
-
|
|
571
|
-
it("does not synthesize mention-forward intent across separate messages", async () => {
|
|
572
|
-
setDedupPassThroughMocks();
|
|
573
|
-
const onMessage = await setupDebounceMonitor();
|
|
574
|
-
|
|
575
|
-
await enqueueDebouncedMessage(
|
|
576
|
-
onMessage,
|
|
577
|
-
createTextEvent({
|
|
578
|
-
messageId: "om_user_mention",
|
|
579
|
-
text: "@alice first",
|
|
580
|
-
mentions: [createMention({ openId: "ou_alice", name: "alice" })],
|
|
581
|
-
}),
|
|
582
|
-
);
|
|
583
|
-
await enqueueDebouncedMessage(
|
|
584
|
-
onMessage,
|
|
585
|
-
createTextEvent({
|
|
586
|
-
messageId: "om_bot_mention",
|
|
587
|
-
text: "@bot second",
|
|
588
|
-
mentions: [createMention({ openId: "ou_bot", name: "bot" })],
|
|
589
|
-
}),
|
|
590
|
-
);
|
|
591
|
-
await vi.advanceTimersByTimeAsync(25);
|
|
592
|
-
|
|
593
|
-
const { dispatched, parsed } = expectParsedFirstDispatchedEvent();
|
|
594
|
-
expect(parsed.mentionedBot).toBe(true);
|
|
595
|
-
expect(parsed.mentionTargets).toBeUndefined();
|
|
596
|
-
const mergedMentions = dispatched.message.mentions ?? [];
|
|
597
|
-
expect(mergedMentions.every((mention) => mention.id.open_id === "ou_bot")).toBe(true);
|
|
598
|
-
});
|
|
599
|
-
|
|
600
|
-
it("preserves bot mention signal when the latest merged message has no mentions", async () => {
|
|
601
|
-
setDedupPassThroughMocks();
|
|
602
|
-
const onMessage = await setupDebounceMonitor();
|
|
603
|
-
|
|
604
|
-
await enqueueDebouncedMessage(
|
|
605
|
-
onMessage,
|
|
606
|
-
createTextEvent({
|
|
607
|
-
messageId: "om_bot_first",
|
|
608
|
-
text: "@bot first",
|
|
609
|
-
mentions: [createMention({ openId: "ou_bot", name: "bot" })],
|
|
610
|
-
}),
|
|
611
|
-
);
|
|
612
|
-
await enqueueDebouncedMessage(
|
|
613
|
-
onMessage,
|
|
614
|
-
createTextEvent({
|
|
615
|
-
messageId: "om_plain_second",
|
|
616
|
-
text: "plain follow-up",
|
|
617
|
-
}),
|
|
618
|
-
);
|
|
619
|
-
await vi.advanceTimersByTimeAsync(25);
|
|
620
|
-
|
|
621
|
-
const { parsed } = expectParsedFirstDispatchedEvent();
|
|
622
|
-
expect(parsed.mentionedBot).toBe(true);
|
|
623
|
-
});
|
|
624
|
-
|
|
625
|
-
it("excludes previously processed retries from combined debounce text", async () => {
|
|
626
|
-
vi.spyOn(dedup, "tryBeginFeishuMessageProcessing").mockReturnValue(true);
|
|
627
|
-
vi.spyOn(dedup, "recordProcessedFeishuMessage").mockResolvedValue(true);
|
|
628
|
-
setStaleRetryMocks();
|
|
629
|
-
const onMessage = await setupDebounceMonitor();
|
|
630
|
-
|
|
631
|
-
await onMessage(createTextEvent({ messageId: "om_old", text: "stale" }));
|
|
632
|
-
await Promise.resolve();
|
|
633
|
-
await Promise.resolve();
|
|
634
|
-
await onMessage(createTextEvent({ messageId: "om_new_1", text: "first" }));
|
|
635
|
-
await Promise.resolve();
|
|
636
|
-
await Promise.resolve();
|
|
637
|
-
await onMessage(createTextEvent({ messageId: "om_old", text: "stale" }));
|
|
638
|
-
await Promise.resolve();
|
|
639
|
-
await Promise.resolve();
|
|
640
|
-
await onMessage(createTextEvent({ messageId: "om_new_2", text: "second" }));
|
|
641
|
-
await Promise.resolve();
|
|
642
|
-
await Promise.resolve();
|
|
643
|
-
await vi.advanceTimersByTimeAsync(25);
|
|
644
|
-
|
|
645
|
-
const dispatched = expectSingleDispatchedEvent();
|
|
646
|
-
expect(dispatched.message.message_id).toBe("om_new_2");
|
|
647
|
-
const combined = JSON.parse(dispatched.message.content) as { text?: string };
|
|
648
|
-
expect(combined.text).toBe("first\nsecond");
|
|
649
|
-
});
|
|
650
|
-
|
|
651
|
-
it("uses latest fresh message id when debounce batch ends with stale retry", async () => {
|
|
652
|
-
vi.spyOn(dedup, "tryBeginFeishuMessageProcessing").mockReturnValue(true);
|
|
653
|
-
const recordSpy = vi.spyOn(dedup, "recordProcessedFeishuMessage").mockResolvedValue(true);
|
|
654
|
-
setStaleRetryMocks("om_old_latest_fresh");
|
|
655
|
-
const onMessage = await setupDebounceMonitor();
|
|
656
|
-
|
|
657
|
-
await onMessage(createTextEvent({ messageId: "om_new_latest_fresh", text: "fresh" }));
|
|
658
|
-
await Promise.resolve();
|
|
659
|
-
await Promise.resolve();
|
|
660
|
-
await onMessage(createTextEvent({ messageId: "om_old_latest_fresh", text: "stale" }));
|
|
661
|
-
await Promise.resolve();
|
|
662
|
-
await Promise.resolve();
|
|
663
|
-
await vi.advanceTimersByTimeAsync(25);
|
|
664
|
-
|
|
665
|
-
const dispatched = expectSingleDispatchedEvent();
|
|
666
|
-
expect(dispatched.message.message_id).toBe("om_new_latest_fresh");
|
|
667
|
-
const combined = JSON.parse(dispatched.message.content) as { text?: string };
|
|
668
|
-
expect(combined.text).toBe("fresh");
|
|
669
|
-
expect(recordSpy).toHaveBeenCalledWith("om_old_latest_fresh", "default", expect.any(Function));
|
|
670
|
-
expect(recordSpy).not.toHaveBeenCalledWith(
|
|
671
|
-
"om_new_latest_fresh",
|
|
672
|
-
"default",
|
|
673
|
-
expect.any(Function),
|
|
674
|
-
);
|
|
675
|
-
});
|
|
676
|
-
|
|
677
|
-
it("releases early event dedupe when debounced dispatch fails", async () => {
|
|
678
|
-
setDedupPassThroughMocks();
|
|
679
|
-
const enqueueMock = vi.fn();
|
|
680
|
-
setFeishuRuntime(
|
|
681
|
-
createFeishuMonitorRuntime({
|
|
682
|
-
createInboundDebouncer: <T>(params: { onError?: (err: unknown, items: T[]) => void }) => ({
|
|
683
|
-
enqueue: async (item: T) => {
|
|
684
|
-
enqueueMock(item);
|
|
685
|
-
params.onError?.(new Error("dispatch failed"), [item]);
|
|
686
|
-
},
|
|
687
|
-
flushKey: async () => {},
|
|
688
|
-
}),
|
|
689
|
-
}),
|
|
690
|
-
);
|
|
691
|
-
const onMessage = await setupDebounceMonitor();
|
|
692
|
-
const event = createTextEvent({ messageId: "om_retryable", text: "hello" });
|
|
693
|
-
|
|
694
|
-
await enqueueDebouncedMessage(onMessage, event);
|
|
695
|
-
expect(enqueueMock).toHaveBeenCalledTimes(1);
|
|
696
|
-
|
|
697
|
-
await enqueueDebouncedMessage(onMessage, event);
|
|
698
|
-
expect(enqueueMock).toHaveBeenCalledTimes(2);
|
|
699
|
-
expect(handleFeishuMessageMock).not.toHaveBeenCalled();
|
|
700
|
-
});
|
|
701
|
-
|
|
702
|
-
it("drops duplicate inbound events before they re-enter the debounce pipeline", async () => {
|
|
703
|
-
const onMessage = await setupDebounceMonitor();
|
|
704
|
-
const event = createTextEvent({ messageId: "om_duplicate", text: "hello" });
|
|
705
|
-
|
|
706
|
-
await enqueueDebouncedMessage(onMessage, event);
|
|
707
|
-
await vi.advanceTimersByTimeAsync(25);
|
|
708
|
-
await enqueueDebouncedMessage(onMessage, event);
|
|
709
|
-
await vi.advanceTimersByTimeAsync(25);
|
|
710
|
-
|
|
711
|
-
expect(handleFeishuMessageMock).toHaveBeenCalledTimes(1);
|
|
712
|
-
});
|
|
713
|
-
});
|