@kodelyth/feishu 2026.5.42 → 2026.6.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.
Files changed (192) hide show
  1. package/klaw.plugin.json +1712 -47
  2. package/package.json +19 -6
  3. package/api.ts +0 -32
  4. package/channel-entry.ts +0 -20
  5. package/channel-plugin-api.ts +0 -1
  6. package/contract-api.ts +0 -16
  7. package/index.ts +0 -82
  8. package/runtime-api.ts +0 -52
  9. package/secret-contract-api.ts +0 -5
  10. package/security-contract-api.ts +0 -1
  11. package/session-key-api.ts +0 -1
  12. package/setup-api.ts +0 -3
  13. package/setup-entry.test.ts +0 -19
  14. package/setup-entry.ts +0 -13
  15. package/src/accounts.test.ts +0 -480
  16. package/src/accounts.ts +0 -333
  17. package/src/agent-config.ts +0 -21
  18. package/src/app-registration.ts +0 -331
  19. package/src/approval-auth.test.ts +0 -24
  20. package/src/approval-auth.ts +0 -25
  21. package/src/async.test.ts +0 -35
  22. package/src/async.ts +0 -104
  23. package/src/audio-preflight.runtime.ts +0 -9
  24. package/src/bitable.test.ts +0 -136
  25. package/src/bitable.ts +0 -762
  26. package/src/bot-content.ts +0 -485
  27. package/src/bot-group-name.test.ts +0 -116
  28. package/src/bot-runtime-api.ts +0 -12
  29. package/src/bot-sender-name.ts +0 -125
  30. package/src/bot.broadcast.test.ts +0 -523
  31. package/src/bot.card-action.test.ts +0 -552
  32. package/src/bot.checkBotMentioned.test.ts +0 -265
  33. package/src/bot.helpers.test.ts +0 -135
  34. package/src/bot.stripBotMention.test.ts +0 -126
  35. package/src/bot.test.ts +0 -3671
  36. package/src/bot.ts +0 -1703
  37. package/src/card-action.ts +0 -447
  38. package/src/card-interaction.test.ts +0 -131
  39. package/src/card-interaction.ts +0 -159
  40. package/src/card-test-helpers.ts +0 -54
  41. package/src/card-ux-approval.ts +0 -65
  42. package/src/card-ux-launcher.test.ts +0 -106
  43. package/src/card-ux-launcher.ts +0 -121
  44. package/src/card-ux-shared.ts +0 -33
  45. package/src/channel-runtime-api.ts +0 -16
  46. package/src/channel.runtime.ts +0 -47
  47. package/src/channel.test.ts +0 -1151
  48. package/src/channel.ts +0 -1423
  49. package/src/chat-schema.ts +0 -25
  50. package/src/chat.test.ts +0 -240
  51. package/src/chat.ts +0 -188
  52. package/src/client-timeout.ts +0 -42
  53. package/src/client.test.ts +0 -447
  54. package/src/client.ts +0 -262
  55. package/src/comment-dispatcher-runtime-api.ts +0 -6
  56. package/src/comment-dispatcher.test.ts +0 -185
  57. package/src/comment-dispatcher.ts +0 -107
  58. package/src/comment-handler-runtime-api.ts +0 -3
  59. package/src/comment-handler.test.ts +0 -592
  60. package/src/comment-handler.ts +0 -303
  61. package/src/comment-reaction.test.ts +0 -138
  62. package/src/comment-reaction.ts +0 -259
  63. package/src/comment-shared.test.ts +0 -183
  64. package/src/comment-shared.ts +0 -406
  65. package/src/comment-target.ts +0 -44
  66. package/src/config-schema.test.ts +0 -326
  67. package/src/config-schema.ts +0 -335
  68. package/src/conversation-id.test.ts +0 -18
  69. package/src/conversation-id.ts +0 -199
  70. package/src/dedup-runtime-api.ts +0 -1
  71. package/src/dedup.ts +0 -141
  72. package/src/dedupe-key.ts +0 -72
  73. package/src/directory.static.ts +0 -61
  74. package/src/directory.test.ts +0 -141
  75. package/src/directory.ts +0 -124
  76. package/src/doc-schema.ts +0 -182
  77. package/src/docx-batch-insert.test.ts +0 -116
  78. package/src/docx-batch-insert.ts +0 -223
  79. package/src/docx-color-text.ts +0 -154
  80. package/src/docx-table-ops.test.ts +0 -53
  81. package/src/docx-table-ops.ts +0 -316
  82. package/src/docx-types.ts +0 -38
  83. package/src/docx.account-selection.test.ts +0 -95
  84. package/src/docx.test.ts +0 -701
  85. package/src/docx.ts +0 -1596
  86. package/src/drive-schema.ts +0 -92
  87. package/src/drive.test.ts +0 -1237
  88. package/src/drive.ts +0 -829
  89. package/src/dynamic-agent.test.ts +0 -155
  90. package/src/dynamic-agent.ts +0 -143
  91. package/src/event-types.ts +0 -45
  92. package/src/external-keys.test.ts +0 -20
  93. package/src/external-keys.ts +0 -19
  94. package/src/lifecycle.test-support.ts +0 -220
  95. package/src/media.test.ts +0 -955
  96. package/src/media.ts +0 -1105
  97. package/src/mention-target.types.ts +0 -5
  98. package/src/mention.ts +0 -114
  99. package/src/message-action-contract.ts +0 -13
  100. package/src/monitor-state-runtime-api.ts +0 -7
  101. package/src/monitor-transport-runtime-api.ts +0 -10
  102. package/src/monitor.account.ts +0 -492
  103. package/src/monitor.acp-init-failure.lifecycle.test-support.ts +0 -219
  104. package/src/monitor.bot-identity.ts +0 -86
  105. package/src/monitor.bot-menu-handler.ts +0 -165
  106. package/src/monitor.bot-menu.lifecycle.test-support.ts +0 -224
  107. package/src/monitor.bot-menu.test.ts +0 -188
  108. package/src/monitor.broadcast.reply-once.lifecycle.test-support.ts +0 -264
  109. package/src/monitor.card-action.lifecycle.test-support.ts +0 -421
  110. package/src/monitor.cleanup.test.ts +0 -383
  111. package/src/monitor.comment-notice-handler.ts +0 -105
  112. package/src/monitor.comment.test.ts +0 -967
  113. package/src/monitor.comment.ts +0 -1386
  114. package/src/monitor.lifecycle.test.ts +0 -4
  115. package/src/monitor.message-handler.ts +0 -350
  116. package/src/monitor.reaction.lifecycle.test-support.ts +0 -68
  117. package/src/monitor.reaction.test.ts +0 -739
  118. package/src/monitor.startup.test.ts +0 -213
  119. package/src/monitor.startup.ts +0 -74
  120. package/src/monitor.state.defaults.test.ts +0 -46
  121. package/src/monitor.state.ts +0 -170
  122. package/src/monitor.synthetic-error.ts +0 -18
  123. package/src/monitor.test-mocks.ts +0 -46
  124. package/src/monitor.transport.ts +0 -451
  125. package/src/monitor.ts +0 -100
  126. package/src/monitor.webhook-e2e.test.ts +0 -279
  127. package/src/monitor.webhook-security.test.ts +0 -389
  128. package/src/monitor.webhook.test-helpers.ts +0 -116
  129. package/src/outbound-runtime-api.ts +0 -1
  130. package/src/outbound.test.ts +0 -1118
  131. package/src/outbound.ts +0 -785
  132. package/src/perm-schema.ts +0 -52
  133. package/src/perm.ts +0 -170
  134. package/src/pins.ts +0 -108
  135. package/src/policy.test.ts +0 -223
  136. package/src/policy.ts +0 -318
  137. package/src/post.test.ts +0 -105
  138. package/src/post.ts +0 -275
  139. package/src/probe.test.ts +0 -283
  140. package/src/probe.ts +0 -166
  141. package/src/processing-claims.ts +0 -59
  142. package/src/qr-terminal.ts +0 -1
  143. package/src/reactions.ts +0 -123
  144. package/src/reasoning-preview.test.ts +0 -113
  145. package/src/reasoning-preview.ts +0 -28
  146. package/src/reply-dispatcher-runtime-api.ts +0 -7
  147. package/src/reply-dispatcher.test.ts +0 -1513
  148. package/src/reply-dispatcher.ts +0 -748
  149. package/src/runtime.ts +0 -9
  150. package/src/secret-contract.ts +0 -145
  151. package/src/secret-input.ts +0 -1
  152. package/src/security-audit-shared.ts +0 -69
  153. package/src/security-audit.test.ts +0 -59
  154. package/src/security-audit.ts +0 -1
  155. package/src/send-result.ts +0 -80
  156. package/src/send-target.test.ts +0 -86
  157. package/src/send-target.ts +0 -35
  158. package/src/send.reply-fallback.test.ts +0 -417
  159. package/src/send.test.ts +0 -621
  160. package/src/send.ts +0 -861
  161. package/src/sequential-key.test.ts +0 -72
  162. package/src/sequential-key.ts +0 -25
  163. package/src/sequential-queue.test.ts +0 -165
  164. package/src/sequential-queue.ts +0 -86
  165. package/src/session-conversation.ts +0 -42
  166. package/src/session-route.ts +0 -48
  167. package/src/setup-core.ts +0 -51
  168. package/src/setup-surface.test.ts +0 -484
  169. package/src/setup-surface.ts +0 -618
  170. package/src/streaming-card.test.ts +0 -397
  171. package/src/streaming-card.ts +0 -571
  172. package/src/subagent-hooks.test.ts +0 -627
  173. package/src/subagent-hooks.ts +0 -413
  174. package/src/targets.ts +0 -97
  175. package/src/test-support/lifecycle-test-support.ts +0 -454
  176. package/src/thread-bindings.test.ts +0 -180
  177. package/src/thread-bindings.ts +0 -331
  178. package/src/tool-account-routing.test.ts +0 -250
  179. package/src/tool-account.test.ts +0 -44
  180. package/src/tool-account.ts +0 -93
  181. package/src/tool-factory-test-harness.ts +0 -79
  182. package/src/tool-result.test.ts +0 -32
  183. package/src/tool-result.ts +0 -16
  184. package/src/tools-config.test.ts +0 -21
  185. package/src/tools-config.ts +0 -22
  186. package/src/types.ts +0 -106
  187. package/src/typing.test.ts +0 -144
  188. package/src/typing.ts +0 -214
  189. package/src/wiki-schema.ts +0 -69
  190. package/src/wiki.ts +0 -270
  191. package/subagent-hooks-api.ts +0 -31
  192. package/tsconfig.json +0 -16
