mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-05 06:11:24 +00:00
425 lines
11 KiB
TypeScript
425 lines
11 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
import type { ClawdbotConfig } from "../runtime-api.js";
|
|
|
|
const {
|
|
mockConvertMarkdownTables,
|
|
mockClientGet,
|
|
mockClientList,
|
|
mockClientPatch,
|
|
mockCreateFeishuClient,
|
|
mockResolveMarkdownTableMode,
|
|
mockResolveFeishuAccount,
|
|
mockRuntimeConvertMarkdownTables,
|
|
mockRuntimeResolveMarkdownTableMode,
|
|
} = vi.hoisted(() => ({
|
|
mockConvertMarkdownTables: vi.fn((text: string) => text),
|
|
mockClientGet: vi.fn(),
|
|
mockClientList: vi.fn(),
|
|
mockClientPatch: vi.fn(),
|
|
mockCreateFeishuClient: vi.fn(),
|
|
mockResolveMarkdownTableMode: vi.fn(() => "preserve"),
|
|
mockResolveFeishuAccount: vi.fn(),
|
|
mockRuntimeConvertMarkdownTables: vi.fn((text: string) => text),
|
|
mockRuntimeResolveMarkdownTableMode: vi.fn(() => "preserve"),
|
|
}));
|
|
|
|
vi.mock("openclaw/plugin-sdk/config-runtime", () => ({
|
|
resolveMarkdownTableMode: mockResolveMarkdownTableMode,
|
|
}));
|
|
|
|
vi.mock("openclaw/plugin-sdk/text-runtime", () => ({
|
|
convertMarkdownTables: mockConvertMarkdownTables,
|
|
}));
|
|
|
|
vi.mock("./client.js", () => ({
|
|
createFeishuClient: mockCreateFeishuClient,
|
|
}));
|
|
|
|
vi.mock("./accounts.js", () => ({
|
|
resolveFeishuAccount: mockResolveFeishuAccount,
|
|
resolveFeishuRuntimeAccount: mockResolveFeishuAccount,
|
|
}));
|
|
|
|
vi.mock("./runtime.js", () => ({
|
|
getFeishuRuntime: () => ({
|
|
channel: {
|
|
text: {
|
|
resolveMarkdownTableMode: mockRuntimeResolveMarkdownTableMode,
|
|
convertMarkdownTables: mockRuntimeConvertMarkdownTables,
|
|
},
|
|
},
|
|
}),
|
|
}));
|
|
|
|
let buildStructuredCard: typeof import("./send.js").buildStructuredCard;
|
|
let editMessageFeishu: typeof import("./send.js").editMessageFeishu;
|
|
let getMessageFeishu: typeof import("./send.js").getMessageFeishu;
|
|
let listFeishuThreadMessages: typeof import("./send.js").listFeishuThreadMessages;
|
|
let resolveFeishuCardTemplate: typeof import("./send.js").resolveFeishuCardTemplate;
|
|
let sendMessageFeishu: typeof import("./send.js").sendMessageFeishu;
|
|
|
|
describe("getMessageFeishu", () => {
|
|
beforeEach(async () => {
|
|
vi.resetModules();
|
|
({
|
|
buildStructuredCard,
|
|
editMessageFeishu,
|
|
getMessageFeishu,
|
|
listFeishuThreadMessages,
|
|
resolveFeishuCardTemplate,
|
|
sendMessageFeishu,
|
|
} = await import("./send.js"));
|
|
vi.clearAllMocks();
|
|
mockResolveMarkdownTableMode.mockReturnValue("preserve");
|
|
mockConvertMarkdownTables.mockImplementation((text: string) => text);
|
|
mockRuntimeResolveMarkdownTableMode.mockReturnValue("preserve");
|
|
mockRuntimeConvertMarkdownTables.mockImplementation((text: string) => text);
|
|
mockResolveFeishuAccount.mockReturnValue({
|
|
accountId: "default",
|
|
configured: true,
|
|
});
|
|
mockCreateFeishuClient.mockReturnValue({
|
|
im: {
|
|
message: {
|
|
create: vi.fn(),
|
|
get: mockClientGet,
|
|
list: mockClientList,
|
|
patch: mockClientPatch,
|
|
},
|
|
},
|
|
});
|
|
});
|
|
|
|
it("sends text without requiring Feishu runtime text helpers", async () => {
|
|
mockRuntimeResolveMarkdownTableMode.mockImplementation(() => {
|
|
throw new Error("Feishu runtime not initialized");
|
|
});
|
|
mockRuntimeConvertMarkdownTables.mockImplementation(() => {
|
|
throw new Error("Feishu runtime not initialized");
|
|
});
|
|
mockClientPatch.mockResolvedValueOnce({ code: 0 });
|
|
mockCreateFeishuClient.mockReturnValue({
|
|
im: {
|
|
message: {
|
|
create: vi.fn().mockResolvedValue({ code: 0, data: { message_id: "om_send" } }),
|
|
reply: vi.fn(),
|
|
get: mockClientGet,
|
|
list: mockClientList,
|
|
patch: mockClientPatch,
|
|
},
|
|
},
|
|
});
|
|
|
|
const result = await sendMessageFeishu({
|
|
cfg: {} as ClawdbotConfig,
|
|
to: "oc_send",
|
|
text: "hello",
|
|
});
|
|
|
|
expect(mockResolveMarkdownTableMode).toHaveBeenCalledWith({
|
|
cfg: {},
|
|
channel: "feishu",
|
|
});
|
|
expect(mockConvertMarkdownTables).toHaveBeenCalledWith("hello", "preserve");
|
|
expect(result).toEqual({ messageId: "om_send", chatId: "oc_send" });
|
|
});
|
|
|
|
it("extracts text content from interactive card elements", async () => {
|
|
mockClientGet.mockResolvedValueOnce({
|
|
code: 0,
|
|
data: {
|
|
items: [
|
|
{
|
|
message_id: "om_1",
|
|
chat_id: "oc_1",
|
|
msg_type: "interactive",
|
|
body: {
|
|
content: JSON.stringify({
|
|
elements: [
|
|
{ tag: "markdown", content: "hello markdown" },
|
|
{ tag: "div", text: { content: "hello div" } },
|
|
],
|
|
}),
|
|
},
|
|
},
|
|
],
|
|
},
|
|
});
|
|
|
|
const result = await getMessageFeishu({
|
|
cfg: {} as ClawdbotConfig,
|
|
messageId: "om_1",
|
|
});
|
|
|
|
expect(result).toEqual(
|
|
expect.objectContaining({
|
|
messageId: "om_1",
|
|
chatId: "oc_1",
|
|
contentType: "interactive",
|
|
content: "hello markdown\nhello div",
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("extracts text content from post messages", async () => {
|
|
mockClientGet.mockResolvedValueOnce({
|
|
code: 0,
|
|
data: {
|
|
items: [
|
|
{
|
|
message_id: "om_post",
|
|
chat_id: "oc_post",
|
|
msg_type: "post",
|
|
body: {
|
|
content: JSON.stringify({
|
|
zh_cn: {
|
|
title: "Summary",
|
|
content: [[{ tag: "text", text: "post body" }]],
|
|
},
|
|
}),
|
|
},
|
|
},
|
|
],
|
|
},
|
|
});
|
|
|
|
const result = await getMessageFeishu({
|
|
cfg: {} as ClawdbotConfig,
|
|
messageId: "om_post",
|
|
});
|
|
|
|
expect(result).toEqual(
|
|
expect.objectContaining({
|
|
messageId: "om_post",
|
|
chatId: "oc_post",
|
|
contentType: "post",
|
|
content: "Summary\n\npost body",
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("returns text placeholder instead of raw JSON for unsupported message types", async () => {
|
|
mockClientGet.mockResolvedValueOnce({
|
|
code: 0,
|
|
data: {
|
|
items: [
|
|
{
|
|
message_id: "om_file",
|
|
chat_id: "oc_file",
|
|
msg_type: "file",
|
|
body: {
|
|
content: JSON.stringify({ file_key: "file_v3_123" }),
|
|
},
|
|
},
|
|
],
|
|
},
|
|
});
|
|
|
|
const result = await getMessageFeishu({
|
|
cfg: {} as ClawdbotConfig,
|
|
messageId: "om_file",
|
|
});
|
|
|
|
expect(result).toEqual(
|
|
expect.objectContaining({
|
|
messageId: "om_file",
|
|
chatId: "oc_file",
|
|
contentType: "file",
|
|
content: "[file message]",
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("supports single-object response shape from Feishu API", async () => {
|
|
mockClientGet.mockResolvedValueOnce({
|
|
code: 0,
|
|
data: {
|
|
message_id: "om_single",
|
|
chat_id: "oc_single",
|
|
msg_type: "text",
|
|
body: {
|
|
content: JSON.stringify({ text: "single payload" }),
|
|
},
|
|
},
|
|
});
|
|
|
|
const result = await getMessageFeishu({
|
|
cfg: {} as ClawdbotConfig,
|
|
messageId: "om_single",
|
|
});
|
|
|
|
expect(result).toEqual(
|
|
expect.objectContaining({
|
|
messageId: "om_single",
|
|
chatId: "oc_single",
|
|
contentType: "text",
|
|
content: "single payload",
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("reuses the same content parsing for thread history messages", async () => {
|
|
mockClientList.mockResolvedValueOnce({
|
|
code: 0,
|
|
data: {
|
|
items: [
|
|
{
|
|
message_id: "om_root",
|
|
msg_type: "text",
|
|
body: {
|
|
content: JSON.stringify({ text: "root starter" }),
|
|
},
|
|
},
|
|
{
|
|
message_id: "om_card",
|
|
msg_type: "interactive",
|
|
body: {
|
|
content: JSON.stringify({
|
|
body: {
|
|
elements: [{ tag: "markdown", content: "hello from card 2.0" }],
|
|
},
|
|
}),
|
|
},
|
|
sender: {
|
|
id: "app_1",
|
|
sender_type: "app",
|
|
},
|
|
create_time: "1710000000000",
|
|
},
|
|
{
|
|
message_id: "om_file",
|
|
msg_type: "file",
|
|
body: {
|
|
content: JSON.stringify({ file_key: "file_v3_123" }),
|
|
},
|
|
sender: {
|
|
id: "ou_1",
|
|
sender_type: "user",
|
|
},
|
|
create_time: "1710000001000",
|
|
},
|
|
],
|
|
},
|
|
});
|
|
|
|
const result = await listFeishuThreadMessages({
|
|
cfg: {} as ClawdbotConfig,
|
|
threadId: "omt_1",
|
|
rootMessageId: "om_root",
|
|
});
|
|
|
|
expect(result).toEqual([
|
|
expect.objectContaining({
|
|
messageId: "om_file",
|
|
contentType: "file",
|
|
content: "[file message]",
|
|
}),
|
|
expect.objectContaining({
|
|
messageId: "om_card",
|
|
contentType: "interactive",
|
|
content: "hello from card 2.0",
|
|
}),
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe("editMessageFeishu", () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
mockResolveFeishuAccount.mockReturnValue({
|
|
accountId: "default",
|
|
configured: true,
|
|
});
|
|
mockCreateFeishuClient.mockReturnValue({
|
|
im: {
|
|
message: {
|
|
patch: mockClientPatch,
|
|
},
|
|
},
|
|
});
|
|
});
|
|
|
|
it("patches post content for text edits", async () => {
|
|
mockRuntimeResolveMarkdownTableMode.mockImplementation(() => {
|
|
throw new Error("Feishu runtime not initialized");
|
|
});
|
|
mockRuntimeConvertMarkdownTables.mockImplementation(() => {
|
|
throw new Error("Feishu runtime not initialized");
|
|
});
|
|
mockClientPatch.mockResolvedValueOnce({ code: 0 });
|
|
|
|
const result = await editMessageFeishu({
|
|
cfg: {} as ClawdbotConfig,
|
|
messageId: "om_edit",
|
|
text: "updated body",
|
|
});
|
|
|
|
expect(mockClientPatch).toHaveBeenCalledWith({
|
|
path: { message_id: "om_edit" },
|
|
data: {
|
|
content: JSON.stringify({
|
|
zh_cn: {
|
|
content: [
|
|
[
|
|
{
|
|
tag: "md",
|
|
text: "updated body",
|
|
},
|
|
],
|
|
],
|
|
},
|
|
}),
|
|
},
|
|
});
|
|
expect(result).toEqual({ messageId: "om_edit", contentType: "post" });
|
|
});
|
|
|
|
it("patches interactive content for card edits", async () => {
|
|
mockClientPatch.mockResolvedValueOnce({ code: 0 });
|
|
|
|
const result = await editMessageFeishu({
|
|
cfg: {} as ClawdbotConfig,
|
|
messageId: "om_card",
|
|
card: { schema: "2.0" },
|
|
});
|
|
|
|
expect(mockClientPatch).toHaveBeenCalledWith({
|
|
path: { message_id: "om_card" },
|
|
data: {
|
|
content: JSON.stringify({ schema: "2.0" }),
|
|
},
|
|
});
|
|
expect(result).toEqual({ messageId: "om_card", contentType: "interactive" });
|
|
});
|
|
});
|
|
|
|
describe("resolveFeishuCardTemplate", () => {
|
|
it("accepts supported Feishu templates", () => {
|
|
expect(resolveFeishuCardTemplate(" purple ")).toBe("purple");
|
|
});
|
|
|
|
it("drops unsupported free-form identity themes", () => {
|
|
expect(resolveFeishuCardTemplate("space lobster")).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe("buildStructuredCard", () => {
|
|
it("falls back to blue when the header template is unsupported", () => {
|
|
const card = buildStructuredCard("hello", {
|
|
header: {
|
|
title: "Agent",
|
|
template: "space lobster",
|
|
},
|
|
});
|
|
|
|
expect(card).toEqual(
|
|
expect.objectContaining({
|
|
header: {
|
|
title: { tag: "plain_text", content: "Agent" },
|
|
template: "blue",
|
|
},
|
|
}),
|
|
);
|
|
});
|
|
});
|