





















@@ -123,6 +123,51 @@ describe("Feishu Card Action Handler", () => {
123123resetProcessedFeishuCardActionTokensForTests();
124124});
125125126+function mockCallArg(
127+mock: { mock: { calls: unknown[][] } },
128+index: number,
129+label: string,
130+): unknown {
131+const call = mock.mock.calls[index];
132+if (!call) {
133+throw new Error(`Expected ${label} call ${index + 1}`);
134+}
135+return call[0];
136+}
137+138+function requireRecord(value: unknown, label: string): Record<string, unknown> {
139+if (!value || typeof value !== "object") {
140+throw new Error(`Expected ${label}`);
141+}
142+return value as Record<string, unknown>;
143+}
144+145+function handleMessageEvent(callIndex = 0) {
146+const arg = requireRecord(
147+mockCallArg(vi.mocked(handleFeishuMessage), callIndex, "handleFeishuMessage"),
148+"handleFeishuMessage args",
149+);
150+return requireRecord(arg.event, "Feishu message event");
151+}
152+153+function handleMessage(callIndex = 0) {
154+return requireRecord(handleMessageEvent(callIndex).message, "Feishu message");
155+}
156+157+function sendMessageCall(callIndex = 0) {
158+return requireRecord(
159+mockCallArg(sendMessageFeishuMock, callIndex, "sendMessageFeishu"),
160+"sendMessageFeishu args",
161+);
162+}
163+164+function sendCardCall(callIndex = 0) {
165+return requireRecord(
166+mockCallArg(sendCardFeishuMock, callIndex, "sendCardFeishu"),
167+"sendCardFeishu args",
168+);
169+}
170+126171it("handles card action with text payload", async () => {
127172const event: FeishuCardActionEvent = {
128173operator: { open_id: "u123", user_id: "uid1", union_id: "un1" },
@@ -133,16 +178,9 @@ describe("Feishu Card Action Handler", () => {
133178134179await handleFeishuCardAction({ cfg, event, runtime });
135180136-expect(handleFeishuMessage).toHaveBeenCalledWith(
137-expect.objectContaining({
138-event: expect.objectContaining({
139-message: expect.objectContaining({
140-content: '{"text":"/ping"}',
141-chat_id: "chat1",
142-}),
143-}),
144-}),
145-);
181+const message = handleMessage();
182+expect(message.content).toBe('{"text":"/ping"}');
183+expect(message.chat_id).toBe("chat1");
146184});
147185148186it("handles card action with JSON object payload", async () => {
@@ -155,16 +193,9 @@ describe("Feishu Card Action Handler", () => {
155193156194await handleFeishuCardAction({ cfg, event, runtime });
157195158-expect(handleFeishuMessage).toHaveBeenCalledWith(
159-expect.objectContaining({
160-event: expect.objectContaining({
161-message: expect.objectContaining({
162-content: '{"text":"{\\"key\\":\\"val\\"}"}',
163-chat_id: "u123", // Fallback to open_id
164-}),
165-}),
166-}),
167-);
196+const message = handleMessage();
197+expect(message.content).toBe('{"text":"{\\"key\\":\\"val\\"}"}');
198+expect(message.chat_id).toBe("u123"); // Fallback to open_id
168199});
169200170201it("routes quick command actions with operator and conversation context", async () => {
@@ -176,23 +207,15 @@ describe("Feishu Card Action Handler", () => {
176207177208await handleFeishuCardAction({ cfg, event, runtime });
178209179-expect(handleFeishuMessage).toHaveBeenCalledWith(
180-expect.objectContaining({
181-event: expect.objectContaining({
182-sender: expect.objectContaining({
183-sender_id: expect.objectContaining({
184-open_id: "u123",
185-user_id: "uid1",
186-union_id: "un1",
187-}),
188-}),
189-message: expect.objectContaining({
190-chat_id: "chat1",
191-content: '{"text":"/help"}',
192-}),
193-}),
194-}),
195-);
210+const eventArg = handleMessageEvent();
211+const sender = requireRecord(eventArg.sender, "Feishu sender");
212+const senderId = requireRecord(sender.sender_id, "Feishu sender id");
213+expect(senderId.open_id).toBe("u123");
214+expect(senderId.user_id).toBe("uid1");
215+expect(senderId.union_id).toBe("un1");
216+const message = requireRecord(eventArg.message, "Feishu message");
217+expect(message.chat_id).toBe("chat1");
218+expect(message.content).toBe('{"text":"/help"}');
196219});
197220198221it("opens an approval card for metadata actions", async () => {
@@ -222,39 +245,27 @@ describe("Feishu Card Action Handler", () => {
222245223246await handleFeishuCardAction({ cfg, event, runtime, accountId: "main" });
224247225-expect(sendCardFeishuMock).toHaveBeenCalledWith(
226-expect.objectContaining({
227-to: "chat:chat1",
228-accountId: "main",
229-card: expect.objectContaining({
230-config: expect.objectContaining({
231-width_mode: "fill",
232-}),
233-header: expect.objectContaining({
234-title: expect.objectContaining({ content: "Confirm action" }),
235-}),
236-body: expect.objectContaining({
237-elements: expect.arrayContaining([
238-expect.objectContaining({
239-tag: "action",
240-actions: expect.arrayContaining([
241-expect.objectContaining({
242-value: expect.objectContaining({
243-c: expect.objectContaining({
244-u: "u123",
245-h: "chat1",
246-t: "group",
247-s: "agent:codex:feishu:chat:chat1",
248-}),
249-}),
250-}),
251-]),
252-}),
253-]),
254-}),
255-}),
256-}),
257-);
248+const cardCall = sendCardCall();
249+expect(cardCall.to).toBe("chat:chat1");
250+expect(cardCall.accountId).toBe("main");
251+const card = requireRecord(cardCall.card, "Feishu card");
252+expect(requireRecord(card.config, "Feishu card config").width_mode).toBe("fill");
253+const header = requireRecord(card.header, "Feishu card header");
254+expect(requireRecord(header.title, "Feishu card title").content).toBe("Confirm action");
255+const body = requireRecord(card.body, "Feishu card body");
256+const elements = body.elements as Array<Record<string, unknown>>;
257+const actionElement = elements.find((element) => element.tag === "action");
258+if (!actionElement) {
259+throw new Error("Expected action element");
260+}
261+const actions = actionElement.actions as Array<Record<string, unknown>>;
262+const actionValue = requireRecord(actions[0]?.value, "Feishu approval action value");
263+const approvalContext = requireRecord(actionValue.c, "Feishu approval context");
264+expect(approvalContext.u).toBe("u123");
265+expect(approvalContext.h).toBe("chat1");
266+expect(approvalContext.t).toBe("group");
267+expect(approvalContext.s).toBe("agent:codex:feishu:chat:chat1");
268+expect(typeof approvalContext.e).toBe("number");
258269expectFirstSentCardUsesFillWidthOnly(sendCardFeishuMock);
259270expect(handleFeishuMessage).not.toHaveBeenCalled();
260271});
@@ -268,15 +279,7 @@ describe("Feishu Card Action Handler", () => {
268279269280await handleFeishuCardAction({ cfg, event, runtime });
270281271-expect(handleFeishuMessage).toHaveBeenCalledWith(
272-expect.objectContaining({
273-event: expect.objectContaining({
274-message: expect.objectContaining({
275-content: '{"text":"/new"}',
276-}),
277-}),
278-}),
279-);
282+expect(handleMessage().content).toBe('{"text":"/new"}');
280283});
281284282285it("safely rejects stale structured actions", async () => {
@@ -292,12 +295,9 @@ describe("Feishu Card Action Handler", () => {
292295293296await handleFeishuCardAction({ cfg, event, runtime });
294297295-expect(sendMessageFeishuMock).toHaveBeenCalledWith(
296-expect.objectContaining({
297-to: "chat:chat1",
298-text: expect.stringContaining("expired"),
299-}),
300-);
298+const sendMessage = sendMessageCall();
299+expect(sendMessage.to).toBe("chat:chat1");
300+expect(String(sendMessage.text)).toContain("expired");
301301expect(handleFeishuMessage).not.toHaveBeenCalled();
302302});
303303@@ -312,11 +312,7 @@ describe("Feishu Card Action Handler", () => {
312312313313await handleFeishuCardAction({ cfg, event, runtime });
314314315-expect(sendMessageFeishuMock).toHaveBeenCalledWith(
316-expect.objectContaining({
317-text: expect.stringContaining("different user"),
318-}),
319-);
315+expect(String(sendMessageCall().text)).toContain("different user");
320316expect(handleFeishuMessage).not.toHaveBeenCalled();
321317});
322318@@ -337,12 +333,9 @@ describe("Feishu Card Action Handler", () => {
337333338334await handleFeishuCardAction({ cfg, event, runtime });
339335340-expect(sendMessageFeishuMock).toHaveBeenCalledWith(
341-expect.objectContaining({
342-to: "chat:chat1",
343-text: "Cancelled.",
344-}),
345-);
336+const sendMessage = sendMessageCall();
337+expect(sendMessage.to).toBe("chat:chat1");
338+expect(sendMessage.text).toBe("Cancelled.");
346339});
347340348341it("preserves p2p callbacks for DM quick actions", async () => {
@@ -356,16 +349,9 @@ describe("Feishu Card Action Handler", () => {
356349357350await handleFeishuCardAction({ cfg, event, runtime });
358351359-expect(handleFeishuMessage).toHaveBeenCalledWith(
360-expect.objectContaining({
361-event: expect.objectContaining({
362-message: expect.objectContaining({
363-chat_id: "p2p-chat-1",
364-chat_type: "p2p",
365-}),
366-}),
367-}),
368-);
352+const message = handleMessage();
353+expect(message.chat_id).toBe("p2p-chat-1");
354+expect(message.chat_type).toBe("p2p");
369355});
370356371357it("resolves DM chat type from the Feishu chat API when card context omits it", async () => {
@@ -384,16 +370,9 @@ describe("Feishu Card Action Handler", () => {
384370385371await handleFeishuCardAction({ cfg, event, runtime });
386372387-expect(handleFeishuMessage).toHaveBeenCalledWith(
388-expect.objectContaining({
389-event: expect.objectContaining({
390-message: expect.objectContaining({
391-chat_id: "oc_dm_chat_123",
392-chat_type: "p2p",
393-}),
394-}),
395-}),
396-);
373+const message = handleMessage();
374+expect(message.chat_id).toBe("oc_dm_chat_123");
375+expect(message.chat_type).toBe("p2p");
397376expect(createFeishuClientMock).toHaveBeenCalledTimes(1);
398377});
399378@@ -445,15 +424,7 @@ describe("Feishu Card Action Handler", () => {
445424446425await handleFeishuCardAction({ cfg, event, runtime });
447426448-expect(handleFeishuMessage).toHaveBeenCalledWith(
449-expect.objectContaining({
450-event: expect.objectContaining({
451-message: expect.objectContaining({
452-chat_type: "p2p",
453-}),
454-}),
455-}),
456-);
427+expect(handleMessage().chat_type).toBe("p2p");
457428});
458429459430it("falls back to p2p when Feishu chat API throws", async () => {
@@ -472,15 +443,7 @@ describe("Feishu Card Action Handler", () => {
472443473444await handleFeishuCardAction({ cfg, event, runtime });
474445475-expect(handleFeishuMessage).toHaveBeenCalledWith(
476-expect.objectContaining({
477-event: expect.objectContaining({
478-message: expect.objectContaining({
479-chat_type: "p2p",
480-}),
481-}),
482-}),
483-);
446+expect(handleMessage().chat_type).toBe("p2p");
484447});
485448486449it("drops duplicate structured callback tokens", async () => {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。