import type { ClawdbotConfig } from "openclaw/plugin-sdk/feishu"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { buildStructuredCard, editMessageFeishu, getMessageFeishu, listFeishuThreadMessages, resolveFeishuCardTemplate, } from "./send.js"; const { mockClientGet, mockClientList, mockClientPatch, mockCreateFeishuClient, mockResolveFeishuAccount, } = vi.hoisted(() => ({ mockClientGet: vi.fn(), mockClientList: vi.fn(), mockClientPatch: vi.fn(), mockCreateFeishuClient: vi.fn(), mockResolveFeishuAccount: vi.fn(), })); vi.mock("./client.js", () => ({ createFeishuClient: mockCreateFeishuClient, })); vi.mock("./accounts.js", () => ({ resolveFeishuAccount: mockResolveFeishuAccount, })); vi.mock("./runtime.js", () => ({ getFeishuRuntime: () => ({ channel: { text: { resolveMarkdownTableMode: () => "preserve", convertMarkdownTables: (text: string) => text, }, }, }), })); describe("getMessageFeishu", () => { beforeEach(() => { vi.clearAllMocks(); mockResolveFeishuAccount.mockReturnValue({ accountId: "default", configured: true, }); mockCreateFeishuClient.mockReturnValue({ im: { message: { get: mockClientGet, list: mockClientList, patch: mockClientPatch, }, }, }); }); it("extracts text content from interactive card elements", async () => { mockClientGet.mockResolvedValueOnce({ code: 0, data: { items: [ { message_id: "om_1", chat_id: "oc_1", msg_type: "interactive", body: { content: JSON.stringify({ elements: [ { tag: "markdown", content: "hello markdown" }, { tag: "div", text: { content: "hello div" } }, ], }), }, }, ], }, }); const result = await getMessageFeishu({ cfg: {} as ClawdbotConfig, messageId: "om_1", }); expect(result).toEqual( expect.objectContaining({ messageId: "om_1", chatId: "oc_1", contentType: "interactive", content: "hello markdown\nhello div", }), ); }); it("extracts text content from post messages", async () => { mockClientGet.mockResolvedValueOnce({ code: 0, data: { items: [ { message_id: "om_post", chat_id: "oc_post", msg_type: "post", body: { content: JSON.stringify({ zh_cn: { title: "Summary", content: [[{ tag: "text", text: "post body" }]], }, }), }, }, ], }, }); const result = await getMessageFeishu({ cfg: {} as ClawdbotConfig, messageId: "om_post", }); expect(result).toEqual( expect.objectContaining({ messageId: "om_post", chatId: "oc_post", contentType: "post", content: "Summary\n\npost body", }), ); }); it("returns text placeholder instead of raw JSON for unsupported message types", async () => { mockClientGet.mockResolvedValueOnce({ code: 0, data: { items: [ { message_id: "om_file", chat_id: "oc_file", msg_type: "file", body: { content: JSON.stringify({ file_key: "file_v3_123" }), }, }, ], }, }); const result = await getMessageFeishu({ cfg: {} as ClawdbotConfig, messageId: "om_file", }); expect(result).toEqual( expect.objectContaining({ messageId: "om_file", chatId: "oc_file", contentType: "file", content: "[file message]", }), ); }); it("supports single-object response shape from Feishu API", async () => { mockClientGet.mockResolvedValueOnce({ code: 0, data: { message_id: "om_single", chat_id: "oc_single", msg_type: "text", body: { content: JSON.stringify({ text: "single payload" }), }, }, }); const result = await getMessageFeishu({ cfg: {} as ClawdbotConfig, messageId: "om_single", }); expect(result).toEqual( expect.objectContaining({ messageId: "om_single", chatId: "oc_single", contentType: "text", content: "single payload", }), ); }); it("reuses the same content parsing for thread history messages", async () => { mockClientList.mockResolvedValueOnce({ code: 0, data: { items: [ { message_id: "om_root", msg_type: "text", body: { content: JSON.stringify({ text: "root starter" }), }, }, { message_id: "om_card", msg_type: "interactive", body: { content: JSON.stringify({ body: { elements: [{ tag: "markdown", content: "hello from card 2.0" }], }, }), }, sender: { id: "app_1", sender_type: "app", }, create_time: "1710000000000", }, { message_id: "om_file", msg_type: "file", body: { content: JSON.stringify({ file_key: "file_v3_123" }), }, sender: { id: "ou_1", sender_type: "user", }, create_time: "1710000001000", }, ], }, }); const result = await listFeishuThreadMessages({ cfg: {} as ClawdbotConfig, threadId: "omt_1", rootMessageId: "om_root", }); expect(result).toEqual([ expect.objectContaining({ messageId: "om_file", contentType: "file", content: "[file message]", }), expect.objectContaining({ messageId: "om_card", contentType: "interactive", content: "hello from card 2.0", }), ]); }); }); describe("editMessageFeishu", () => { beforeEach(() => { vi.clearAllMocks(); mockResolveFeishuAccount.mockReturnValue({ accountId: "default", configured: true, }); mockCreateFeishuClient.mockReturnValue({ im: { message: { patch: mockClientPatch, }, }, }); }); it("patches post content for text edits", async () => { mockClientPatch.mockResolvedValueOnce({ code: 0 }); const result = await editMessageFeishu({ cfg: {} as ClawdbotConfig, messageId: "om_edit", text: "updated body", }); expect(mockClientPatch).toHaveBeenCalledWith({ path: { message_id: "om_edit" }, data: { content: JSON.stringify({ zh_cn: { content: [ [ { tag: "md", text: "updated body", }, ], ], }, }), }, }); expect(result).toEqual({ messageId: "om_edit", contentType: "post" }); }); it("patches interactive content for card edits", async () => { mockClientPatch.mockResolvedValueOnce({ code: 0 }); const result = await editMessageFeishu({ cfg: {} as ClawdbotConfig, messageId: "om_card", card: { schema: "2.0" }, }); expect(mockClientPatch).toHaveBeenCalledWith({ path: { message_id: "om_card" }, data: { content: JSON.stringify({ schema: "2.0" }), }, }); expect(result).toEqual({ messageId: "om_card", contentType: "interactive" }); }); }); describe("resolveFeishuCardTemplate", () => { it("accepts supported Feishu templates", () => { expect(resolveFeishuCardTemplate(" purple ")).toBe("purple"); }); it("drops unsupported free-form identity themes", () => { expect(resolveFeishuCardTemplate("space lobster")).toBeUndefined(); }); }); describe("buildStructuredCard", () => { it("falls back to blue when the header template is unsupported", () => { const card = buildStructuredCard("hello", { header: { title: "Agent", template: "space lobster", }, }); expect(card).toEqual( expect.objectContaining({ header: { title: { tag: "plain_text", content: "Agent" }, template: "blue", }, }), ); }); });