test(feishu): type basic fixtures

This commit is contained in:
Ayaan Zaidi 2026-03-27 11:40:27 +05:30
parent 9a7c8e186e
commit b23dc5073f
No known key found for this signature in database
5 changed files with 92 additions and 99 deletions

View File

@ -1,12 +1,12 @@
import { describe, it, expect } from "vitest";
import { parseFeishuMessageEvent } from "./bot.js";
import { parseFeishuMessageEvent, type FeishuMessageEvent } from "./bot.js";
// Helper to build a minimal FeishuMessageEvent for testing
function makeEvent(
chatType: "p2p" | "group" | "private",
mentions?: Array<{ key: string; name: string; id: { open_id?: string } }>,
text = "hello",
) {
): FeishuMessageEvent {
return {
sender: {
sender_id: { user_id: "u1", open_id: "ou_sender" },
@ -22,7 +22,7 @@ function makeEvent(
};
}
function makePostEvent(content: unknown) {
function makePostEvent(content: unknown): FeishuMessageEvent {
return {
sender: { sender_id: { user_id: "u1", open_id: "ou_sender" } },
message: {
@ -36,7 +36,7 @@ function makePostEvent(content: unknown) {
};
}
function makeShareChatEvent(content: unknown) {
function makeShareChatEvent(content: unknown): FeishuMessageEvent {
return {
sender: { sender_id: { user_id: "u1", open_id: "ou_sender" } },
message: {
@ -55,15 +55,15 @@ describe("parseFeishuMessageEvent mentionedBot", () => {
it("returns mentionedBot=false when there are no mentions", () => {
const event = makeEvent("group", []);
const ctx = parseFeishuMessageEvent(event as any, BOT_OPEN_ID);
const ctx = parseFeishuMessageEvent(event, BOT_OPEN_ID);
expect(ctx.mentionedBot).toBe(false);
});
it("falls back to sender user_id when open_id is missing", () => {
const event = makeEvent("p2p", []);
(event as any).sender.sender_id = { user_id: "u_mobile_only" };
event.sender.sender_id = { user_id: "u_mobile_only" };
const ctx = parseFeishuMessageEvent(event as any, BOT_OPEN_ID);
const ctx = parseFeishuMessageEvent(event, BOT_OPEN_ID);
expect(ctx.senderOpenId).toBe("u_mobile_only");
expect(ctx.senderId).toBe("u_mobile_only");
});
@ -72,7 +72,7 @@ describe("parseFeishuMessageEvent mentionedBot", () => {
const event = makeEvent("group", [
{ key: "@_user_1", name: "Bot", id: { open_id: BOT_OPEN_ID } },
]);
const ctx = parseFeishuMessageEvent(event as any, BOT_OPEN_ID);
const ctx = parseFeishuMessageEvent(event, BOT_OPEN_ID);
expect(ctx.mentionedBot).toBe(true);
});
@ -80,7 +80,7 @@ describe("parseFeishuMessageEvent mentionedBot", () => {
const event = makeEvent("group", [
{ key: "@_user_1", name: "OpenClaw Bot (Alias)", id: { open_id: BOT_OPEN_ID } },
]);
const ctx = parseFeishuMessageEvent(event as any, BOT_OPEN_ID, "OpenClaw Bot");
const ctx = parseFeishuMessageEvent(event, BOT_OPEN_ID, "OpenClaw Bot");
expect(ctx.mentionedBot).toBe(true);
});
@ -88,7 +88,7 @@ describe("parseFeishuMessageEvent mentionedBot", () => {
const event = makeEvent("group", [
{ key: "@_user_1", name: "Alice", id: { open_id: "ou_alice" } },
]);
const ctx = parseFeishuMessageEvent(event as any, BOT_OPEN_ID);
const ctx = parseFeishuMessageEvent(event, BOT_OPEN_ID);
expect(ctx.mentionedBot).toBe(false);
});
@ -96,7 +96,7 @@ describe("parseFeishuMessageEvent mentionedBot", () => {
const event = makeEvent("group", [
{ key: "@_user_1", name: "Alice", id: { open_id: "ou_alice" } },
]);
const ctx = parseFeishuMessageEvent(event as any, undefined);
const ctx = parseFeishuMessageEvent(event, undefined);
expect(ctx.mentionedBot).toBe(false);
});
@ -104,7 +104,7 @@ describe("parseFeishuMessageEvent mentionedBot", () => {
const event = makeEvent("group", [
{ key: "@_user_1", name: "Alice", id: { open_id: "ou_alice" } },
]);
const ctx = parseFeishuMessageEvent(event as any, "");
const ctx = parseFeishuMessageEvent(event, "");
expect(ctx.mentionedBot).toBe(false);
});
@ -114,7 +114,7 @@ describe("parseFeishuMessageEvent mentionedBot", () => {
[{ key: "@_bot_1", name: ".*", id: { open_id: BOT_OPEN_ID } }],
"@NotBot hello",
);
const ctx = parseFeishuMessageEvent(event as any, BOT_OPEN_ID);
const ctx = parseFeishuMessageEvent(event, BOT_OPEN_ID);
expect(ctx.content).toBe("@NotBot hello");
});
@ -124,7 +124,7 @@ describe("parseFeishuMessageEvent mentionedBot", () => {
[{ key: ".*", name: "Bot", id: { open_id: BOT_OPEN_ID } }],
"hello world",
);
const ctx = parseFeishuMessageEvent(event as any, BOT_OPEN_ID);
const ctx = parseFeishuMessageEvent(event, BOT_OPEN_ID);
expect(ctx.content).toBe("hello world");
});
@ -136,7 +136,7 @@ describe("parseFeishuMessageEvent mentionedBot", () => {
[{ tag: "text", text: "What does this document say" }],
],
});
const ctx = parseFeishuMessageEvent(event as any, BOT_OPEN_ID);
const ctx = parseFeishuMessageEvent(event, BOT_OPEN_ID);
expect(ctx.mentionedBot).toBe(true);
});
@ -144,7 +144,7 @@ describe("parseFeishuMessageEvent mentionedBot", () => {
const event = makePostEvent({
content: [[{ tag: "text", text: "hello" }]],
});
const ctx = parseFeishuMessageEvent(event as any, "ou_bot_123");
const ctx = parseFeishuMessageEvent(event, "ou_bot_123");
expect(ctx.mentionedBot).toBe(false);
});
@ -155,7 +155,7 @@ describe("parseFeishuMessageEvent mentionedBot", () => {
[{ tag: "text", text: "hello" }],
],
});
const ctx = parseFeishuMessageEvent(event as any, "ou_bot_123");
const ctx = parseFeishuMessageEvent(event, "ou_bot_123");
expect(ctx.mentionedBot).toBe(false);
});
@ -169,7 +169,7 @@ describe("parseFeishuMessageEvent mentionedBot", () => {
[{ tag: "code_block", language: "ts", text: "const x = 1;" }],
],
});
const ctx = parseFeishuMessageEvent(event as any, "ou_bot_123");
const ctx = parseFeishuMessageEvent(event, "ou_bot_123");
expect(ctx.content).toContain("before `inline()`");
expect(ctx.content).toContain("```ts\nconst x = 1;\n```");
});
@ -179,7 +179,7 @@ describe("parseFeishuMessageEvent mentionedBot", () => {
body: "Merged and Forwarded Message",
share_chat_id: "sc_abc123",
});
const ctx = parseFeishuMessageEvent(event as any, "ou_bot_123");
const ctx = parseFeishuMessageEvent(event, "ou_bot_123");
expect(ctx.content).toBe("Merged and Forwarded Message");
});
@ -187,7 +187,7 @@ describe("parseFeishuMessageEvent mentionedBot", () => {
const event = makeShareChatEvent({
share_chat_id: "sc_abc123",
});
const ctx = parseFeishuMessageEvent(event as any, "ou_bot_123");
const ctx = parseFeishuMessageEvent(event, "ou_bot_123");
expect(ctx.content).toBe("[Forwarded message: sc_abc123]");
});
});

