






















@@ -166,6 +166,32 @@ function resetOutboundMocks() {
166166cleanupAmbientCommentTypingReactionMock.mockResolvedValue(false);
167167}
168168169+function sendMessageCall(index = 0): Record<string, any> | undefined {
170+const calls = sendMessageFeishuMock.mock.calls as unknown as Array<[Record<string, any>]>;
171+return calls[index]?.[0];
172+}
173+174+function sendMediaCall(index = 0): Record<string, any> | undefined {
175+const calls = sendMediaFeishuMock.mock.calls as unknown as Array<[Record<string, any>]>;
176+return calls[index]?.[0];
177+}
178+179+function sendCardCall(index = 0): Record<string, any> | undefined {
180+const calls = sendCardFeishuMock.mock.calls as unknown as Array<[Record<string, any>]>;
181+return calls[index]?.[0];
182+}
183+184+function sendStructuredCardCall(index = 0): Record<string, any> | undefined {
185+const calls = sendStructuredCardFeishuMock.mock.calls as unknown as Array<[Record<string, any>]>;
186+return calls[index]?.[0];
187+}
188+189+function expectFeishuResult(result: unknown, messageId: string) {
190+const typedResult = result as { channel?: string; messageId?: string } | undefined;
191+expect(typedResult?.channel).toBe("feishu");
192+expect(typedResult?.messageId).toBe(messageId);
193+}
194+169195describe("feishuOutbound.sendText local-image auto-convert", () => {
170196beforeEach(() => {
171197resetOutboundMocks();
@@ -194,52 +220,43 @@ describe("feishuOutbound.sendText local-image auto-convert", () => {
194220const adapterSendText = requireFeishuTextSender(adapter);
195221const adapterSendMedia = requireFeishuMediaSender(adapter);
196222197-await expect(
198-verifyChannelMessageAdapterCapabilityProofs({
199-adapterName: "feishu",
200- adapter,
201-proofs: {
202-text: async () => {
203-const result = await adapterSendText({
204-cfg: emptyConfig,
205-to: "chat:chat-1",
206-text: "hello",
207-accountId: "default",
208-});
209-expect(sendMessageFeishuMock).toHaveBeenCalledWith(
210-expect.objectContaining({
211-to: "chat:chat-1",
212-text: "hello",
213-accountId: "default",
214-}),
215-);
216-expect(result.receipt.platformMessageIds).toEqual(["feishu-text-1"]);
217-},
218-media: async () => {
219-const result = await adapterSendMedia({
220-cfg: emptyConfig,
221-to: "chat:chat-1",
222-text: "",
223-mediaUrl: "https://example.com/image.png",
224-accountId: "default",
225-});
226-expect(sendMediaFeishuMock).toHaveBeenCalledWith(
227-expect.objectContaining({
228-to: "chat:chat-1",
229-mediaUrl: "https://example.com/image.png",
230-accountId: "default",
231-}),
232-);
233-expect(result.receipt.platformMessageIds).toEqual(["feishu-media-1"]);
234-},
223+const proofs = await verifyChannelMessageAdapterCapabilityProofs({
224+adapterName: "feishu",
225+ adapter,
226+proofs: {
227+text: async () => {
228+const result = await adapterSendText({
229+cfg: emptyConfig,
230+to: "chat:chat-1",
231+text: "hello",
232+accountId: "default",
233+});
234+expect(sendMessageCall()?.to).toBe("chat:chat-1");
235+expect(sendMessageCall()?.text).toBe("hello");
236+expect(sendMessageCall()?.accountId).toBe("default");
237+expect(result.receipt.platformMessageIds).toEqual(["feishu-text-1"]);
235238},
236-}),
237-).resolves.toEqual(
238-expect.arrayContaining([
239-{ capability: "text", status: "verified" },
240-{ capability: "media", status: "verified" },
241-]),
239+media: async () => {
240+const result = await adapterSendMedia({
241+cfg: emptyConfig,
242+to: "chat:chat-1",
243+text: "",
244+mediaUrl: "https://example.com/image.png",
245+accountId: "default",
246+});
247+expect(sendMediaCall()?.to).toBe("chat:chat-1");
248+expect(sendMediaCall()?.mediaUrl).toBe("https://example.com/image.png");
249+expect(sendMediaCall()?.accountId).toBe("default");
250+expect(result.receipt.platformMessageIds).toEqual(["feishu-media-1"]);
251+},
252+},
253+});
254+expect(proofs.some((proof) => proof.capability === "text" && proof.status === "verified")).toBe(
255+true,
242256);
257+expect(
258+proofs.some((proof) => proof.capability === "media" && proof.status === "verified"),
259+).toBe(true);
243260});
244261245262it("chunks outbound text without requiring Feishu runtime initialization", () => {
@@ -269,18 +286,12 @@ describe("feishuOutbound.sendText local-image auto-convert", () => {
269286mediaLocalRoots: [dir],
270287});
271288272-expect(sendMediaFeishuMock).toHaveBeenCalledWith(
273-expect.objectContaining({
274-to: "chat_1",
275-mediaUrl: file,
276-accountId: "main",
277-mediaLocalRoots: [dir],
278-}),
279-);
289+expect(sendMediaCall()?.to).toBe("chat_1");
290+expect(sendMediaCall()?.mediaUrl).toBe(file);
291+expect(sendMediaCall()?.accountId).toBe("main");
292+expect(sendMediaCall()?.mediaLocalRoots).toEqual([dir]);
280293expect(sendMessageFeishuMock).not.toHaveBeenCalled();
281-expect(result).toEqual(
282-expect.objectContaining({ channel: "feishu", messageId: "media_msg" }),
283-);
294+expectFeishuResult(result, "media_msg");
284295} finally {
285296await fs.rm(dir, { recursive: true, force: true });
286297}
@@ -295,13 +306,9 @@ describe("feishuOutbound.sendText local-image auto-convert", () => {
295306});
296307297308expect(sendMediaFeishuMock).not.toHaveBeenCalled();
298-expect(sendMessageFeishuMock).toHaveBeenCalledWith(
299-expect.objectContaining({
300-to: "chat_1",
301-text: "please upload /tmp/example.png",
302-accountId: "main",
303-}),
304-);
309+expect(sendMessageCall()?.to).toBe("chat_1");
310+expect(sendMessageCall()?.text).toBe("please upload /tmp/example.png");
311+expect(sendMessageCall()?.accountId).toBe("main");
305312});
306313307314it("falls back to plain text if local-image media send fails", async () => {
@@ -316,13 +323,9 @@ describe("feishuOutbound.sendText local-image auto-convert", () => {
316323});
317324318325expect(sendMediaFeishuMock).toHaveBeenCalledTimes(1);
319-expect(sendMessageFeishuMock).toHaveBeenCalledWith(
320-expect.objectContaining({
321-to: "chat_1",
322-text: file,
323-accountId: "main",
324-}),
325-);
326+expect(sendMessageCall()?.to).toBe("chat_1");
327+expect(sendMessageCall()?.text).toBe(file);
328+expect(sendMessageCall()?.accountId).toBe("main");
326329} finally {
327330await fs.rm(dir, { recursive: true, force: true });
328331}
@@ -336,15 +339,11 @@ describe("feishuOutbound.sendText local-image auto-convert", () => {
336339accountId: "main",
337340});
338341339-expect(sendStructuredCardFeishuMock).toHaveBeenCalledWith(
340-expect.objectContaining({
341-to: "chat_1",
342-text: "| a | b |\n| - | - |",
343-accountId: "main",
344-}),
345-);
342+expect(sendStructuredCardCall()?.to).toBe("chat_1");
343+expect(sendStructuredCardCall()?.text).toBe("| a | b |\n| - | - |");
344+expect(sendStructuredCardCall()?.accountId).toBe("main");
346345expect(sendMessageFeishuMock).not.toHaveBeenCalled();
347-expect(result).toEqual(expect.objectContaining({ channel: "feishu", messageId: "card_msg" }));
346+expectFeishuResult(result, "card_msg");
348347});
349348350349it("forwards replyToId as replyToMessageId on sendText", async () => {
@@ -356,14 +355,10 @@ describe("feishuOutbound.sendText local-image auto-convert", () => {
356355accountId: "main",
357356});
358357359-expect(sendMessageFeishuMock).toHaveBeenCalledWith(
360-expect.objectContaining({
361-to: "chat_1",
362-text: "hello",
363-replyToMessageId: "om_reply_1",
364-accountId: "main",
365-}),
366-);
358+expect(sendMessageCall()?.to).toBe("chat_1");
359+expect(sendMessageCall()?.text).toBe("hello");
360+expect(sendMessageCall()?.replyToMessageId).toBe("om_reply_1");
361+expect(sendMessageCall()?.accountId).toBe("main");
367362});
368363369364it("falls back to threadId when replyToId is empty on sendText", async () => {
@@ -376,15 +371,11 @@ describe("feishuOutbound.sendText local-image auto-convert", () => {
376371accountId: "main",
377372});
378373379-expect(sendMessageFeishuMock).toHaveBeenCalledWith(
380-expect.objectContaining({
381-to: "chat_1",
382-text: "hello",
383-replyToMessageId: "om_thread_2",
384-replyInThread: true,
385-accountId: "main",
386-}),
387-);
374+expect(sendMessageCall()?.to).toBe("chat_1");
375+expect(sendMessageCall()?.text).toBe("hello");
376+expect(sendMessageCall()?.replyToMessageId).toBe("om_thread_2");
377+expect(sendMessageCall()?.replyInThread).toBe(true);
378+expect(sendMessageCall()?.accountId).toBe("main");
388379});
389380});
390381@@ -427,32 +418,26 @@ describe("feishuOutbound.sendPayload native cards", () => {
427418},
428419});
429420430-expect(rendered).toEqual(
431-expect.objectContaining({
432-text: "Approval\n\nApprove the request?\n\n- Approve",
433-channelData: {
434-feishu: {
435-card: expect.objectContaining({
436-schema: "2.0",
437-header: {
438-title: { tag: "plain_text", content: "Approval" },
439-template: "green",
440-},
441-body: {
442-elements: expect.arrayContaining([
443-{ tag: "markdown", content: "Approve the request?" },
444-expect.objectContaining({ tag: "action" }),
445-]),
446-},
447-}),
448-},
449-},
450-}),
451-);
452-453421if (!rendered) {
454422throw new Error("expected Feishu presentation renderer to return a payload");
455423}
424+expect(rendered.text).toBe("Approval\n\nApprove the request?\n\n- Approve");
425+const renderedChannelData = rendered.channelData as
426+| { feishu?: { card?: Record<string, any> } }
427+| undefined;
428+const renderedCard = renderedChannelData?.feishu?.card;
429+expect(renderedCard?.schema).toBe("2.0");
430+expect(renderedCard?.header).toEqual({
431+title: { tag: "plain_text", content: "Approval" },
432+template: "green",
433+});
434+expect(renderedCard?.body?.elements?.[0]).toEqual({
435+tag: "markdown",
436+content: "Approve the request?",
437+});
438+expect(
439+renderedCard?.body?.elements?.some((element: { tag?: string }) => element.tag === "action"),
440+).toBe(true);
456441const { presentation: _presentation, ...coreRenderedPayload } = rendered;
457442const result = await feishuOutbound.sendPayload?.({
458443cfg: emptyConfig,
@@ -462,21 +447,13 @@ describe("feishuOutbound.sendPayload native cards", () => {
462447payload: coreRenderedPayload,
463448});
464449465-expect(sendCardFeishuMock).toHaveBeenCalledWith(
466-expect.objectContaining({
467-to: "chat_1",
468-card: expect.objectContaining({
469-header: {
470-title: { tag: "plain_text", content: "Approval" },
471-template: "green",
472-},
473-}),
474-}),
475-);
450+expect(sendCardCall()?.to).toBe("chat_1");
451+expect(sendCardCall()?.card?.header).toEqual({
452+title: { tag: "plain_text", content: "Approval" },
453+template: "green",
454+});
476455expect(sendMessageFeishuMock).not.toHaveBeenCalled();
477-expect(result).toEqual(
478-expect.objectContaining({ channel: "feishu", messageId: "native_card_msg" }),
479-);
456+expectFeishuResult(result, "native_card_msg");
480457});
481458482459it("sends interactive button payloads as native Feishu cards", async () => {
@@ -502,52 +479,31 @@ describe("feishuOutbound.sendPayload native cards", () => {
502479},
503480});
504481505-expect(sendCardFeishuMock).toHaveBeenCalledWith(
506-expect.objectContaining({
507-cfg: emptyConfig,
508-to: "chat_1",
509-accountId: "main",
510-}),
511-);
482+expect(sendCardCall()?.cfg).toBe(emptyConfig);
483+expect(sendCardCall()?.to).toBe("chat_1");
484+expect(sendCardCall()?.accountId).toBe("main");
512485const card = sendCardFeishuMock.mock.calls[0][0].card;
513-expect(card).toEqual(
514-expect.objectContaining({
515-schema: "2.0",
516-body: {
517-elements: expect.arrayContaining([
518-{ tag: "markdown", content: "Choose an action" },
519-{ tag: "markdown", content: "Approve the request?" },
520-expect.objectContaining({
521-tag: "action",
522-actions: [
523-expect.objectContaining({
524-text: { tag: "plain_text", content: "Approve" },
525-type: "primary",
526-value: expect.objectContaining({
527-oc: "ocf1",
528-k: "quick",
529-q: "/approve req_1 allow-once",
530-}),
531-}),
532-expect.objectContaining({
533-text: { tag: "plain_text", content: "Deny" },
534-type: "danger",
535-value: expect.objectContaining({
536-oc: "ocf1",
537-k: "quick",
538-q: "/approve req_1 deny",
539-}),
540-}),
541-],
542-}),
543-]),
544-},
545-}),
486+expect(card.schema).toBe("2.0");
487+expect(card.body.elements[0]).toEqual({ tag: "markdown", content: "Choose an action" });
488+expect(card.body.elements[1]).toEqual({
489+tag: "markdown",
490+content: "Approve the request?",
491+});
492+const actionElement = card.body.elements.find(
493+(element: { tag?: string }) => element.tag === "action",
546494);
495+expect(actionElement?.actions[0]?.text).toEqual({ tag: "plain_text", content: "Approve" });
496+expect(actionElement?.actions[0]?.type).toBe("primary");
497+expect(actionElement?.actions[0]?.value?.oc).toBe("ocf1");
498+expect(actionElement?.actions[0]?.value?.k).toBe("quick");
499+expect(actionElement?.actions[0]?.value?.q).toBe("/approve req_1 allow-once");
500+expect(actionElement?.actions[1]?.text).toEqual({ tag: "plain_text", content: "Deny" });
501+expect(actionElement?.actions[1]?.type).toBe("danger");
502+expect(actionElement?.actions[1]?.value?.oc).toBe("ocf1");
503+expect(actionElement?.actions[1]?.value?.k).toBe("quick");
504+expect(actionElement?.actions[1]?.value?.q).toBe("/approve req_1 deny");
547505expect(sendMessageFeishuMock).not.toHaveBeenCalled();
548-expect(result).toEqual(
549-expect.objectContaining({ channel: "feishu", messageId: "native_card_msg" }),
550-);
506+expectFeishuResult(result, "native_card_msg");
551507});
552508553509it("escapes generated markdown card text and drops unsafe button URLs", async () => {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。