import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import type { ClawdbotConfig } from "../runtime-api.js"; import { buildMarkdownCard } from "./send.js"; const { mockConvertMarkdownTables, mockClientGet, mockClientList, mockClientPatch, mockCreateFeishuClient, mockResolveMarkdownTableMode, mockResolveFeishuAccount, mockRuntimeConvertMarkdownTables, mockRuntimeResolveMarkdownTableMode, } = vi.hoisted(() => ({ mockConvertMarkdownTables: vi.fn((text: string) => text), mockClientGet: vi.fn(), mockClientList: vi.fn(), mockClientPatch: vi.fn(), mockCreateFeishuClient: vi.fn(), mockResolveMarkdownTableMode: vi.fn(() => "preserve"), mockResolveFeishuAccount: vi.fn(), mockRuntimeConvertMarkdownTables: vi.fn((text: string) => text), mockRuntimeResolveMarkdownTableMode: vi.fn(() => "preserve"), })); vi.mock("openclaw/plugin-sdk/config-runtime", () => ({ resolveMarkdownTableMode: mockResolveMarkdownTableMode, })); vi.mock("openclaw/plugin-sdk/text-runtime", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, convertMarkdownTables: mockConvertMarkdownTables, }; }); vi.mock("./client.js", () => ({ createFeishuClient: mockCreateFeishuClient, })); vi.mock("./accounts.js", () => ({ resolveFeishuAccount: mockResolveFeishuAccount, resolveFeishuRuntimeAccount: mockResolveFeishuAccount, })); vi.mock("./runtime.js", () => ({ getFeishuRuntime: () => ({ channel: { text: { resolveMarkdownTableMode: mockRuntimeResolveMarkdownTableMode, convertMarkdownTables: mockRuntimeConvertMarkdownTables, }, }, }), })); let buildStructuredCard: typeof import("./send.js").buildStructuredCard; let editMessageFeishu: typeof import("./send.js").editMessageFeishu; let getMessageFeishu: typeof import("./send.js").getMessageFeishu; let listFeishuThreadMessages: typeof import("./send.js").listFeishuThreadMessages; let resolveFeishuCardTemplate: typeof import("./send.js").resolveFeishuCardTemplate; let sendMessageFeishu: typeof import("./send.js").sendMessageFeishu; describe("getMessageFeishu", () => { beforeAll(async () => { ({ buildStructuredCard, editMessageFeishu, getMessageFeishu, listFeishuThreadMessages, resolveFeishuCardTemplate, sendMessageFeishu, } = await import("./send.js")); }); beforeEach(() => { vi.clearAllMocks(); mockResolveMarkdownTableMode.mockReturnValue("preserve"); mockConvertMarkdownTables.mockImplementation((text: string) => text); mockRuntimeResolveMarkdownTableMode.mockReturnValue("preserve"); mockRuntimeConvertMarkdownTables.mockImplementation((text: string) => text); mockResolveFeishuAccount.mockReturnValue({ accountId: "default", configured: true, }); mockCreateFeishuClient.mockReturnValue({ im: { message: { create: vi.fn(), get: mockClientGet, list: mockClientList, patch: mockClientPatch, }, }, }); }); it("sends text without requiring Feishu runtime text helpers", async () => { mockRuntimeResolveMarkdownTableMode.mockImplementation(() => { throw new Error("Feishu runtime not initialized"); }); mockRuntimeConvertMarkdownTables.mockImplementation(() => { throw new Error("Feishu runtime not initialized"); }); mockClientPatch.mockResolvedValueOnce({ code: 0 }); mockCreateFeishuClient.mockReturnValue({ im: { message: { create: vi.fn().mockResolvedValue({ code: 0, data: { message_id: "om_send" } }), reply: vi.fn(), get: mockClientGet, list: mockClientList, patch: mockClientPatch, }, }, }); const result = await sendMessageFeishu({ cfg: {} as ClawdbotConfig, to: "oc_send", text: "hello", }); expect(mockResolveMarkdownTableMode).toHaveBeenCalledWith({ cfg: {}, channel: "feishu", }); expect(mockConvertMarkdownTables).toHaveBeenCalledWith("hello", "preserve"); expect(result).toEqual({ messageId: "om_send", chatId: "oc_send" }); }); 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 () => { mockRuntimeResolveMarkdownTableMode.mockImplementation(() => { throw new Error("Feishu runtime not initialized"); }); mockRuntimeConvertMarkdownTables.mockImplementation(() => { throw new Error("Feishu runtime not initialized"); }); 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("uses schema-2.0 width config instead of legacy wide screen mode", () => { const card = buildStructuredCard("hello") as { config: { width_mode?: string; enable_forward?: boolean; wide_screen_mode?: boolean; }; }; expect(card.config.width_mode).toBe("fill"); expect(card.config.enable_forward).toBeUndefined(); expect(card.config.wide_screen_mode).toBeUndefined(); }); 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", }, }), ); }); }); describe("buildMarkdownCard", () => { it("uses schema-2.0 width config instead of legacy wide screen mode", () => { const card = buildMarkdownCard("hello") as { config: { width_mode?: string; enable_forward?: boolean; wide_screen_mode?: boolean; }; }; expect(card.config.width_mode).toBe("fill"); expect(card.config.enable_forward).toBeUndefined(); expect(card.config.wide_screen_mode).toBeUndefined(); }); });