View File

@ -1,11 +1,11 @@
import { describe, expect, it } from "vitest";
import { parseFeishuMessageEvent } from "./bot.js";
import { parseFeishuMessageEvent, type FeishuMessageEvent } from "./bot.js";
function makeEvent(
text: string,
mentions?: Array<{ key: string; name: string; id: { open_id?: string; user_id?: string } }>,
chatType: "p2p" | "group" = "p2p",
) {
): FeishuMessageEvent {
return {
sender: { sender_id: { user_id: "u1", open_id: "ou_sender" } },
message: {
@ -23,7 +23,7 @@ const BOT_OPEN_ID = "ou_bot";
describe("normalizeMentions (via parseFeishuMessageEvent)", () => {
it("returns original text when mentions are missing", () => {
const ctx = parseFeishuMessageEvent(makeEvent("hello world", undefined) as any, BOT_OPEN_ID);
const ctx = parseFeishuMessageEvent(makeEvent("hello world", undefined), BOT_OPEN_ID);
expect(ctx.content).toBe("hello world");
});
@ -31,7 +31,7 @@ describe("normalizeMentions (via parseFeishuMessageEvent)", () => {
const ctx = parseFeishuMessageEvent(
makeEvent("@_bot_1 hello", [
{ key: "@_bot_1", name: "Bot", id: { open_id: "ou_bot" } },
]) as any,
]),
BOT_OPEN_ID,
);
expect(ctx.content).toBe("hello");
@ -43,7 +43,7 @@ describe("normalizeMentions (via parseFeishuMessageEvent)", () => {
"@_bot_1 hello",
[{ key: "@_bot_1", name: "Bot", id: { open_id: "ou_bot" } }],
"group",
) as any,
),
BOT_OPEN_ID,
);
expect(ctx.content).toBe("hello");
@ -55,7 +55,7 @@ describe("normalizeMentions (via parseFeishuMessageEvent)", () => {
"@_bot_1 /model",
[{ key: "@_bot_1", name: "Bot", id: { open_id: "ou_bot" } }],
"group",
) as any,
),
BOT_OPEN_ID,
);
expect(ctx.content).toBe("/model");
@ -66,7 +66,7 @@ describe("normalizeMentions (via parseFeishuMessageEvent)", () => {
makeEvent("@_bot_1 @_user_alice hello", [
{ key: "@_bot_1", name: "Bot", id: { open_id: "ou_bot" } },
{ key: "@_user_alice", name: "Alice", id: { open_id: "ou_alice" } },
]) as any,
]),
BOT_OPEN_ID,
);
expect(ctx.content).toBe('<at user_id="ou_alice">Alice</at> hello');
@ -76,7 +76,7 @@ describe("normalizeMentions (via parseFeishuMessageEvent)", () => {
const ctx = parseFeishuMessageEvent(
makeEvent("@_user_1 hi", [
{ key: "@_user_1", name: "Alice", id: { user_id: "uid_alice" } },
]) as any,
]),
BOT_OPEN_ID,
);
expect(ctx.content).toBe("@Alice hi");
@ -84,7 +84,7 @@ describe("normalizeMentions (via parseFeishuMessageEvent)", () => {
it("falls back to plain @name when no id is present", () => {
const ctx = parseFeishuMessageEvent(
makeEvent("@_unknown hey", [{ key: "@_unknown", name: "Nobody", id: {} }]) as any,
makeEvent("@_unknown hey", [{ key: "@_unknown", name: "Nobody", id: {} }]),
BOT_OPEN_ID,
);
expect(ctx.content).toBe("@Nobody hey");
@ -92,7 +92,7 @@ describe("normalizeMentions (via parseFeishuMessageEvent)", () => {
it("treats mention key regex metacharacters as literal text", () => {
const ctx = parseFeishuMessageEvent(
makeEvent("hello world", [{ key: ".*", name: "Bot", id: { open_id: "ou_bot" } }]) as any,
makeEvent("hello world", [{ key: ".*", name: "Bot", id: { open_id: "ou_bot" } }]),
BOT_OPEN_ID,
);
expect(ctx.content).toBe("hello world");
@ -103,7 +103,7 @@ describe("normalizeMentions (via parseFeishuMessageEvent)", () => {
makeEvent("@_bot_1 hi @_user_2", [
{ key: "@_bot_1", name: "Bot One", id: { open_id: "ou_bot_1" } },
{ key: "@_user_2", name: "User Two", id: { open_id: "ou_user_2" } },
]) as any,
]),
BOT_OPEN_ID,
);
expect(ctx.content).toBe(
@ -115,7 +115,7 @@ describe("normalizeMentions (via parseFeishuMessageEvent)", () => {
const ctx = parseFeishuMessageEvent(
makeEvent("@_user_1 hi", [
{ key: "@_user_1", name: "$& the user", id: { open_id: "ou_x" } },
]) as any,
]),
BOT_OPEN_ID,
);
// $ is preserved literally (no $& pattern substitution); & is not escaped in tag body
@ -126,7 +126,7 @@ describe("normalizeMentions (via parseFeishuMessageEvent)", () => {
const ctx = parseFeishuMessageEvent(
makeEvent("@_user_1 test", [
{ key: "@_user_1", name: "<script>", id: { open_id: "ou_x" } },
]) as any,
]),
BOT_OPEN_ID,
);
expect(ctx.content).toBe('<at user_id="ou_x">&lt;script&gt;</at> test');

View File

@ -2,6 +2,7 @@ import fs from "node:fs/promises";
import path from "node:path";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { resolvePreferredOpenClawTmpDir } from "../../../src/infra/tmp-openclaw-dir.js";
import type { ClawdbotConfig } from "../runtime-api.js";
const createFeishuClientMock = vi.hoisted(() => vi.fn());
const resolveFeishuAccountMock = vi.hoisted(() => vi.fn());
@ -17,6 +18,7 @@ const messageResourceGetMock = vi.hoisted(() => vi.fn());
const messageReplyMock = vi.hoisted(() => vi.fn());
const FEISHU_MEDIA_HTTP_TIMEOUT_MS = 120_000;
const emptyConfig: ClawdbotConfig = {};
vi.mock("./client.js", () => ({
createFeishuClient: createFeishuClientMock,
@ -135,7 +137,7 @@ describe("sendMediaFeishu msg_type routing", () => {
it("uses msg_type=media for mp4 video", async () => {
await sendMediaFeishu({
cfg: {} as any,
cfg: emptyConfig,
to: "user:ou_target",
mediaBuffer: Buffer.from("video"),
fileName: "clip.mp4",
@ -156,7 +158,7 @@ describe("sendMediaFeishu msg_type routing", () => {
it("uses msg_type=audio for opus", async () => {
await sendMediaFeishu({
cfg: {} as any,
cfg: emptyConfig,
to: "user:ou_target",
mediaBuffer: Buffer.from("audio"),
fileName: "voice.opus",
@ -177,7 +179,7 @@ describe("sendMediaFeishu msg_type routing", () => {
it("uses msg_type=file for documents", async () => {
await sendMediaFeishu({
cfg: {} as any,
cfg: emptyConfig,
to: "user:ou_target",
mediaBuffer: Buffer.from("doc"),
fileName: "paper.pdf",
@ -205,7 +207,7 @@ describe("sendMediaFeishu msg_type routing", () => {
});
await sendMediaFeishu({
cfg: {} as any,
cfg: emptyConfig,
to: "user:ou_target",
mediaUrl: "https://example.com/video",
});
@ -231,7 +233,7 @@ describe("sendMediaFeishu msg_type routing", () => {
});
await sendMediaFeishu({
cfg: {} as any,
cfg: emptyConfig,
to: "user:ou_target",
mediaUrl: "https://example.com/song.mp3",
});
@ -250,7 +252,7 @@ describe("sendMediaFeishu msg_type routing", () => {
it("configures the media client timeout for image uploads", async () => {
await sendMediaFeishu({
cfg: {} as any,
cfg: emptyConfig,
to: "user:ou_target",
mediaBuffer: Buffer.from("image"),
fileName: "photo.png",
@ -266,7 +268,7 @@ describe("sendMediaFeishu msg_type routing", () => {
it("uses msg_type=media when replying with mp4", async () => {
await sendMediaFeishu({
cfg: {} as any,
cfg: emptyConfig,
to: "user:ou_target",
mediaBuffer: Buffer.from("video"),
fileName: "reply.mp4",
@ -285,7 +287,7 @@ describe("sendMediaFeishu msg_type routing", () => {
it("passes reply_in_thread when replyInThread is true", async () => {
await sendMediaFeishu({
cfg: {} as any,
cfg: emptyConfig,
to: "user:ou_target",
mediaBuffer: Buffer.from("video"),
fileName: "reply.mp4",
@ -306,7 +308,7 @@ describe("sendMediaFeishu msg_type routing", () => {
it("omits reply_in_thread when replyInThread is false", async () => {
await sendMediaFeishu({
cfg: {} as any,
cfg: emptyConfig,
to: "user:ou_target",
mediaBuffer: Buffer.from("video"),
fileName: "reply.mp4",
@ -328,7 +330,7 @@ describe("sendMediaFeishu msg_type routing", () => {
const roots = ["/allowed/workspace", "/tmp/openclaw"];
await sendMediaFeishu({
cfg: {} as any,
cfg: emptyConfig,
to: "user:ou_target",
mediaUrl: "/allowed/workspace/file.pdf",
mediaLocalRoots: roots,
@ -351,7 +353,7 @@ describe("sendMediaFeishu msg_type routing", () => {
await expect(
sendMediaFeishu({
cfg: {} as any,
cfg: emptyConfig,
to: "user:ou_target",
mediaUrl: "https://x/img",
fileName: "voice.opus",
@ -375,7 +377,7 @@ describe("sendMediaFeishu msg_type routing", () => {
});
const result = await downloadImageFeishu({
cfg: {} as any,
cfg: emptyConfig,
imageKey,
});
@ -402,7 +404,7 @@ describe("sendMediaFeishu msg_type routing", () => {
});
const result = await downloadMessageResourceFeishu({
cfg: {} as any,
cfg: emptyConfig,
messageId: "om_123",
fileKey,
type: "image",
@ -416,7 +418,7 @@ describe("sendMediaFeishu msg_type routing", () => {
it("rejects invalid image keys before calling feishu api", async () => {
await expect(
downloadImageFeishu({
cfg: {} as any,
cfg: emptyConfig,
imageKey: "a/../../bad",
}),
).rejects.toThrow("invalid image_key");
@ -427,7 +429,7 @@ describe("sendMediaFeishu msg_type routing", () => {
it("rejects invalid file keys before calling feishu api", async () => {
await expect(
downloadMessageResourceFeishu({
cfg: {} as any,
cfg: emptyConfig,
messageId: "om_123",
fileKey: "x/../../bad",
type: "file",
@ -439,7 +441,7 @@ describe("sendMediaFeishu msg_type routing", () => {
it("preserves Chinese filenames for file uploads", async () => {
await sendMediaFeishu({
cfg: {} as any,
cfg: emptyConfig,
to: "user:ou_target",
mediaBuffer: Buffer.from("doc"),
fileName: "测试文档.pdf",
@ -451,7 +453,7 @@ describe("sendMediaFeishu msg_type routing", () => {
it("preserves ASCII filenames unchanged for file uploads", async () => {
await sendMediaFeishu({
cfg: {} as any,
cfg: emptyConfig,
to: "user:ou_target",
mediaBuffer: Buffer.from("doc"),
fileName: "report-2026.pdf",
@ -463,7 +465,7 @@ describe("sendMediaFeishu msg_type routing", () => {
it("preserves special Unicode characters (em-dash, full-width brackets) in filenames", async () => {
await sendMediaFeishu({
cfg: {} as any,
cfg: emptyConfig,
to: "user:ou_target",
mediaBuffer: Buffer.from("doc"),
fileName: "报告—详情2026.md",
@ -538,7 +540,7 @@ describe("downloadMessageResourceFeishu", () => {
// Audio/video resources must use type=file, not type=audio (#8746).
it("forwards provided type=file for non-image resources", async () => {
const result = await downloadMessageResourceFeishu({
cfg: {} as any,
cfg: emptyConfig,
messageId: "om_audio_msg",
fileKey: "file_key_audio",
type: "file",
@ -558,7 +560,7 @@ describe("downloadMessageResourceFeishu", () => {
messageResourceGetMock.mockResolvedValue(Buffer.from("fake-image-data"));
const result = await downloadMessageResourceFeishu({
cfg: {} as any,
cfg: emptyConfig,
messageId: "om_img_msg",
fileKey: "img_key_1",
type: "image",
@ -584,7 +586,7 @@ describe("downloadMessageResourceFeishu", () => {
});
const result = await downloadMessageResourceFeishu({
cfg: {} as any,
cfg: emptyConfig,
messageId: "om_video_msg",
fileKey: "file_key_video",
type: "file",

View File

@ -2,6 +2,7 @@ import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { ClawdbotConfig } from "../runtime-api.js";
const sendMediaFeishuMock = vi.hoisted(() => vi.fn());
const sendMessageFeishuMock = vi.hoisted(() => vi.fn());
@ -30,6 +31,14 @@ vi.mock("./runtime.js", () => ({
import { feishuOutbound } from "./outbound.js";
const sendText = feishuOutbound.sendText!;
const emptyConfig: ClawdbotConfig = {};
const cardRenderConfig: ClawdbotConfig = {
channels: {
feishu: {
renderMode: "card",
},
},
};
function resetOutboundMocks() {
vi.clearAllMocks();
@ -55,7 +64,7 @@ describe("feishuOutbound.sendText local-image auto-convert", () => {
const { dir, file } = await createTmpImage();
try {
const result = await sendText({
cfg: {} as any,
cfg: emptyConfig,
to: "chat_1",
text: file,
accountId: "main",
@ -81,7 +90,7 @@ describe("feishuOutbound.sendText local-image auto-convert", () => {
it("keeps non-path text on the text-send path", async () => {
await sendText({
cfg: {} as any,
cfg: emptyConfig,
to: "chat_1",
text: "please upload /tmp/example.png",
accountId: "main",
@ -102,7 +111,7 @@ describe("feishuOutbound.sendText local-image auto-convert", () => {
sendMediaFeishuMock.mockRejectedValueOnce(new Error("upload failed"));
try {
await sendText({
cfg: {} as any,
cfg: emptyConfig,
to: "chat_1",
text: file,
accountId: "main",
@ -123,13 +132,7 @@ describe("feishuOutbound.sendText local-image auto-convert", () => {
it("uses markdown cards when renderMode=card", async () => {
const result = await sendText({
cfg: {
channels: {
feishu: {
renderMode: "card",
},
},
} as any,
cfg: cardRenderConfig,
to: "chat_1",
text: "| a | b |\n| - | - |",
accountId: "main",
@ -148,12 +151,12 @@ describe("feishuOutbound.sendText local-image auto-convert", () => {
it("forwards replyToId as replyToMessageId on sendText", async () => {
await sendText({
cfg: {} as any,
cfg: emptyConfig,
to: "chat_1",
text: "hello",
replyToId: "om_reply_1",
accountId: "main",
} as any);
});
expect(sendMessageFeishuMock).toHaveBeenCalledWith(
expect.objectContaining({
@ -167,13 +170,13 @@ describe("feishuOutbound.sendText local-image auto-convert", () => {
it("falls back to threadId when replyToId is empty on sendText", async () => {
await sendText({
cfg: {} as any,
cfg: emptyConfig,
to: "chat_1",
text: "hello",
replyToId: " ",
threadId: "om_thread_2",
accountId: "main",
} as any);
});
expect(sendMessageFeishuMock).toHaveBeenCalledWith(
expect.objectContaining({
@ -193,7 +196,7 @@ describe("feishuOutbound.sendText replyToId forwarding", () => {
it("forwards replyToId as replyToMessageId to sendMessageFeishu", async () => {
await sendText({
cfg: {} as any,
cfg: emptyConfig,
to: "chat_1",
text: "hello",
replyToId: "om_reply_target",
@ -212,13 +215,7 @@ describe("feishuOutbound.sendText replyToId forwarding", () => {
it("forwards replyToId to sendStructuredCardFeishu when renderMode=card", async () => {
await sendText({
cfg: {
channels: {
feishu: {
renderMode: "card",
},
},
} as any,
cfg: cardRenderConfig,
to: "chat_1",
text: "```code```",
replyToId: "om_reply_target",
@ -234,7 +231,7 @@ describe("feishuOutbound.sendText replyToId forwarding", () => {
it("does not pass replyToMessageId when replyToId is absent", async () => {
await sendText({
cfg: {} as any,
cfg: emptyConfig,
to: "chat_1",
text: "hello",
accountId: "main",
@ -258,7 +255,7 @@ describe("feishuOutbound.sendMedia replyToId forwarding", () => {
it("forwards replyToId to sendMediaFeishu", async () => {
await feishuOutbound.sendMedia?.({
cfg: {} as any,
cfg: emptyConfig,
to: "chat_1",
text: "",
mediaUrl: "https://example.com/image.png",
@ -275,7 +272,7 @@ describe("feishuOutbound.sendMedia replyToId forwarding", () => {
it("forwards replyToId to text caption send", async () => {
await feishuOutbound.sendMedia?.({
cfg: {} as any,
cfg: emptyConfig,
to: "chat_1",
text: "caption text",
mediaUrl: "https://example.com/image.png",
@ -298,13 +295,7 @@ describe("feishuOutbound.sendMedia renderMode", () => {
it("uses markdown cards for captions when renderMode=card", async () => {
const result = await feishuOutbound.sendMedia?.({
cfg: {
channels: {
feishu: {
renderMode: "card",
},
},
} as any,
cfg: cardRenderConfig,
to: "chat_1",
text: "| a | b |\n| - | - |",
mediaUrl: "https://example.com/image.png",
@ -331,13 +322,13 @@ describe("feishuOutbound.sendMedia renderMode", () => {
it("uses threadId fallback as replyToMessageId on sendMedia", async () => {
await feishuOutbound.sendMedia?.({
cfg: {} as any,
cfg: emptyConfig,
to: "chat_1",
text: "caption",
mediaUrl: "https://example.com/image.png",
threadId: "om_thread_1",
accountId: "main",
} as any);
});
expect(sendMediaFeishuMock).toHaveBeenCalledWith(
expect.objectContaining({

View File

@ -3,14 +3,14 @@ import {
getRequiredHookHandler,
registerHookHandlersForTest,
} from "../../../test/helpers/extensions/subagent-hooks.js";
import type { OpenClawPluginApi } from "../runtime-api.js";
import type { ClawdbotConfig, OpenClawPluginApi } from "../runtime-api.js";
import { registerFeishuSubagentHooks } from "./subagent-hooks.js";
import {
__testing as threadBindingTesting,
createFeishuThreadBindingManager,
} from "./thread-bindings.js";
const baseConfig = {
const baseConfig: ClawdbotConfig = {
session: { mainKey: "main", scope: "per-sender" },
channels: { feishu: {} },
};
@ -38,7 +38,7 @@ describe("feishu subagent hook handlers", () => {
it("binds a Feishu DM conversation on subagent_spawning", async () => {
const handlers = registerHandlersForTest();
const handler = getRequiredHookHandler(handlers, "subagent_spawning");
createFeishuThreadBindingManager({ cfg: baseConfig as any, accountId: "work" });
createFeishuThreadBindingManager({ cfg: baseConfig, accountId: "work" });
const result = await handler(
{
@ -85,7 +85,7 @@ describe("feishu subagent hook handlers", () => {
it("preserves the original Feishu DM delivery target", async () => {
const handlers = registerHandlersForTest();
const deliveryHandler = getRequiredHookHandler(handlers, "subagent_delivery_target");
const manager = createFeishuThreadBindingManager({ cfg: baseConfig as any, accountId: "work" });
const manager = createFeishuThreadBindingManager({ cfg: baseConfig, accountId: "work" });
manager.bindConversation({
conversationId: "ou_sender_1",
@ -124,7 +124,7 @@ describe("feishu subagent hook handlers", () => {
const handlers = registerHandlersForTest();
const spawnHandler = getRequiredHookHandler(handlers, "subagent_spawning");
const deliveryHandler = getRequiredHookHandler(handlers, "subagent_delivery_target");
createFeishuThreadBindingManager({ cfg: baseConfig as any, accountId: "work" });
createFeishuThreadBindingManager({ cfg: baseConfig, accountId: "work" });
const result = await spawnHandler(
{
@ -173,7 +173,7 @@ describe("feishu subagent hook handlers", () => {
const handlers = registerHandlersForTest();
const spawnHandler = getRequiredHookHandler(handlers, "subagent_spawning");
const deliveryHandler = getRequiredHookHandler(handlers, "subagent_delivery_target");
const manager = createFeishuThreadBindingManager({ cfg: baseConfig as any, accountId: "work" });
const manager = createFeishuThreadBindingManager({ cfg: baseConfig, accountId: "work" });
manager.bindConversation({
conversationId: "oc_group_chat:topic:om_topic_root:sender:ou_sender_1",
@ -242,7 +242,7 @@ describe("feishu subagent hook handlers", () => {
const handlers = registerHandlersForTest();
const spawnHandler = getRequiredHookHandler(handlers, "subagent_spawning");
const deliveryHandler = getRequiredHookHandler(handlers, "subagent_delivery_target");
createFeishuThreadBindingManager({ cfg: baseConfig as any, accountId: "work" });
createFeishuThreadBindingManager({ cfg: baseConfig, accountId: "work" });
await spawnHandler(
{
@ -302,7 +302,7 @@ describe("feishu subagent hook handlers", () => {
const handlers = registerHandlersForTest();
const spawnHandler = getRequiredHookHandler(handlers, "subagent_spawning");
const deliveryHandler = getRequiredHookHandler(handlers, "subagent_delivery_target");
const manager = createFeishuThreadBindingManager({ cfg: baseConfig as any, accountId: "work" });
const manager = createFeishuThreadBindingManager({ cfg: baseConfig, accountId: "work" });
manager.bindConversation({
conversationId: "oc_group_chat:topic:om_topic_root:sender:ou_sender_1",
@ -365,7 +365,7 @@ describe("feishu subagent hook handlers", () => {
const handlers = registerHandlersForTest();
const spawnHandler = getRequiredHookHandler(handlers, "subagent_spawning");
const deliveryHandler = getRequiredHookHandler(handlers, "subagent_delivery_target");
const manager = createFeishuThreadBindingManager({ cfg: baseConfig as any, accountId: "work" });
const manager = createFeishuThreadBindingManager({ cfg: baseConfig, accountId: "work" });
manager.bindConversation({
conversationId: "oc_group_chat:topic:om_topic_root",
@ -495,7 +495,7 @@ describe("feishu subagent hook handlers", () => {
it("returns an error for unsupported non-topic Feishu group conversations", async () => {
const handler = getRequiredHookHandler(registerHandlersForTest(), "subagent_spawning");
createFeishuThreadBindingManager({ cfg: baseConfig as any, accountId: "work" });
createFeishuThreadBindingManager({ cfg: baseConfig, accountId: "work" });
await expect(
handler(
@ -523,7 +523,7 @@ describe("feishu subagent hook handlers", () => {
const spawnHandler = getRequiredHookHandler(handlers, "subagent_spawning");
const deliveryHandler = getRequiredHookHandler(handlers, "subagent_delivery_target");
const endedHandler = getRequiredHookHandler(handlers, "subagent_ended");
createFeishuThreadBindingManager({ cfg: baseConfig as any, accountId: "work" });
createFeishuThreadBindingManager({ cfg: baseConfig, accountId: "work" });
await spawnHandler(
{