mirror of https://github.com/openclaw/openclaw.git
test(feishu): type basic fixtures
This commit is contained in:
parent
9a7c8e186e
commit
b23dc5073f
|
|
@ -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]");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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"><script></at> test');
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in New Issue