diff --git a/extensions/feishu/src/channel.test.ts b/extensions/feishu/src/channel.test.ts index 730dddea4a2..9bc6fa07fd1 100644 --- a/extensions/feishu/src/channel.test.ts +++ b/extensions/feishu/src/channel.test.ts @@ -59,6 +59,36 @@ function getDescribedActions(cfg: OpenClawConfig, accountId?: string): string[] return [...(feishuPlugin.actions?.describeMessageTool?.({ cfg, accountId })?.actions ?? [])]; } +function requireRecord(value: unknown, label: string): Record { + if (!value || typeof value !== "object" || Array.isArray(value)) { + throw new Error(`Expected ${label}`); + } + return value as Record; +} + +function requireArray(value: unknown, label: string): unknown[] { + if (!Array.isArray(value)) { + throw new Error(`Expected ${label}`); + } + return value; +} + +function mockCallArg(mock: unknown, callIndex: number, argIndex: number, label: string) { + const calls = (mock as { mock?: { calls?: unknown[][] } }).mock?.calls; + if (!Array.isArray(calls)) { + throw new Error(`Expected ${label} mock calls`); + } + const call = calls[callIndex]; + if (!call) { + throw new Error(`Expected ${label} call ${callIndex + 1}`); + } + return call[argIndex]; +} + +function resultDetails(result: unknown) { + return requireRecord(requireRecord(result, "action result").details, "action result details"); +} + afterAll(() => { vi.doUnmock("./probe.js"); vi.doUnmock("./client.js"); @@ -93,14 +123,16 @@ describe("feishuPlugin.status.probeAccount", () => { }); expect(probeFeishuMock).toHaveBeenCalledTimes(1); - expect(probeFeishuMock).toHaveBeenCalledWith( - expect.objectContaining({ - accountId: "main", - appId: "cli_main", - appSecret: "secret_main", - }), + const probeArgs = requireRecord( + mockCallArg(probeFeishuMock, 0, 0, "probeFeishu"), + "probe args", ); - expect(result).toMatchObject({ ok: true, appId: "cli_main" }); + expect(probeArgs.accountId).toBe("main"); + expect(probeArgs.appId).toBe("cli_main"); + expect(probeArgs.appSecret).toBe("secret_main"); + const resultRecord = requireRecord(result, "probe result"); + expect(resultRecord.ok).toBe(true); + expect(resultRecord.appId).toBe("cli_main"); }); }); @@ -131,13 +163,13 @@ describe("feishuPlugin.pairing.notifyApproval", () => { accountId: "work", }); - expect(sendMessageFeishuMock).toHaveBeenCalledWith( - expect.objectContaining({ - cfg, - to: "ou_user", - accountId: "work", - }), + const sendArgs = requireRecord( + mockCallArg(sendMessageFeishuMock, 0, 0, "sendMessageFeishu"), + "send args", ); + expect(sendArgs.cfg).toBe(cfg); + expect(sendArgs.to).toBe("ou_user"); + expect(sendArgs.accountId).toBe("work"); }); }); @@ -311,7 +343,10 @@ describe("feishuPlugin actions", () => { replyToMessageId: undefined, replyInThread: false, }); - expect(result?.details).toMatchObject({ ok: true, messageId: "om_sent", chatId: "oc_group_1" }); + const details = resultDetails(result); + expect(details.ok).toBe(true); + expect(details.messageId).toBe("om_sent"); + expect(details.chatId).toBe("oc_group_1"); }); it("renders presentation messages as cards", async () => { @@ -331,29 +366,33 @@ describe("feishuPlugin actions", () => { toolContext: {}, } as never); - expect(sendCardFeishuMock).toHaveBeenCalledWith({ - cfg, - to: "chat:oc_group_1", - card: expect.objectContaining({ - schema: "2.0", - header: { - title: { tag: "plain_text", content: "Status" }, - template: "blue", - }, - body: { - elements: [ - { - tag: "markdown", - content: "Build completed", - }, - ], - }, - }), - accountId: undefined, - replyToMessageId: undefined, - replyInThread: false, + const sendCardArgs = requireRecord( + mockCallArg(sendCardFeishuMock, 0, 0, "sendCardFeishu"), + "send card args", + ); + expect(sendCardArgs.cfg).toBe(cfg); + expect(sendCardArgs.to).toBe("chat:oc_group_1"); + expect(sendCardArgs.accountId).toBeUndefined(); + expect(sendCardArgs.replyToMessageId).toBeUndefined(); + expect(sendCardArgs.replyInThread).toBe(false); + const card = requireRecord(sendCardArgs.card, "card"); + expect(card.schema).toBe("2.0"); + expect(card.header).toEqual({ + title: { tag: "plain_text", content: "Status" }, + template: "blue", }); - expect(result?.details).toMatchObject({ ok: true, messageId: "om_card", chatId: "oc_group_1" }); + expect(card.body).toEqual({ + elements: [ + { + tag: "markdown", + content: "Build completed", + }, + ], + }); + const details = resultDetails(result); + expect(details.ok).toBe(true); + expect(details.messageId).toBe("om_card"); + expect(details.chatId).toBe("oc_group_1"); }); it("renders presentation button labels into the card fallback", async () => { @@ -377,20 +416,17 @@ describe("feishuPlugin actions", () => { toolContext: {}, } as never); - expect(sendCardFeishuMock).toHaveBeenCalledWith( - expect.objectContaining({ - card: expect.objectContaining({ - body: { - elements: [ - { - tag: "markdown", - content: "- Run help", - }, - ], - }, - }), - }), + const sendCardArgs = requireRecord( + mockCallArg(sendCardFeishuMock, 0, 0, "sendCardFeishu"), + "send card args", ); + const card = requireRecord(sendCardArgs.card, "card"); + expect(requireRecord(card.body, "card body").elements).toEqual([ + { + tag: "markdown", + content: "- Run help", + }, + ]); }); it("renders presentation select labels into the card fallback", async () => { @@ -415,20 +451,17 @@ describe("feishuPlugin actions", () => { toolContext: {}, } as never); - expect(sendCardFeishuMock).toHaveBeenCalledWith( - expect.objectContaining({ - card: expect.objectContaining({ - body: { - elements: [ - { - tag: "markdown", - content: "Pick one:\n- Option A", - }, - ], - }, - }), - }), + const sendCardArgs = requireRecord( + mockCallArg(sendCardFeishuMock, 0, 0, "sendCardFeishu"), + "send card args", ); + const card = requireRecord(sendCardArgs.card, "card"); + expect(requireRecord(card.body, "card body").elements).toEqual([ + { + tag: "markdown", + content: "Pick one:\n- Option A", + }, + ]); }); it("sends media through the outbound adapter", async () => { @@ -460,7 +493,7 @@ describe("feishuPlugin actions", () => { mediaLocalRoots: ["/tmp"], replyToId: undefined, }); - expect(result?.details).toMatchObject({ messageId: "om_media" }); + expect(resultDetails(result).messageId).toBe("om_media"); }); it("passes asVoice through media sends", async () => { @@ -483,12 +516,12 @@ describe("feishuPlugin actions", () => { mediaLocalRoots: [], } as never); - expect(feishuOutboundSendMediaMock).toHaveBeenCalledWith( - expect.objectContaining({ - mediaUrl: "https://example.com/reply.mp3", - audioAsVoice: true, - }), + const mediaArgs = requireRecord( + mockCallArg(feishuOutboundSendMediaMock, 0, 0, "feishuOutbound.sendMedia"), + "media args", ); + expect(mediaArgs.mediaUrl).toBe("https://example.com/reply.mp3"); + expect(mediaArgs.audioAsVoice).toBe(true); }); it("reads messages", async () => { @@ -510,10 +543,11 @@ describe("feishuPlugin actions", () => { messageId: "om_1", accountId: undefined, }); - expect(result?.details).toMatchObject({ - ok: true, - message: expect.objectContaining({ messageId: "om_1", content: "hello" }), - }); + const details = resultDetails(result); + expect(details.ok).toBe(true); + const message = requireRecord(details.message, "read message"); + expect(message.messageId).toBe("om_1"); + expect(message.content).toBe("hello"); }); it("returns an error result when message reads fail", async () => { @@ -549,7 +583,10 @@ describe("feishuPlugin actions", () => { card: undefined, accountId: undefined, }); - expect(result?.details).toMatchObject({ ok: true, messageId: "om_2", contentType: "post" }); + const details = resultDetails(result); + expect(details.ok).toBe(true); + expect(details.messageId).toBe("om_2"); + expect(details.contentType).toBe("post"); }); it("sends explicit thread replies with reply_in_thread semantics", async () => { @@ -571,11 +608,10 @@ describe("feishuPlugin actions", () => { replyToMessageId: "om_parent", replyInThread: true, }); - expect(result?.details).toMatchObject({ - ok: true, - action: "thread-reply", - messageId: "om_reply", - }); + const details = resultDetails(result); + expect(details.ok).toBe(true); + expect(details.action).toBe("thread-reply"); + expect(details.messageId).toBe("om_reply"); }); it("auto-threads `send` text against the inbound trigger in group_topic sessions", async () => { @@ -618,12 +654,12 @@ describe("feishuPlugin actions", () => { toolContext: { currentMessageId: "om_inbound" }, } as never); - expect(sendCardFeishuMock).toHaveBeenCalledWith( - expect.objectContaining({ - replyToMessageId: "om_inbound", - replyInThread: true, - }), + const sendCardArgs = requireRecord( + mockCallArg(sendCardFeishuMock, 0, 0, "sendCardFeishu"), + "send card args", ); + expect(sendCardArgs.replyToMessageId).toBe("om_inbound"); + expect(sendCardArgs.replyInThread).toBe(true); }); it("auto-threads `send` media against the inbound trigger in group_topic sessions", async () => { @@ -647,14 +683,12 @@ describe("feishuPlugin actions", () => { mediaLocalRoots: ["/tmp"], } as never); - expect(feishuOutboundSendMediaMock).toHaveBeenCalledWith( - expect.objectContaining({ - threadId: "om_inbound", - }), - ); - expect(feishuOutboundSendMediaMock).toHaveBeenCalledWith( - expect.not.objectContaining({ replyToId: expect.anything() }), + const mediaArgs = requireRecord( + mockCallArg(feishuOutboundSendMediaMock, 0, 0, "feishuOutbound.sendMedia"), + "media args", ); + expect(mediaArgs.threadId).toBe("om_inbound"); + expect("replyToId" in mediaArgs).toBe(false); }); it("auto-threads `send` in group_topic_sender sessions too", async () => { @@ -669,12 +703,12 @@ describe("feishuPlugin actions", () => { toolContext: { currentMessageId: "om_inbound" }, } as never); - expect(sendMessageFeishuMock).toHaveBeenCalledWith( - expect.objectContaining({ - replyToMessageId: "om_inbound", - replyInThread: true, - }), + const sendArgs = requireRecord( + mockCallArg(sendMessageFeishuMock, 0, 0, "sendMessageFeishu"), + "send args", ); + expect(sendArgs.replyToMessageId).toBe("om_inbound"); + expect(sendArgs.replyInThread).toBe(true); }); it("does not auto-thread `send` in plain group sessions (no topic)", async () => { @@ -736,10 +770,9 @@ describe("feishuPlugin actions", () => { messageId: "om_pin", accountId: undefined, }); - expect(result?.details).toMatchObject({ - ok: true, - pin: expect.objectContaining({ messageId: "om_pin" }), - }); + const details = resultDetails(result); + expect(details.ok).toBe(true); + expect(requireRecord(details.pin, "pin").messageId).toBe("om_pin"); }); it("lists pins", async () => { @@ -767,10 +800,10 @@ describe("feishuPlugin actions", () => { pageToken: undefined, accountId: undefined, }); - expect(result?.details).toMatchObject({ - ok: true, - pins: [expect.objectContaining({ messageId: "om_pin" })], - }); + const details = resultDetails(result); + expect(details.ok).toBe(true); + const pins = requireArray(details.pins, "pins"); + expect(requireRecord(pins[0], "pin").messageId).toBe("om_pin"); }); it("removes pins", async () => { @@ -786,7 +819,9 @@ describe("feishuPlugin actions", () => { messageId: "om_pin", accountId: undefined, }); - expect(result?.details).toMatchObject({ ok: true, messageId: "om_pin" }); + const details = resultDetails(result); + expect(details.ok).toBe(true); + expect(details.messageId).toBe("om_pin"); }); it("fetches channel info", async () => { @@ -802,10 +837,11 @@ describe("feishuPlugin actions", () => { expect(createFeishuClientMock).toHaveBeenCalled(); expect(getChatInfoMock).toHaveBeenCalledWith({ tag: "client" }, "oc_group_1"); - expect(result?.details).toMatchObject({ - ok: true, - channel: expect.objectContaining({ chat_id: "oc_group_1", name: "Eng" }), - }); + const details = resultDetails(result); + expect(details.ok).toBe(true); + const channel = requireRecord(details.channel, "channel"); + expect(channel.chat_id).toBe("oc_group_1"); + expect(channel.name).toBe("Eng"); }); it("fetches member lists from a chat", async () => { @@ -830,10 +866,12 @@ describe("feishuPlugin actions", () => { undefined, "open_id", ); - expect(result?.details).toMatchObject({ - ok: true, - members: [expect.objectContaining({ member_id: "ou_1", name: "Alice" })], - }); + const details = resultDetails(result); + expect(details.ok).toBe(true); + const members = requireArray(details.members, "members"); + const member = requireRecord(members[0], "member"); + expect(member.member_id).toBe("ou_1"); + expect(member.name).toBe("Alice"); }); it("fetches individual member info", async () => { @@ -848,10 +886,11 @@ describe("feishuPlugin actions", () => { } as never); expect(getFeishuMemberInfoMock).toHaveBeenCalledWith({ tag: "client" }, "ou_1", "open_id"); - expect(result?.details).toMatchObject({ - ok: true, - member: expect.objectContaining({ member_id: "ou_1", name: "Alice" }), - }); + const details = resultDetails(result); + expect(details.ok).toBe(true); + const member = requireRecord(details.member, "member"); + expect(member.member_id).toBe("ou_1"); + expect(member.name).toBe("Alice"); }); it("infers user_id lookups from the userId alias", async () => { @@ -907,11 +946,12 @@ describe("feishuPlugin actions", () => { fallbackToStatic: false, accountId: undefined, }); - expect(result?.details).toMatchObject({ - ok: true, - groups: [expect.objectContaining({ id: "oc_group_1" })], - peers: [expect.objectContaining({ id: "ou_1" })], - }); + const details = resultDetails(result); + expect(details.ok).toBe(true); + const groups = requireArray(details.groups, "groups"); + const peers = requireArray(details.peers, "peers"); + expect(requireRecord(groups[0], "group").id).toBe("oc_group_1"); + expect(requireRecord(peers[0], "peer").id).toBe("ou_1"); }); it("fails channel-list when live discovery fails", async () => { @@ -959,7 +999,9 @@ describe("feishuPlugin actions", () => { accountId: undefined, }); expect(removeReactionFeishuMock).toHaveBeenCalledTimes(2); - expect(result?.details).toMatchObject({ ok: true, removed: 2 }); + const details = resultDetails(result); + expect(details.ok).toBe(true); + expect(details.removed).toBe(2); }); it("fails for missing params on supported actions", async () => { @@ -992,13 +1034,13 @@ describe("feishuPlugin actions", () => { mediaLocalRoots: [], } as never); - expect(feishuOutboundSendMediaMock).toHaveBeenCalledWith( - expect.objectContaining({ - to: "chat:oc_group_1", - mediaUrl: "https://example.com/image.png", - }), + const mediaArgs = requireRecord( + mockCallArg(feishuOutboundSendMediaMock, 0, 0, "feishuOutbound.sendMedia"), + "media args", ); - expect(result?.details).toMatchObject({ messageId: "om_media_only" }); + expect(mediaArgs.to).toBe("chat:oc_group_1"); + expect(mediaArgs.mediaUrl).toBe("https://example.com/image.png"); + expect(resultDetails(result).messageId).toBe("om_media_only"); }); it("fails for unsupported action names", async () => {