@@ -1,1151 +0,0 @@
1
- import { afterAll, beforeEach, describe, expect, it, vi } from "vitest";
2
- import type { KlawConfig } from "../runtime-api.js";
3
- import { feishuPlugin } from "./channel.js";
4
- import { looksLikeFeishuId, normalizeFeishuTarget, resolveReceiveIdType } from "./targets.js";
5
-
6
- const probeFeishuMock = vi.hoisted(() => vi.fn());
7
- const createFeishuClientMock = vi.hoisted(() => vi.fn());
8
- const addReactionFeishuMock = vi.hoisted(() => vi.fn());
9
- const listReactionsFeishuMock = vi.hoisted(() => vi.fn());
10
- const removeReactionFeishuMock = vi.hoisted(() => vi.fn());
11
- const sendCardFeishuMock = vi.hoisted(() => vi.fn());
12
- const sendMessageFeishuMock = vi.hoisted(() => vi.fn());
13
- const getMessageFeishuMock = vi.hoisted(() => vi.fn());
14
- const editMessageFeishuMock = vi.hoisted(() => vi.fn());
15
- const createPinFeishuMock = vi.hoisted(() => vi.fn());
16
- const listPinsFeishuMock = vi.hoisted(() => vi.fn());
17
- const removePinFeishuMock = vi.hoisted(() => vi.fn());
18
- const getChatInfoMock = vi.hoisted(() => vi.fn());
19
- const getChatMembersMock = vi.hoisted(() => vi.fn());
20
- const getFeishuMemberInfoMock = vi.hoisted(() => vi.fn());
21
- const listFeishuDirectoryPeersLiveMock = vi.hoisted(() => vi.fn());
22
- const listFeishuDirectoryGroupsLiveMock = vi.hoisted(() => vi.fn());
23
- const feishuOutboundSendMediaMock = vi.hoisted(() => vi.fn());
24
-
25
- vi.mock("./probe.js", () => ({
26
- probeFeishu: probeFeishuMock,
27
- }));
28
-
29
- vi.mock("./client.js", () => ({
30
- createFeishuClient: createFeishuClientMock,
31
- }));
32
-
33
- vi.mock("./channel.runtime.js", () => ({
34
- feishuChannelRuntime: {
35
- addReactionFeishu: addReactionFeishuMock,
36
- createPinFeishu: createPinFeishuMock,
37
- editMessageFeishu: editMessageFeishuMock,
38
- getChatInfo: getChatInfoMock,
39
- getChatMembers: getChatMembersMock,
40
- getFeishuMemberInfo: getFeishuMemberInfoMock,
41
- getMessageFeishu: getMessageFeishuMock,
42
- listFeishuDirectoryGroupsLive: listFeishuDirectoryGroupsLiveMock,
43
- listFeishuDirectoryPeersLive: listFeishuDirectoryPeersLiveMock,
44
- listPinsFeishu: listPinsFeishuMock,
45
- listReactionsFeishu: listReactionsFeishuMock,
46
- probeFeishu: probeFeishuMock,
47
- removePinFeishu: removePinFeishuMock,
48
- removeReactionFeishu: removeReactionFeishuMock,
49
- sendCardFeishu: sendCardFeishuMock,
50
- sendMessageFeishu: sendMessageFeishuMock,
51
- feishuOutbound: {
52
- sendText: vi.fn(),
53
- sendMedia: feishuOutboundSendMediaMock,
54
- },
55
- },
56
- }));
57
-
58
- function getDescribedActions(cfg: KlawConfig, accountId?: string): string[] {
59
- return [...(feishuPlugin.actions?.describeMessageTool?.({ cfg, accountId })?.actions ?? [])];
60
- }
61
-
62
- function requireRecord(value: unknown, label: string): Record<string, unknown> {
63
- if (!value || typeof value !== "object" || Array.isArray(value)) {
64
- throw new Error(`Expected ${label}`);
65
- }
66
- return value as Record<string, unknown>;
67
- }
68
-
69
- function requireArray(value: unknown, label: string): unknown[] {
70
- if (!Array.isArray(value)) {
71
- throw new Error(`Expected ${label}`);
72
- }
73
- return value;
74
- }
75
-
76
- function mockCallArg(mock: unknown, callIndex: number, argIndex: number, label: string) {
77
- const calls = (mock as { mock?: { calls?: unknown[][] } }).mock?.calls;
78
- if (!Array.isArray(calls)) {
79
- throw new Error(`Expected ${label} mock calls`);
80
- }
81
- const call = calls[callIndex];
82
- if (!call) {
83
- throw new Error(`Expected ${label} call ${callIndex + 1}`);
84
- }
85
- return call[argIndex];
86
- }
87
-
88
- function resultDetails(result: unknown) {
89
- return requireRecord(requireRecord(result, "action result").details, "action result details");
90
- }
91
-
92
- afterAll(() => {
93
- vi.doUnmock("./probe.js");
94
- vi.doUnmock("./client.js");
95
- vi.doUnmock("./channel.runtime.js");
96
- vi.resetModules();
97
- });
98
-
99
- describe("feishuPlugin.status.probeAccount", () => {
100
- it("uses current account credentials for multi-account config", async () => {
101
- const cfg = {
102
- channels: {
103
- feishu: {
104
- enabled: true,
105
- accounts: {
106
- main: {
107
- appId: "cli_main",
108
- appSecret: "secret_main",
109
- enabled: true,
110
- },
111
- },
112
- },
113
- },
114
- } as KlawConfig;
115
-
116
- const account = feishuPlugin.config.resolveAccount(cfg, "main");
117
- probeFeishuMock.mockResolvedValueOnce({ ok: true, appId: "cli_main" });
118
-
119
- const result = await feishuPlugin.status?.probeAccount?.({
120
- account,
121
- timeoutMs: 1_000,
122
- cfg,
123
- });
124
-
125
- expect(probeFeishuMock).toHaveBeenCalledTimes(1);
126
- const probeArgs = requireRecord(
127
- mockCallArg(probeFeishuMock, 0, 0, "probeFeishu"),
128
- "probe args",
129
- );
130
- expect(probeArgs.accountId).toBe("main");
131
- expect(probeArgs.appId).toBe("cli_main");
132
- expect(probeArgs.appSecret).toBe("secret_main");
133
- const resultRecord = requireRecord(result, "probe result");
134
- expect(resultRecord.ok).toBe(true);
135
- expect(resultRecord.appId).toBe("cli_main");
136
- });
137
- });
138
-
139
- describe("feishuPlugin.pairing.notifyApproval", () => {
140
- beforeEach(() => {
141
- sendMessageFeishuMock.mockReset();
142
- sendMessageFeishuMock.mockResolvedValue({ messageId: "pairing-msg", chatId: "ou_user" });
143
- });
144
-
145
- it("preserves accountId when sending pairing approvals", async () => {
146
- const cfg = {
147
- channels: {
148
- feishu: {
149
- accounts: {
150
- work: {
151
- appId: "cli_work",
152
- appSecret: "secret_work",
153
- enabled: true,
154
- },
155
- },
156
- },
157
- },
158
- } as KlawConfig;
159
-
160
- await feishuPlugin.pairing?.notifyApproval?.({
161
- cfg,
162
- id: "ou_user",
163
- accountId: "work",
164
- });
165
-
166
- const sendArgs = requireRecord(
167
- mockCallArg(sendMessageFeishuMock, 0, 0, "sendMessageFeishu"),
168
- "send args",
169
- );
170
- expect(sendArgs.cfg).toBe(cfg);
171
- expect(sendArgs.to).toBe("ou_user");
172
- expect(sendArgs.accountId).toBe("work");
173
- });
174
- });
175
-
176
- describe("feishuPlugin messaging", () => {
177
- it("owns sender/topic session inheritance candidates", () => {
178
- expect(
179
- feishuPlugin.messaging?.resolveSessionConversation?.({
180
- kind: "group",
181
- rawId: "oc_group_chat:topic:om_topic_root:sender:ou_topic_user",
182
- }),
183
- ).toEqual({
184
- id: "oc_group_chat:topic:om_topic_root:sender:ou_topic_user",
185
- baseConversationId: "oc_group_chat",
186
- parentConversationCandidates: ["oc_group_chat:topic:om_topic_root", "oc_group_chat"],
187
- });
188
- expect(
189
- feishuPlugin.messaging?.resolveSessionConversation?.({
190
- kind: "group",
191
- rawId: "oc_group_chat:topic:om_topic_root",
192
- }),
193
- ).toEqual({
194
- id: "oc_group_chat:topic:om_topic_root",
195
- baseConversationId: "oc_group_chat",
196
- parentConversationCandidates: ["oc_group_chat"],
197
- });
198
- expect(
199
- feishuPlugin.messaging?.resolveSessionConversation?.({
200
- kind: "group",
201
- rawId: "oc_group_chat:Topic:om_topic_root:Sender:ou_topic_user",
202
- }),
203
- ).toEqual({
204
- id: "oc_group_chat:topic:om_topic_root:sender:ou_topic_user",
205
- baseConversationId: "oc_group_chat",
206
- parentConversationCandidates: ["oc_group_chat:topic:om_topic_root", "oc_group_chat"],
207
- });
208
- });
209
- });
210
-
211
- describe("feishuPlugin actions", () => {
212
- const cfg = {
213
- channels: {
214
- feishu: {
215
- enabled: true,
216
- appId: "cli_main",
217
- appSecret: "secret_main",
218
- actions: {
219
- reactions: true,
220
- },
221
- },
222
- },
223
- } as KlawConfig;
224
-
225
- beforeEach(() => {
226
- vi.clearAllMocks();
227
- createFeishuClientMock.mockReturnValue({ tag: "client" });
228
- });
229
-
230
- it("advertises the expanded Feishu action surface", () => {
231
- expect(getDescribedActions(cfg)).toEqual([
232
- "send",
233
- "read",
234
- "edit",
235
- "thread-reply",
236
- "pin",
237
- "list-pins",
238
- "unpin",
239
- "member-info",
240
- "channel-info",
241
- "channel-list",
242
- "react",
243
- "reactions",
244
- ]);
245
- });
246
-
247
- it("does not advertise reactions when disabled via actions config", () => {
248
- const disabledCfg = {
249
- channels: {
250
- feishu: {
251
- enabled: true,
252
- appId: "cli_main",
253
- appSecret: "secret_main",
254
- actions: {
255
- reactions: false,
256
- },
257
- },
258
- },
259
- } as KlawConfig;
260
-
261
- expect(getDescribedActions(disabledCfg)).toEqual([
262
- "send",
263
- "read",
264
- "edit",
265
- "thread-reply",
266
- "pin",
267
- "list-pins",
268
- "unpin",
269
- "member-info",
270
- "channel-info",
271
- "channel-list",
272
- ]);
273
- });
274
-
275
- it("honors the selected Feishu account during discovery", () => {
276
- const cfg = {
277
- channels: {
278
- feishu: {
279
- enabled: true,
280
- actions: { reactions: false },
281
- accounts: {
282
- default: {
283
- enabled: true,
284
- appId: "cli_main",
285
- appSecret: "secret_main",
286
- actions: { reactions: false },
287
- },
288
- work: {
289
- enabled: true,
290
- appId: "cli_work",
291
- appSecret: "secret_work",
292
- actions: { reactions: true },
293
- },
294
- },
295
- },
296
- },
297
- } as KlawConfig;
298
-
299
- expect(getDescribedActions(cfg, "default")).toEqual([
300
- "send",
301
- "read",
302
- "edit",
303
- "thread-reply",
304
- "pin",
305
- "list-pins",
306
- "unpin",
307
- "member-info",
308
- "channel-info",
309
- "channel-list",
310
- ]);
311
- expect(getDescribedActions(cfg, "work")).toEqual([
312
- "send",
313
- "read",
314
- "edit",
315
- "thread-reply",
316
- "pin",
317
- "list-pins",
318
- "unpin",
319
- "member-info",
320
- "channel-info",
321
- "channel-list",
322
- "react",
323
- "reactions",
324
- ]);
325
- });
326
-
327
- it("sends text messages", async () => {
328
- sendMessageFeishuMock.mockResolvedValueOnce({ messageId: "om_sent", chatId: "oc_group_1" });
329
-
330
- const result = await feishuPlugin.actions?.handleAction?.({
331
- action: "send",
332
- params: { to: "chat:oc_group_1", message: "hello" },
333
- cfg,
334
- accountId: undefined,
335
- toolContext: {},
336
- } as never);
337
-
338
- expect(sendMessageFeishuMock).toHaveBeenCalledWith({
339
- cfg,
340
- to: "chat:oc_group_1",
341
- text: "hello",
342
- accountId: undefined,
343
- replyToMessageId: undefined,
344
- replyInThread: false,
345
- });
346
- const details = resultDetails(result);
347
- expect(details.ok).toBe(true);
348
- expect(details.messageId).toBe("om_sent");
349
- expect(details.chatId).toBe("oc_group_1");
350
- });
351
-
352
- it("renders presentation messages as cards", async () => {
353
- sendCardFeishuMock.mockResolvedValueOnce({ messageId: "om_card", chatId: "oc_group_1" });
354
-
355
- const result = await feishuPlugin.actions?.handleAction?.({
356
- action: "send",
357
- params: {
358
- to: "chat:oc_group_1",
359
- presentation: {
360
- title: "Status",
361
- blocks: [{ type: "text", text: "Build completed" }],
362
- },
363
- },
364
- cfg,
365
- accountId: undefined,
366
- toolContext: {},
367
- } as never);
368
-
369
- const sendCardArgs = requireRecord(
370
- mockCallArg(sendCardFeishuMock, 0, 0, "sendCardFeishu"),
371
- "send card args",
372
- );
373
- expect(sendCardArgs.cfg).toBe(cfg);
374
- expect(sendCardArgs.to).toBe("chat:oc_group_1");
375
- expect(sendCardArgs.accountId).toBeUndefined();
376
- expect(sendCardArgs.replyToMessageId).toBeUndefined();
377
- expect(sendCardArgs.replyInThread).toBe(false);
378
- const card = requireRecord(sendCardArgs.card, "card");
379
- expect(card.schema).toBe("2.0");
380
- expect(card.header).toEqual({
381
- title: { tag: "plain_text", content: "Status" },
382
- template: "blue",
383
- });
384
- expect(card.body).toEqual({
385
- elements: [
386
- {
387
- tag: "markdown",
388
- content: "Build completed",
389
- },
390
- ],
391
- });
392
- const details = resultDetails(result);
393
- expect(details.ok).toBe(true);
394
- expect(details.messageId).toBe("om_card");
395
- expect(details.chatId).toBe("oc_group_1");
396
- });
397
-
398
- it("renders presentation button labels into the card fallback", async () => {
399
- sendCardFeishuMock.mockResolvedValueOnce({ messageId: "om_card", chatId: "oc_group_1" });
400
-
401
- await feishuPlugin.actions?.handleAction?.({
402
- action: "send",
403
- params: {
404
- to: "chat:oc_group_1",
405
- presentation: {
406
- blocks: [
407
- {
408
- type: "buttons",
409
- buttons: [{ label: "Run help", value: "feishu.quick_actions.help" }],
410
- },
411
- ],
412
- },
413
- },
414
- cfg,
415
- accountId: undefined,
416
- toolContext: {},
417
- } as never);
418
-
419
- const sendCardArgs = requireRecord(
420
- mockCallArg(sendCardFeishuMock, 0, 0, "sendCardFeishu"),
421
- "send card args",
422
- );
423
- const card = requireRecord(sendCardArgs.card, "card");
424
- expect(requireRecord(card.body, "card body").elements).toEqual([
425
- {
426
- tag: "markdown",
427
- content: "- Run help",
428
- },
429
- ]);
430
- });
431
-
432
- it("renders presentation select labels into the card fallback", async () => {
433
- sendCardFeishuMock.mockResolvedValueOnce({ messageId: "om_card", chatId: "oc_group_1" });
434
-
435
- await feishuPlugin.actions?.handleAction?.({
436
- action: "send",
437
- params: {
438
- to: "chat:oc_group_1",
439
- presentation: {
440
- blocks: [
441
- {
442
- type: "select",
443
- placeholder: "Pick one",
444
- options: [{ label: "Option A", value: "a" }],
445
- },
446
- ],
447
- },
448
- },
449
- cfg,
450
- accountId: undefined,
451
- toolContext: {},
452
- } as never);
453
-
454
- const sendCardArgs = requireRecord(
455
- mockCallArg(sendCardFeishuMock, 0, 0, "sendCardFeishu"),
456
- "send card args",
457
- );
458
- const card = requireRecord(sendCardArgs.card, "card");
459
- expect(requireRecord(card.body, "card body").elements).toEqual([
460
- {
461
- tag: "markdown",
462
- content: "Pick one:\n- Option A",
463
- },
464
- ]);
465
- });
466
-
467
- it("sends media through the outbound adapter", async () => {
468
- feishuOutboundSendMediaMock.mockResolvedValueOnce({
469
- channel: "feishu",
470
- messageId: "om_media",
471
- details: { messageId: "om_media", chatId: "oc_group_1" },
472
- });
473
-
474
- const result = await feishuPlugin.actions?.handleAction?.({
475
- action: "send",
476
- params: {
477
- to: "chat:oc_group_1",
478
- message: "test",
479
- media: "/tmp/image.png",
480
- },
481
- cfg,
482
- accountId: undefined,
483
- toolContext: {},
484
- mediaLocalRoots: ["/tmp"],
485
- } as never);
486
-
487
- expect(feishuOutboundSendMediaMock).toHaveBeenCalledWith({
488
- cfg,
489
- to: "chat:oc_group_1",
490
- text: "test",
491
- mediaUrl: "/tmp/image.png",
492
- accountId: undefined,
493
- mediaLocalRoots: ["/tmp"],
494
- replyToId: undefined,
495
- });
496
- expect(resultDetails(result).messageId).toBe("om_media");
497
- });
498
-
499
- it("passes asVoice through media sends", async () => {
500
- feishuOutboundSendMediaMock.mockResolvedValueOnce({
501
- channel: "feishu",
502
- messageId: "om_voice",
503
- details: { messageId: "om_voice", chatId: "oc_group_1" },
504
- });
505
-
506
- await feishuPlugin.actions?.handleAction?.({
507
- action: "send",
508
- params: {
509
- to: "chat:oc_group_1",
510
- media: "https://example.com/reply.mp3",
511
- asVoice: true,
512
- },
513
- cfg,
514
- accountId: undefined,
515
- toolContext: {},
516
- mediaLocalRoots: [],
517
- } as never);
518
-
519
- const mediaArgs = requireRecord(
520
- mockCallArg(feishuOutboundSendMediaMock, 0, 0, "feishuOutbound.sendMedia"),
521
- "media args",
522
- );
523
- expect(mediaArgs.mediaUrl).toBe("https://example.com/reply.mp3");
524
- expect(mediaArgs.audioAsVoice).toBe(true);
525
- });
526
-
527
- it("reads messages", async () => {
528
- getMessageFeishuMock.mockResolvedValueOnce({
529
- messageId: "om_1",
530
- content: "hello",
531
- contentType: "text",
532
- });
533
-
534
- const result = await feishuPlugin.actions?.handleAction?.({
535
- action: "read",
536
- params: { messageId: "om_1" },
537
- cfg,
538
- accountId: undefined,
539
- } as never);
540
-
541
- expect(getMessageFeishuMock).toHaveBeenCalledWith({
542
- cfg,
543
- messageId: "om_1",
544
- accountId: undefined,
545
- });
546
- const details = resultDetails(result);
547
- expect(details.ok).toBe(true);
548
- const message = requireRecord(details.message, "read message");
549
- expect(message.messageId).toBe("om_1");
550
- expect(message.content).toBe("hello");
551
- });
552
-
553
- it("returns an error result when message reads fail", async () => {
554
- getMessageFeishuMock.mockResolvedValueOnce(null);
555
-
556
- const result = await feishuPlugin.actions?.handleAction?.({
557
- action: "read",
558
- params: { messageId: "om_missing" },
559
- cfg,
560
- accountId: undefined,
561
- } as never);
562
-
563
- expect((result as { isError?: boolean } | undefined)?.isError).toBe(true);
564
- expect(result?.details).toEqual({
565
- error: "Feishu read failed or message not found: om_missing",
566
- });
567
- });
568
-
569
- it("edits messages", async () => {
570
- editMessageFeishuMock.mockResolvedValueOnce({ messageId: "om_2", contentType: "post" });
571
-
572
- const result = await feishuPlugin.actions?.handleAction?.({
573
- action: "edit",
574
- params: { messageId: "om_2", text: "updated" },
575
- cfg,
576
- accountId: undefined,
577
- } as never);
578
-
579
- expect(editMessageFeishuMock).toHaveBeenCalledWith({
580
- cfg,
581
- messageId: "om_2",
582
- text: "updated",
583
- card: undefined,
584
- accountId: undefined,
585
- });
586
- const details = resultDetails(result);
587
- expect(details.ok).toBe(true);
588
- expect(details.messageId).toBe("om_2");
589
- expect(details.contentType).toBe("post");
590
- });
591
-
592
- it("sends explicit thread replies with reply_in_thread semantics", async () => {
593
- sendMessageFeishuMock.mockResolvedValueOnce({ messageId: "om_reply", chatId: "oc_group_1" });
594
-
595
- const result = await feishuPlugin.actions?.handleAction?.({
596
- action: "thread-reply",
597
- params: { to: "chat:oc_group_1", messageId: "om_parent", text: "reply body" },
598
- cfg,
599
- accountId: undefined,
600
- toolContext: {},
601
- } as never);
602
-
603
- expect(sendMessageFeishuMock).toHaveBeenCalledWith({
604
- cfg,
605
- to: "chat:oc_group_1",
606
- text: "reply body",
607
- accountId: undefined,
608
- replyToMessageId: "om_parent",
609
- replyInThread: true,
610
- });
611
- const details = resultDetails(result);
612
- expect(details.ok).toBe(true);
613
- expect(details.action).toBe("thread-reply");
614
- expect(details.messageId).toBe("om_reply");
615
- });
616
-
617
- it("auto-threads `send` text against the inbound trigger in group_topic sessions", async () => {
618
- sendMessageFeishuMock.mockResolvedValueOnce({ messageId: "om_topic", chatId: "oc_group_1" });
619
-
620
- await feishuPlugin.actions?.handleAction?.({
621
- action: "send",
622
- params: { to: "chat:oc_group_1", text: "topic reply" },
623
- cfg,
624
- accountId: undefined,
625
- sessionKey: "feishu:group:oc_group_1:topic:om_inbound",
626
- toolContext: { currentMessageId: "om_inbound" },
627
- } as never);
628
-
629
- expect(sendMessageFeishuMock).toHaveBeenCalledWith({
630
- cfg,
631
- to: "chat:oc_group_1",
632
- text: "topic reply",
633
- accountId: undefined,
634
- replyToMessageId: "om_inbound",
635
- replyInThread: true,
636
- });
637
- });
638
-
639
- it("auto-threads `send` cards against the inbound trigger in group_topic sessions", async () => {
640
- sendCardFeishuMock.mockResolvedValueOnce({ messageId: "om_topic_card", chatId: "oc_group_1" });
641
-
642
- await feishuPlugin.actions?.handleAction?.({
643
- action: "send",
644
- params: {
645
- to: "chat:oc_group_1",
646
- presentation: {
647
- title: "Topic update",
648
- blocks: [{ type: "text", text: "topic reply" }],
649
- },
650
- },
651
- cfg,
652
- accountId: undefined,
653
- sessionKey: "feishu:group:oc_group_1:topic:om_inbound",
654
- toolContext: { currentMessageId: "om_inbound" },
655
- } as never);
656
-
657
- const sendCardArgs = requireRecord(
658
- mockCallArg(sendCardFeishuMock, 0, 0, "sendCardFeishu"),
659
- "send card args",
660
- );
661
- expect(sendCardArgs.replyToMessageId).toBe("om_inbound");
662
- expect(sendCardArgs.replyInThread).toBe(true);
663
- });
664
-
665
- it("auto-threads `send` media against the inbound trigger in group_topic sessions", async () => {
666
- feishuOutboundSendMediaMock.mockResolvedValueOnce({
667
- channel: "feishu",
668
- messageId: "om_topic_media",
669
- details: { messageId: "om_topic_media", chatId: "oc_group_1" },
670
- });
671
-
672
- await feishuPlugin.actions?.handleAction?.({
673
- action: "send",
674
- params: {
675
- to: "chat:oc_group_1",
676
- message: "topic reply",
677
- media: "/tmp/image.png",
678
- },
679
- cfg,
680
- accountId: undefined,
681
- sessionKey: "feishu:group:oc_group_1:topic:om_inbound",
682
- toolContext: { currentMessageId: "om_inbound" },
683
- mediaLocalRoots: ["/tmp"],
684
- } as never);
685
-
686
- const mediaArgs = requireRecord(
687
- mockCallArg(feishuOutboundSendMediaMock, 0, 0, "feishuOutbound.sendMedia"),
688
- "media args",
689
- );
690
- expect(mediaArgs.threadId).toBe("om_inbound");
691
- expect("replyToId" in mediaArgs).toBe(false);
692
- });
693
-
694
- it("auto-threads `send` in group_topic_sender sessions too", async () => {
695
- sendMessageFeishuMock.mockResolvedValueOnce({ messageId: "om_topic", chatId: "oc_group_1" });
696
-
697
- await feishuPlugin.actions?.handleAction?.({
698
- action: "send",
699
- params: { to: "chat:oc_group_1", text: "topic reply" },
700
- cfg,
701
- accountId: undefined,
702
- sessionKey: "feishu:group:oc_group_1:topic:om_inbound:sender:ou_user",
703
- toolContext: { currentMessageId: "om_inbound" },
704
- } as never);
705
-
706
- const sendArgs = requireRecord(
707
- mockCallArg(sendMessageFeishuMock, 0, 0, "sendMessageFeishu"),
708
- "send args",
709
- );
710
- expect(sendArgs.replyToMessageId).toBe("om_inbound");
711
- expect(sendArgs.replyInThread).toBe(true);
712
- });
713
-
714
- it("does not auto-thread `send` in plain group sessions (no topic)", async () => {
715
- sendMessageFeishuMock.mockResolvedValueOnce({ messageId: "om_plain", chatId: "oc_group_1" });
716
-
717
- await feishuPlugin.actions?.handleAction?.({
718
- action: "send",
719
- params: { to: "chat:oc_group_1", text: "plain group reply" },
720
- cfg,
721
- accountId: undefined,
722
- sessionKey: "feishu:group:oc_group_1",
723
- toolContext: { currentMessageId: "om_inbound" },
724
- } as never);
725
-
726
- expect(sendMessageFeishuMock).toHaveBeenCalledWith({
727
- cfg,
728
- to: "chat:oc_group_1",
729
- text: "plain group reply",
730
- accountId: undefined,
731
- replyToMessageId: undefined,
732
- replyInThread: false,
733
- });
734
- });
735
-
736
- it("does not auto-thread `send` in group_topic when no inbound currentMessageId is available", async () => {
737
- sendMessageFeishuMock.mockResolvedValueOnce({ messageId: "om_topic", chatId: "oc_group_1" });
738
-
739
- await feishuPlugin.actions?.handleAction?.({
740
- action: "send",
741
- params: { to: "chat:oc_group_1", text: "topic reply" },
742
- cfg,
743
- accountId: undefined,
744
- sessionKey: "feishu:group:oc_group_1:topic:om_inbound",
745
- toolContext: {},
746
- } as never);
747
-
748
- expect(sendMessageFeishuMock).toHaveBeenCalledWith({
749
- cfg,
750
- to: "chat:oc_group_1",
751
- text: "topic reply",
752
- accountId: undefined,
753
- replyToMessageId: undefined,
754
- replyInThread: false,
755
- });
756
- });
757
-
758
- it("creates pins", async () => {
759
- createPinFeishuMock.mockResolvedValueOnce({ messageId: "om_pin", chatId: "oc_group_1" });
760
-
761
- const result = await feishuPlugin.actions?.handleAction?.({
762
- action: "pin",
763
- params: { messageId: "om_pin" },
764
- cfg,
765
- accountId: undefined,
766
- } as never);
767
-
768
- expect(createPinFeishuMock).toHaveBeenCalledWith({
769
- cfg,
770
- messageId: "om_pin",
771
- accountId: undefined,
772
- });
773
- const details = resultDetails(result);
774
- expect(details.ok).toBe(true);
775
- expect(requireRecord(details.pin, "pin").messageId).toBe("om_pin");
776
- });
777
-
778
- it("lists pins", async () => {
779
- listPinsFeishuMock.mockResolvedValueOnce({
780
- chatId: "oc_group_1",
781
- pins: [{ messageId: "om_pin" }],
782
- hasMore: false,
783
- pageToken: undefined,
784
- });
785
-
786
- const result = await feishuPlugin.actions?.handleAction?.({
787
- action: "list-pins",
788
- params: { chatId: "oc_group_1" },
789
- cfg,
790
- accountId: undefined,
791
- toolContext: {},
792
- } as never);
793
-
794
- expect(listPinsFeishuMock).toHaveBeenCalledWith({
795
- cfg,
796
- chatId: "oc_group_1",
797
- startTime: undefined,
798
- endTime: undefined,
799
- pageSize: undefined,
800
- pageToken: undefined,
801
- accountId: undefined,
802
- });
803
- const details = resultDetails(result);
804
- expect(details.ok).toBe(true);
805
- const pins = requireArray(details.pins, "pins");
806
- expect(requireRecord(pins[0], "pin").messageId).toBe("om_pin");
807
- });
808
-
809
- it("removes pins", async () => {
810
- const result = await feishuPlugin.actions?.handleAction?.({
811
- action: "unpin",
812
- params: { messageId: "om_pin" },
813
- cfg,
814
- accountId: undefined,
815
- } as never);
816
-
817
- expect(removePinFeishuMock).toHaveBeenCalledWith({
818
- cfg,
819
- messageId: "om_pin",
820
- accountId: undefined,
821
- });
822
- const details = resultDetails(result);
823
- expect(details.ok).toBe(true);
824
- expect(details.messageId).toBe("om_pin");
825
- });
826
-
827
- it("fetches channel info", async () => {
828
- getChatInfoMock.mockResolvedValueOnce({ chat_id: "oc_group_1", name: "Eng" });
829
-
830
- const result = await feishuPlugin.actions?.handleAction?.({
831
- action: "channel-info",
832
- params: { chatId: "oc_group_1" },
833
- cfg,
834
- accountId: undefined,
835
- toolContext: {},
836
- } as never);
837
-
838
- expect(createFeishuClientMock).toHaveBeenCalled();
839
- expect(getChatInfoMock).toHaveBeenCalledWith({ tag: "client" }, "oc_group_1");
840
- const details = resultDetails(result);
841
- expect(details.ok).toBe(true);
842
- const channel = requireRecord(details.channel, "channel");
843
- expect(channel.chat_id).toBe("oc_group_1");
844
- expect(channel.name).toBe("Eng");
845
- });
846
-
847
- it("fetches member lists from a chat", async () => {
848
- getChatMembersMock.mockResolvedValueOnce({
849
- chat_id: "oc_group_1",
850
- members: [{ member_id: "ou_1", name: "Alice" }],
851
- has_more: false,
852
- });
853
-
854
- const result = await feishuPlugin.actions?.handleAction?.({
855
- action: "member-info",
856
- params: { chatId: "oc_group_1" },
857
- cfg,
858
- accountId: undefined,
859
- toolContext: {},
860
- } as never);
861
-
862
- expect(getChatMembersMock).toHaveBeenCalledWith(
863
- { tag: "client" },
864
- "oc_group_1",
865
- undefined,
866
- undefined,
867
- "open_id",
868
- );
869
- const details = resultDetails(result);
870
- expect(details.ok).toBe(true);
871
- const members = requireArray(details.members, "members");
872
- const member = requireRecord(members[0], "member");
873
- expect(member.member_id).toBe("ou_1");
874
- expect(member.name).toBe("Alice");
875
- });
876
-
877
- it("fetches individual member info", async () => {
878
- getFeishuMemberInfoMock.mockResolvedValueOnce({ member_id: "ou_1", name: "Alice" });
879
-
880
- const result = await feishuPlugin.actions?.handleAction?.({
881
- action: "member-info",
882
- params: { memberId: "ou_1" },
883
- cfg,
884
- accountId: undefined,
885
- toolContext: {},
886
- } as never);
887
-
888
- expect(getFeishuMemberInfoMock).toHaveBeenCalledWith({ tag: "client" }, "ou_1", "open_id");
889
- const details = resultDetails(result);
890
- expect(details.ok).toBe(true);
891
- const member = requireRecord(details.member, "member");
892
- expect(member.member_id).toBe("ou_1");
893
- expect(member.name).toBe("Alice");
894
- });
895
-
896
- it("infers user_id lookups from the userId alias", async () => {
897
- getFeishuMemberInfoMock.mockResolvedValueOnce({ member_id: "u_1", name: "Alice" });
898
-
899
- await feishuPlugin.actions?.handleAction?.({
900
- action: "member-info",
901
- params: { userId: "u_1" },
902
- cfg,
903
- accountId: undefined,
904
- toolContext: {},
905
- } as never);
906
-
907
- expect(getFeishuMemberInfoMock).toHaveBeenCalledWith({ tag: "client" }, "u_1", "user_id");
908
- });
909
-
910
- it("honors explicit open_id over alias heuristics", async () => {
911
- getFeishuMemberInfoMock.mockResolvedValueOnce({ member_id: "u_1", name: "Alice" });
912
-
913
- await feishuPlugin.actions?.handleAction?.({
914
- action: "member-info",
915
- params: { userId: "u_1", memberIdType: "open_id" },
916
- cfg,
917
- accountId: undefined,
918
- toolContext: {},
919
- } as never);
920
-
921
- expect(getFeishuMemberInfoMock).toHaveBeenCalledWith({ tag: "client" }, "u_1", "open_id");
922
- });
923
-
924
- it("lists directory-backed peers and groups", async () => {
925
- listFeishuDirectoryGroupsLiveMock.mockResolvedValueOnce([{ kind: "group", id: "oc_group_1" }]);
926
- listFeishuDirectoryPeersLiveMock.mockResolvedValueOnce([{ kind: "user", id: "ou_1" }]);
927
-
928
- const result = await feishuPlugin.actions?.handleAction?.({
929
- action: "channel-list",
930
- params: { query: "eng", limit: 5 },
931
- cfg,
932
- accountId: undefined,
933
- } as never);
934
-
935
- expect(listFeishuDirectoryGroupsLiveMock).toHaveBeenCalledWith({
936
- cfg,
937
- query: "eng",
938
- limit: 5,
939
- fallbackToStatic: false,
940
- accountId: undefined,
941
- });
942
- expect(listFeishuDirectoryPeersLiveMock).toHaveBeenCalledWith({
943
- cfg,
944
- query: "eng",
945
- limit: 5,
946
- fallbackToStatic: false,
947
- accountId: undefined,
948
- });
949
- const details = resultDetails(result);
950
- expect(details.ok).toBe(true);
951
- const groups = requireArray(details.groups, "groups");
952
- const peers = requireArray(details.peers, "peers");
953
- expect(requireRecord(groups[0], "group").id).toBe("oc_group_1");
954
- expect(requireRecord(peers[0], "peer").id).toBe("ou_1");
955
- });
956
-
957
- it("fails channel-list when live discovery fails", async () => {
958
- listFeishuDirectoryGroupsLiveMock.mockRejectedValueOnce(new Error("token expired"));
959
-
960
- await expect(
961
- feishuPlugin.actions?.handleAction?.({
962
- action: "channel-list",
963
- params: { query: "eng", limit: 5, scope: "groups" },
964
- cfg,
965
- accountId: undefined,
966
- } as never),
967
- ).rejects.toThrow("token expired");
968
- });
969
-
970
- it("requires clearAll=true before removing all bot reactions", async () => {
971
- await expect(
972
- feishuPlugin.actions?.handleAction?.({
973
- action: "react",
974
- params: { messageId: "om_msg1" },
975
- cfg,
976
- accountId: undefined,
977
- } as never),
978
- ).rejects.toThrow(
979
- "Emoji is required to add a Feishu reaction. Set clearAll=true to remove all bot reactions.",
980
- );
981
- });
982
-
983
- it("allows explicit clearAll=true when removing all bot reactions", async () => {
984
- listReactionsFeishuMock.mockResolvedValueOnce([
985
- { reactionId: "r1", operatorType: "app" },
986
- { reactionId: "r2", operatorType: "app" },
987
- ]);
988
-
989
- const result = await feishuPlugin.actions?.handleAction?.({
990
- action: "react",
991
- params: { messageId: "om_msg1", clearAll: true },
992
- cfg,
993
- accountId: undefined,
994
- } as never);
995
-
996
- expect(listReactionsFeishuMock).toHaveBeenCalledWith({
997
- cfg,
998
- messageId: "om_msg1",
999
- accountId: undefined,
1000
- });
1001
- expect(removeReactionFeishuMock).toHaveBeenCalledTimes(2);
1002
- const details = resultDetails(result);
1003
- expect(details.ok).toBe(true);
1004
- expect(details.removed).toBe(2);
1005
- });
1006
-
1007
- it("fails for missing params on supported actions", async () => {
1008
- await expect(
1009
- feishuPlugin.actions?.handleAction?.({
1010
- action: "thread-reply",
1011
- params: { to: "chat:oc_group_1", message: "reply body" },
1012
- cfg,
1013
- accountId: undefined,
1014
- } as never),
1015
- ).rejects.toThrow("Feishu thread-reply requires messageId.");
1016
- });
1017
-
1018
- it("sends media-only messages without requiring card", async () => {
1019
- feishuOutboundSendMediaMock.mockResolvedValueOnce({
1020
- channel: "feishu",
1021
- messageId: "om_media_only",
1022
- details: { messageId: "om_media_only", chatId: "oc_group_1" },
1023
- });
1024
-
1025
- const result = await feishuPlugin.actions?.handleAction?.({
1026
- action: "send",
1027
- params: {
1028
- to: "chat:oc_group_1",
1029
- media: "https://example.com/image.png",
1030
- },
1031
- cfg,
1032
- accountId: undefined,
1033
- toolContext: {},
1034
- mediaLocalRoots: [],
1035
- } as never);
1036
-
1037
- const mediaArgs = requireRecord(
1038
- mockCallArg(feishuOutboundSendMediaMock, 0, 0, "feishuOutbound.sendMedia"),
1039
- "media args",
1040
- );
1041
- expect(mediaArgs.to).toBe("chat:oc_group_1");
1042
- expect(mediaArgs.mediaUrl).toBe("https://example.com/image.png");
1043
- expect(resultDetails(result).messageId).toBe("om_media_only");
1044
- });
1045
-
1046
- it("fails for unsupported action names", async () => {
1047
- await expect(
1048
- feishuPlugin.actions?.handleAction?.({
1049
- action: "search",
1050
- params: {},
1051
- cfg,
1052
- accountId: undefined,
1053
- } as never),
1054
- ).rejects.toThrow('Unsupported Feishu action: "search"');
1055
- });
1056
- });
1057
-
1058
- describe("resolveReceiveIdType", () => {
1059
- it("resolves chat IDs by oc_ prefix", () => {
1060
- expect(resolveReceiveIdType("oc_123")).toBe("chat_id");
1061
- });
1062
-
1063
- it("resolves open IDs by ou_ prefix", () => {
1064
- expect(resolveReceiveIdType("ou_123")).toBe("open_id");
1065
- });
1066
-
1067
- it("defaults unprefixed IDs to user_id", () => {
1068
- expect(resolveReceiveIdType("u_123")).toBe("user_id");
1069
- });
1070
-
1071
- it("treats explicit group targets as chat_id", () => {
1072
- expect(resolveReceiveIdType("group:oc_123")).toBe("chat_id");
1073
- });
1074
-
1075
- it("treats explicit channel targets as chat_id", () => {
1076
- expect(resolveReceiveIdType("channel:oc_123")).toBe("chat_id");
1077
- });
1078
-
1079
- it("treats dm-prefixed open IDs as open_id", () => {
1080
- expect(resolveReceiveIdType("dm:ou_123")).toBe("open_id");
1081
- });
1082
- });
1083
-
1084
- describe("normalizeFeishuTarget", () => {
1085
- it("strips provider and user prefixes", () => {
1086
- expect(normalizeFeishuTarget("feishu:user:ou_123")).toBe("ou_123");
1087
- expect(normalizeFeishuTarget("lark:user:ou_123")).toBe("ou_123");
1088
- });
1089
-
1090
- it("strips provider and chat prefixes", () => {
1091
- expect(normalizeFeishuTarget("feishu:chat:oc_123")).toBe("oc_123");
1092
- });
1093
-
1094
- it("normalizes group/channel prefixes to chat ids", () => {
1095
- expect(normalizeFeishuTarget("group:oc_123")).toBe("oc_123");
1096
- expect(normalizeFeishuTarget("feishu:group:oc_123")).toBe("oc_123");
1097
- expect(normalizeFeishuTarget("channel:oc_456")).toBe("oc_456");
1098
- expect(normalizeFeishuTarget("lark:channel:oc_456")).toBe("oc_456");
1099
- });
1100
-
1101
- it("accepts provider-prefixed raw ids", () => {
1102
- expect(normalizeFeishuTarget("feishu:ou_123")).toBe("ou_123");
1103
- });
1104
-
1105
- it("strips provider and dm prefixes", () => {
1106
- expect(normalizeFeishuTarget("lark:dm:ou_123")).toBe("ou_123");
1107
- });
1108
- });
1109
-
1110
- describe("feishuPlugin.messaging.resolveDeliveryTarget", () => {
1111
- it("routes direct conversations to user targets", () => {
1112
- expect(
1113
- feishuPlugin.messaging?.resolveDeliveryTarget?.({
1114
- conversationId: "ou_123",
1115
- }),
1116
- ).toEqual({ to: "user:ou_123" });
1117
- });
1118
-
1119
- it("routes group conversations to chat targets", () => {
1120
- expect(
1121
- feishuPlugin.messaging?.resolveDeliveryTarget?.({
1122
- conversationId: "oc_123",
1123
- }),
1124
- ).toEqual({ to: "chat:oc_123" });
1125
- });
1126
-
1127
- it("routes topic conversations to parent chat plus thread id", () => {
1128
- expect(
1129
- feishuPlugin.messaging?.resolveDeliveryTarget?.({
1130
- conversationId: "oc_123:topic:omt_456",
1131
- parentConversationId: "oc_123",
1132
- }),
1133
- ).toEqual({ to: "chat:oc_123", threadId: "omt_456" });
1134
- });
1135
- });
1136
-
1137
- describe("looksLikeFeishuId", () => {
1138
- it("accepts provider-prefixed user targets", () => {
1139
- expect(looksLikeFeishuId("feishu:user:ou_123")).toBe(true);
1140
- });
1141
-
1142
- it("accepts provider-prefixed chat targets", () => {
1143
- expect(looksLikeFeishuId("lark:chat:oc_123")).toBe(true);
1144
- });
1145
-
1146
- it("accepts group/channel targets", () => {
1147
- expect(looksLikeFeishuId("feishu:group:oc_123")).toBe(true);
1148
- expect(looksLikeFeishuId("group:oc_123")).toBe(true);
1149
- expect(looksLikeFeishuId("channel:oc_456")).toBe(true);
1150
- });
1151
- });