import { describe, expect, it, vi } from "vitest"; import { mkdtemp, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { resolveSenderName, sendFile, sendLongText, sendTextMessage, type FeishuRuntime } from "../../src/feishu/client.js"; import { SenderNameCache } from "../../src/feishu/senderCache.js"; describe("Feishu client helpers", () => { it("accepts top-level file_key from the Lark SDK file upload response", async () => { const dir = await mkdtemp(join(tmpdir(), "hub-feishu-client-")); try { const file = join(dir, "student.pdf"); await writeFile(file, "pdf bytes"); const fileCreate = vi.fn(async () => ({ file_key: "file-key-1" })); const messageCreate = vi.fn(async () => ({ data: { message_id: "message-1" } })); const rt = mockRuntime({ fileCreate, messageCreate }); await expect(sendFile(rt, "chat-1", file, "student.pdf")).resolves.toBe("message-1"); expect(messageCreate).toHaveBeenCalledWith({ params: { receive_id_type: "chat_id" }, data: { receive_id: "chat-1", msg_type: "file", content: JSON.stringify({ file_key: "file-key-1" }) }, }); } finally { await rm(dir, { recursive: true, force: true }); } }); it("accepts top-level message_id from message create responses", async () => { const rt = mockRuntime({ fileCreate: vi.fn(), messageCreate: vi.fn(async () => ({ message_id: "message-1" })), }); await expect(sendTextMessage(rt, "chat-1", "hello")).resolves.toBe("message-1"); }); it("replies to a message without enabling Feishu topic mode", async () => { const messageCreate = vi.fn(); const messageReply = vi.fn(async () => ({ data: { message_id: "reply-1" } })); const rt = mockRuntime({ fileCreate: vi.fn(), messageCreate, messageReply }); await expect(sendTextMessage(rt, "chat-1", "hello", { replyToMessageId: "m-trigger" })).resolves.toBe("reply-1"); expect(messageCreate).not.toHaveBeenCalled(); expect(messageReply).toHaveBeenCalledWith({ path: { message_id: "m-trigger" }, data: { msg_type: "text", content: JSON.stringify({ text: "hello" }), }, }); }); it("sends long text in chunks and returns the last successful message id", async () => { let messageNumber = 0; const messageCreate = vi.fn(async () => ({ data: { message_id: `message-${++messageNumber}` } })); const rt = mockRuntime({ fileCreate: vi.fn(), messageCreate }); await expect(sendLongText(rt, "chat-1", "x".repeat(16_005))).resolves.toBe("message-3"); expect(messageCreate).toHaveBeenCalledTimes(3); const sentTexts = messageCreate.mock.calls.map(([payload]) => { const content = (payload as { data: { content: string } }).data.content; return (JSON.parse(content) as { text: string }).text; }); expect(sentTexts.map((text) => text.length)).toEqual([8000, 8000, 5]); }); it("resolves sender names through the basic contact API and caches hits", async () => { const contactBasicBatch = vi.fn(async () => ({ data: { users: [{ name: "Jiarong" }] } })); const request = vi.fn(); const rt = mockRuntime({ fileCreate: vi.fn(), messageCreate: vi.fn(), contactBasicBatch, request }); const cache = new SenderNameCache(); await expect(resolveSenderName(rt, "ou_user_1", cache)).resolves.toBe("Jiarong"); await expect(resolveSenderName(rt, "ou_user_1", cache)).resolves.toBe("Jiarong"); expect(contactBasicBatch).toHaveBeenCalledTimes(1); expect(contactBasicBatch).toHaveBeenCalledWith({ params: { user_id_type: "open_id" }, data: { user_ids: ["ou_user_1"] }, }); expect(request).not.toHaveBeenCalled(); }); }); function mockRuntime(options: { readonly fileCreate: (payload: unknown) => Promise; readonly messageCreate: (payload: unknown) => Promise; readonly messageReply?: (payload: unknown) => Promise; readonly contactBasicBatch?: (payload: unknown) => Promise; readonly request?: (payload: unknown) => Promise; }): FeishuRuntime { return { client: { request: options.request, contact: { v3: { user: { basicBatch: options.contactBasicBatch, }, }, }, im: { v1: { file: { create: options.fileCreate }, message: { create: options.messageCreate, reply: options.messageReply ?? vi.fn(async () => ({ data: { message_id: "reply-1" } })), }, }, }, } as unknown as FeishuRuntime["client"], logger: { info() {}, warn() {}, error() {}, debug() {}, fatal() {}, child() { return this; }, level: "silent", } as unknown as FeishuRuntime["logger"], }; }