openclaw/extensions/feishu/src/channel.test.ts

951 lines
27 KiB
TypeScript

import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../runtime-api.js";
import { createFeishuCardInteractionEnvelope } from "./card-interaction.js";
import { looksLikeFeishuId, normalizeFeishuTarget, resolveReceiveIdType } from "./targets.js";
const probeFeishuMock = vi.hoisted(() => vi.fn());
const createFeishuClientMock = vi.hoisted(() => vi.fn());
const addReactionFeishuMock = vi.hoisted(() => vi.fn());
const listReactionsFeishuMock = vi.hoisted(() => vi.fn());
const removeReactionFeishuMock = vi.hoisted(() => vi.fn());
const sendCardFeishuMock = vi.hoisted(() => vi.fn());
const sendMessageFeishuMock = vi.hoisted(() => vi.fn());
const getMessageFeishuMock = vi.hoisted(() => vi.fn());
const editMessageFeishuMock = vi.hoisted(() => vi.fn());
const createPinFeishuMock = vi.hoisted(() => vi.fn());
const listPinsFeishuMock = vi.hoisted(() => vi.fn());
const removePinFeishuMock = vi.hoisted(() => vi.fn());
const getChatInfoMock = vi.hoisted(() => vi.fn());
const getChatMembersMock = vi.hoisted(() => vi.fn());
const getFeishuMemberInfoMock = vi.hoisted(() => vi.fn());
const listFeishuDirectoryPeersLiveMock = vi.hoisted(() => vi.fn());
const listFeishuDirectoryGroupsLiveMock = vi.hoisted(() => vi.fn());
const feishuOutboundSendMediaMock = vi.hoisted(() => vi.fn());
vi.mock("./probe.js", () => ({
probeFeishu: probeFeishuMock,
}));
vi.mock("./client.js", () => ({
createFeishuClient: createFeishuClientMock,
}));
vi.mock("./channel.runtime.js", () => ({
feishuChannelRuntime: {
addReactionFeishu: addReactionFeishuMock,
createPinFeishu: createPinFeishuMock,
editMessageFeishu: editMessageFeishuMock,
getChatInfo: getChatInfoMock,
getChatMembers: getChatMembersMock,
getFeishuMemberInfo: getFeishuMemberInfoMock,
getMessageFeishu: getMessageFeishuMock,
listFeishuDirectoryGroupsLive: listFeishuDirectoryGroupsLiveMock,
listFeishuDirectoryPeersLive: listFeishuDirectoryPeersLiveMock,
listPinsFeishu: listPinsFeishuMock,
listReactionsFeishu: listReactionsFeishuMock,
probeFeishu: probeFeishuMock,
removePinFeishu: removePinFeishuMock,
removeReactionFeishu: removeReactionFeishuMock,
sendCardFeishu: sendCardFeishuMock,
sendMessageFeishu: sendMessageFeishuMock,
feishuOutbound: {
sendText: vi.fn(),
sendMedia: feishuOutboundSendMediaMock,
},
},
}));
vi.mock("../../../src/channels/plugins/bundled.js", () => ({
bundledChannelPlugins: [],
bundledChannelSetupPlugins: [],
}));
let feishuPlugin: typeof import("./channel.js").feishuPlugin;
function getDescribedActions(cfg: OpenClawConfig, accountId?: string): string[] {
return [...(feishuPlugin.actions?.describeMessageTool?.({ cfg, accountId })?.actions ?? [])];
}
function createLegacyFeishuButtonCard(value: { command?: string; text?: string }) {
return {
schema: "2.0",
body: {
elements: [
{
tag: "action",
actions: [
{
tag: "button",
text: { tag: "plain_text", content: "Run /new" },
value,
},
],
},
],
},
};
}
async function expectLegacyFeishuCardPayloadRejected(cfg: OpenClawConfig, card: unknown) {
await expect(
feishuPlugin.actions?.handleAction?.({
action: "send",
params: { to: "chat:oc_group_1", card },
cfg,
accountId: undefined,
toolContext: {},
} as never),
).rejects.toThrow(
"Feishu card buttons that trigger text or commands must use structured interaction envelopes.",
);
expect(sendCardFeishuMock).not.toHaveBeenCalled();
}
beforeAll(async () => {
({ feishuPlugin } = await import("./channel.js"));
});
describe("feishuPlugin.status.probeAccount", () => {
it("uses current account credentials for multi-account config", async () => {
const cfg = {
channels: {
feishu: {
enabled: true,
accounts: {
main: {
appId: "cli_main",
appSecret: "secret_main",
enabled: true,
},
},
},
},
} as OpenClawConfig;
const account = feishuPlugin.config.resolveAccount(cfg, "main");
probeFeishuMock.mockResolvedValueOnce({ ok: true, appId: "cli_main" });
const result = await feishuPlugin.status?.probeAccount?.({
account,
timeoutMs: 1_000,
cfg,
});
expect(probeFeishuMock).toHaveBeenCalledTimes(1);
expect(probeFeishuMock).toHaveBeenCalledWith(
expect.objectContaining({
accountId: "main",
appId: "cli_main",
appSecret: "secret_main",
}),
);
expect(result).toMatchObject({ ok: true, appId: "cli_main" });
});
});
describe("feishuPlugin.pairing.notifyApproval", () => {
beforeEach(() => {
sendMessageFeishuMock.mockReset();
sendMessageFeishuMock.mockResolvedValue({ messageId: "pairing-msg", chatId: "ou_user" });
});
it("preserves accountId when sending pairing approvals", async () => {
const cfg = {
channels: {
feishu: {
accounts: {
work: {
appId: "cli_work",
appSecret: "secret_work",
enabled: true,
},
},
},
},
} as OpenClawConfig;
await feishuPlugin.pairing?.notifyApproval?.({
cfg,
id: "ou_user",
accountId: "work",
});
expect(sendMessageFeishuMock).toHaveBeenCalledWith(
expect.objectContaining({
cfg,
to: "ou_user",
accountId: "work",
}),
);
});
});
describe("feishuPlugin messaging", () => {
it("owns sender/topic session inheritance candidates", () => {
expect(
feishuPlugin.messaging?.resolveSessionConversation?.({
kind: "group",
rawId: "oc_group_chat:topic:om_topic_root:sender:ou_topic_user",
}),
).toEqual({
id: "oc_group_chat:topic:om_topic_root:sender:ou_topic_user",
baseConversationId: "oc_group_chat",
parentConversationCandidates: ["oc_group_chat:topic:om_topic_root", "oc_group_chat"],
});
expect(
feishuPlugin.messaging?.resolveSessionConversation?.({
kind: "group",
rawId: "oc_group_chat:topic:om_topic_root",
}),
).toEqual({
id: "oc_group_chat:topic:om_topic_root",
baseConversationId: "oc_group_chat",
parentConversationCandidates: ["oc_group_chat"],
});
expect(
feishuPlugin.messaging?.resolveSessionConversation?.({
kind: "group",
rawId: "oc_group_chat:Topic:om_topic_root:Sender:ou_topic_user",
}),
).toEqual({
id: "oc_group_chat:topic:om_topic_root:sender:ou_topic_user",
baseConversationId: "oc_group_chat",
parentConversationCandidates: ["oc_group_chat:topic:om_topic_root", "oc_group_chat"],
});
});
});
describe("feishuPlugin actions", () => {
const cfg = {
channels: {
feishu: {
enabled: true,
appId: "cli_main",
appSecret: "secret_main",
actions: {
reactions: true,
},
},
},
} as OpenClawConfig;
beforeEach(() => {
vi.clearAllMocks();
createFeishuClientMock.mockReturnValue({ tag: "client" });
});
it("advertises the expanded Feishu action surface", () => {
expect(getDescribedActions(cfg)).toEqual([
"send",
"read",
"edit",
"thread-reply",
"pin",
"list-pins",
"unpin",
"member-info",
"channel-info",
"channel-list",
"react",
"reactions",
]);
});
it("does not advertise reactions when disabled via actions config", () => {
const disabledCfg = {
channels: {
feishu: {
enabled: true,
appId: "cli_main",
appSecret: "secret_main",
actions: {
reactions: false,
},
},
},
} as OpenClawConfig;
expect(getDescribedActions(disabledCfg)).toEqual([
"send",
"read",
"edit",
"thread-reply",
"pin",
"list-pins",
"unpin",
"member-info",
"channel-info",
"channel-list",
]);
});
it("honors the selected Feishu account during discovery", () => {
const cfg = {
channels: {
feishu: {
enabled: true,
actions: { reactions: false },
accounts: {
default: {
enabled: true,
appId: "cli_main",
appSecret: "secret_main",
actions: { reactions: false },
},
work: {
enabled: true,
appId: "cli_work",
appSecret: "secret_work",
actions: { reactions: true },
},
},
},
},
} as OpenClawConfig;
expect(getDescribedActions(cfg, "default")).toEqual([
"send",
"read",
"edit",
"thread-reply",
"pin",
"list-pins",
"unpin",
"member-info",
"channel-info",
"channel-list",
]);
expect(getDescribedActions(cfg, "work")).toEqual([
"send",
"read",
"edit",
"thread-reply",
"pin",
"list-pins",
"unpin",
"member-info",
"channel-info",
"channel-list",
"react",
"reactions",
]);
});
it("sends text messages", async () => {
sendMessageFeishuMock.mockResolvedValueOnce({ messageId: "om_sent", chatId: "oc_group_1" });
const result = await feishuPlugin.actions?.handleAction?.({
action: "send",
params: { to: "chat:oc_group_1", message: "hello" },
cfg,
accountId: undefined,
toolContext: {},
} as never);
expect(sendMessageFeishuMock).toHaveBeenCalledWith({
cfg,
to: "chat:oc_group_1",
text: "hello",
accountId: undefined,
replyToMessageId: undefined,
replyInThread: false,
});
expect(result?.details).toMatchObject({ ok: true, messageId: "om_sent", chatId: "oc_group_1" });
});
it("sends card messages", async () => {
sendCardFeishuMock.mockResolvedValueOnce({ messageId: "om_card", chatId: "oc_group_1" });
const result = await feishuPlugin.actions?.handleAction?.({
action: "send",
params: { to: "chat:oc_group_1", card: { schema: "2.0" } },
cfg,
accountId: undefined,
toolContext: {},
} as never);
expect(sendCardFeishuMock).toHaveBeenCalledWith({
cfg,
to: "chat:oc_group_1",
card: { schema: "2.0" },
accountId: undefined,
replyToMessageId: undefined,
replyInThread: false,
});
expect(result?.details).toMatchObject({ ok: true, messageId: "om_card", chatId: "oc_group_1" });
});
it("allows structured card button payloads", async () => {
sendCardFeishuMock.mockResolvedValueOnce({ messageId: "om_card", chatId: "oc_group_1" });
const card = {
schema: "2.0",
body: {
elements: [
{
tag: "action",
actions: [
{
tag: "button",
text: { tag: "plain_text", content: "Run /new" },
value: createFeishuCardInteractionEnvelope({
k: "quick",
a: "feishu.quick_actions.help",
q: "/help",
c: { u: "u123", h: "oc_group_1", t: "group", e: Date.now() + 60_000 },
}),
},
],
},
],
},
};
await feishuPlugin.actions?.handleAction?.({
action: "send",
params: { to: "chat:oc_group_1", card },
cfg,
accountId: undefined,
toolContext: {},
} as never);
expect(sendCardFeishuMock).toHaveBeenCalledWith(
expect.objectContaining({
card,
}),
);
});
it("rejects raw legacy card command payloads", async () => {
await expectLegacyFeishuCardPayloadRejected(
cfg,
createLegacyFeishuButtonCard({ command: "/new" }),
);
});
it("rejects raw legacy card text payloads", async () => {
await expectLegacyFeishuCardPayloadRejected(
cfg,
createLegacyFeishuButtonCard({ text: "/new" }),
);
});
it("allows non-button controls to carry text metadata values", async () => {
sendCardFeishuMock.mockResolvedValueOnce({ messageId: "om_card", chatId: "oc_group_1" });
const card = {
schema: "2.0",
body: {
elements: [
{
tag: "action",
actions: [
{
tag: "select_static",
placeholder: { tag: "plain_text", content: "Pick one" },
value: { text: "display-only metadata" },
options: [
{
text: { tag: "plain_text", content: "Option A" },
value: "a",
},
],
},
],
},
],
},
};
await feishuPlugin.actions?.handleAction?.({
action: "send",
params: { to: "chat:oc_group_1", card },
cfg,
accountId: undefined,
toolContext: {},
} as never);
expect(sendCardFeishuMock).toHaveBeenCalledWith(
expect.objectContaining({
card,
}),
);
});
it("sends media through the outbound adapter", async () => {
feishuOutboundSendMediaMock.mockResolvedValueOnce({
channel: "feishu",
messageId: "om_media",
details: { messageId: "om_media", chatId: "oc_group_1" },
});
const result = await feishuPlugin.actions?.handleAction?.({
action: "send",
params: {
to: "chat:oc_group_1",
message: "test",
media: "/tmp/image.png",
},
cfg,
accountId: undefined,
toolContext: {},
mediaLocalRoots: ["/tmp"],
} as never);
expect(feishuOutboundSendMediaMock).toHaveBeenCalledWith({
cfg,
to: "chat:oc_group_1",
text: "test",
mediaUrl: "/tmp/image.png",
accountId: undefined,
mediaLocalRoots: ["/tmp"],
replyToId: undefined,
});
expect(result?.details).toMatchObject({ messageId: "om_media" });
});
it("reads messages", async () => {
getMessageFeishuMock.mockResolvedValueOnce({
messageId: "om_1",
content: "hello",
contentType: "text",
});
const result = await feishuPlugin.actions?.handleAction?.({
action: "read",
params: { messageId: "om_1" },
cfg,
accountId: undefined,
} as never);
expect(getMessageFeishuMock).toHaveBeenCalledWith({
cfg,
messageId: "om_1",
accountId: undefined,
});
expect(result?.details).toMatchObject({
ok: true,
message: expect.objectContaining({ messageId: "om_1", content: "hello" }),
});
});
it("returns an error result when message reads fail", async () => {
getMessageFeishuMock.mockResolvedValueOnce(null);
const result = await feishuPlugin.actions?.handleAction?.({
action: "read",
params: { messageId: "om_missing" },
cfg,
accountId: undefined,
} as never);
expect((result as { isError?: boolean } | undefined)?.isError).toBe(true);
expect(result?.details).toEqual({
error: "Feishu read failed or message not found: om_missing",
});
});
it("edits messages", async () => {
editMessageFeishuMock.mockResolvedValueOnce({ messageId: "om_2", contentType: "post" });
const result = await feishuPlugin.actions?.handleAction?.({
action: "edit",
params: { messageId: "om_2", text: "updated" },
cfg,
accountId: undefined,
} as never);
expect(editMessageFeishuMock).toHaveBeenCalledWith({
cfg,
messageId: "om_2",
text: "updated",
card: undefined,
accountId: undefined,
});
expect(result?.details).toMatchObject({ ok: true, messageId: "om_2", contentType: "post" });
});
it("sends explicit thread replies with reply_in_thread semantics", async () => {
sendMessageFeishuMock.mockResolvedValueOnce({ messageId: "om_reply", chatId: "oc_group_1" });
const result = await feishuPlugin.actions?.handleAction?.({
action: "thread-reply",
params: { to: "chat:oc_group_1", messageId: "om_parent", text: "reply body" },
cfg,
accountId: undefined,
toolContext: {},
} as never);
expect(sendMessageFeishuMock).toHaveBeenCalledWith({
cfg,
to: "chat:oc_group_1",
text: "reply body",
accountId: undefined,
replyToMessageId: "om_parent",
replyInThread: true,
});
expect(result?.details).toMatchObject({
ok: true,
action: "thread-reply",
messageId: "om_reply",
});
});
it("creates pins", async () => {
createPinFeishuMock.mockResolvedValueOnce({ messageId: "om_pin", chatId: "oc_group_1" });
const result = await feishuPlugin.actions?.handleAction?.({
action: "pin",
params: { messageId: "om_pin" },
cfg,
accountId: undefined,
} as never);
expect(createPinFeishuMock).toHaveBeenCalledWith({
cfg,
messageId: "om_pin",
accountId: undefined,
});
expect(result?.details).toMatchObject({
ok: true,
pin: expect.objectContaining({ messageId: "om_pin" }),
});
});
it("lists pins", async () => {
listPinsFeishuMock.mockResolvedValueOnce({
chatId: "oc_group_1",
pins: [{ messageId: "om_pin" }],
hasMore: false,
pageToken: undefined,
});
const result = await feishuPlugin.actions?.handleAction?.({
action: "list-pins",
params: { chatId: "oc_group_1" },
cfg,
accountId: undefined,
toolContext: {},
} as never);
expect(listPinsFeishuMock).toHaveBeenCalledWith({
cfg,
chatId: "oc_group_1",
startTime: undefined,
endTime: undefined,
pageSize: undefined,
pageToken: undefined,
accountId: undefined,
});
expect(result?.details).toMatchObject({
ok: true,
pins: [expect.objectContaining({ messageId: "om_pin" })],
});
});
it("removes pins", async () => {
const result = await feishuPlugin.actions?.handleAction?.({
action: "unpin",
params: { messageId: "om_pin" },
cfg,
accountId: undefined,
} as never);
expect(removePinFeishuMock).toHaveBeenCalledWith({
cfg,
messageId: "om_pin",
accountId: undefined,
});
expect(result?.details).toMatchObject({ ok: true, messageId: "om_pin" });
});
it("fetches channel info", async () => {
getChatInfoMock.mockResolvedValueOnce({ chat_id: "oc_group_1", name: "Eng" });
const result = await feishuPlugin.actions?.handleAction?.({
action: "channel-info",
params: { chatId: "oc_group_1" },
cfg,
accountId: undefined,
toolContext: {},
} as never);
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" }),
});
});
it("fetches member lists from a chat", async () => {
getChatMembersMock.mockResolvedValueOnce({
chat_id: "oc_group_1",
members: [{ member_id: "ou_1", name: "Alice" }],
has_more: false,
});
const result = await feishuPlugin.actions?.handleAction?.({
action: "member-info",
params: { chatId: "oc_group_1" },
cfg,
accountId: undefined,
toolContext: {},
} as never);
expect(getChatMembersMock).toHaveBeenCalledWith(
{ tag: "client" },
"oc_group_1",
undefined,
undefined,
"open_id",
);
expect(result?.details).toMatchObject({
ok: true,
members: [expect.objectContaining({ member_id: "ou_1", name: "Alice" })],
});
});
it("fetches individual member info", async () => {
getFeishuMemberInfoMock.mockResolvedValueOnce({ member_id: "ou_1", name: "Alice" });
const result = await feishuPlugin.actions?.handleAction?.({
action: "member-info",
params: { memberId: "ou_1" },
cfg,
accountId: undefined,
toolContext: {},
} 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" }),
});
});
it("infers user_id lookups from the userId alias", async () => {
getFeishuMemberInfoMock.mockResolvedValueOnce({ member_id: "u_1", name: "Alice" });
await feishuPlugin.actions?.handleAction?.({
action: "member-info",
params: { userId: "u_1" },
cfg,
accountId: undefined,
toolContext: {},
} as never);
expect(getFeishuMemberInfoMock).toHaveBeenCalledWith({ tag: "client" }, "u_1", "user_id");
});
it("honors explicit open_id over alias heuristics", async () => {
getFeishuMemberInfoMock.mockResolvedValueOnce({ member_id: "u_1", name: "Alice" });
await feishuPlugin.actions?.handleAction?.({
action: "member-info",
params: { userId: "u_1", memberIdType: "open_id" },
cfg,
accountId: undefined,
toolContext: {},
} as never);
expect(getFeishuMemberInfoMock).toHaveBeenCalledWith({ tag: "client" }, "u_1", "open_id");
});
it("lists directory-backed peers and groups", async () => {
listFeishuDirectoryGroupsLiveMock.mockResolvedValueOnce([{ kind: "group", id: "oc_group_1" }]);
listFeishuDirectoryPeersLiveMock.mockResolvedValueOnce([{ kind: "user", id: "ou_1" }]);
const result = await feishuPlugin.actions?.handleAction?.({
action: "channel-list",
params: { query: "eng", limit: 5 },
cfg,
accountId: undefined,
} as never);
expect(listFeishuDirectoryGroupsLiveMock).toHaveBeenCalledWith({
cfg,
query: "eng",
limit: 5,
fallbackToStatic: false,
accountId: undefined,
});
expect(listFeishuDirectoryPeersLiveMock).toHaveBeenCalledWith({
cfg,
query: "eng",
limit: 5,
fallbackToStatic: false,
accountId: undefined,
});
expect(result?.details).toMatchObject({
ok: true,
groups: [expect.objectContaining({ id: "oc_group_1" })],
peers: [expect.objectContaining({ id: "ou_1" })],
});
});
it("fails channel-list when live discovery fails", async () => {
listFeishuDirectoryGroupsLiveMock.mockRejectedValueOnce(new Error("token expired"));
await expect(
feishuPlugin.actions?.handleAction?.({
action: "channel-list",
params: { query: "eng", limit: 5, scope: "groups" },
cfg,
accountId: undefined,
} as never),
).rejects.toThrow("token expired");
});
it("requires clearAll=true before removing all bot reactions", async () => {
await expect(
feishuPlugin.actions?.handleAction?.({
action: "react",
params: { messageId: "om_msg1" },
cfg,
accountId: undefined,
} as never),
).rejects.toThrow(
"Emoji is required to add a Feishu reaction. Set clearAll=true to remove all bot reactions.",
);
});
it("allows explicit clearAll=true when removing all bot reactions", async () => {
listReactionsFeishuMock.mockResolvedValueOnce([
{ reactionId: "r1", operatorType: "app" },
{ reactionId: "r2", operatorType: "app" },
]);
const result = await feishuPlugin.actions?.handleAction?.({
action: "react",
params: { messageId: "om_msg1", clearAll: true },
cfg,
accountId: undefined,
} as never);
expect(listReactionsFeishuMock).toHaveBeenCalledWith({
cfg,
messageId: "om_msg1",
accountId: undefined,
});
expect(removeReactionFeishuMock).toHaveBeenCalledTimes(2);
expect(result?.details).toMatchObject({ ok: true, removed: 2 });
});
it("fails for missing params on supported actions", async () => {
await expect(
feishuPlugin.actions?.handleAction?.({
action: "thread-reply",
params: { to: "chat:oc_group_1", message: "reply body" },
cfg,
accountId: undefined,
} as never),
).rejects.toThrow("Feishu thread-reply requires messageId.");
});
it("sends media-only messages without requiring card", async () => {
feishuOutboundSendMediaMock.mockResolvedValueOnce({
channel: "feishu",
messageId: "om_media_only",
details: { messageId: "om_media_only", chatId: "oc_group_1" },
});
const result = await feishuPlugin.actions?.handleAction?.({
action: "send",
params: {
to: "chat:oc_group_1",
media: "https://example.com/image.png",
},
cfg,
accountId: undefined,
toolContext: {},
mediaLocalRoots: [],
} as never);
expect(feishuOutboundSendMediaMock).toHaveBeenCalledWith(
expect.objectContaining({
to: "chat:oc_group_1",
mediaUrl: "https://example.com/image.png",
}),
);
expect(result?.details).toMatchObject({ messageId: "om_media_only" });
});
it("fails for unsupported action names", async () => {
await expect(
feishuPlugin.actions?.handleAction?.({
action: "search",
params: {},
cfg,
accountId: undefined,
} as never),
).rejects.toThrow('Unsupported Feishu action: "search"');
});
});
describe("resolveReceiveIdType", () => {
it("resolves chat IDs by oc_ prefix", () => {
expect(resolveReceiveIdType("oc_123")).toBe("chat_id");
});
it("resolves open IDs by ou_ prefix", () => {
expect(resolveReceiveIdType("ou_123")).toBe("open_id");
});
it("defaults unprefixed IDs to user_id", () => {
expect(resolveReceiveIdType("u_123")).toBe("user_id");
});
it("treats explicit group targets as chat_id", () => {
expect(resolveReceiveIdType("group:oc_123")).toBe("chat_id");
});
it("treats explicit channel targets as chat_id", () => {
expect(resolveReceiveIdType("channel:oc_123")).toBe("chat_id");
});
it("treats dm-prefixed open IDs as open_id", () => {
expect(resolveReceiveIdType("dm:ou_123")).toBe("open_id");
});
});
describe("normalizeFeishuTarget", () => {
it("strips provider and user prefixes", () => {
expect(normalizeFeishuTarget("feishu:user:ou_123")).toBe("ou_123");
expect(normalizeFeishuTarget("lark:user:ou_123")).toBe("ou_123");
});
it("strips provider and chat prefixes", () => {
expect(normalizeFeishuTarget("feishu:chat:oc_123")).toBe("oc_123");
});
it("normalizes group/channel prefixes to chat ids", () => {
expect(normalizeFeishuTarget("group:oc_123")).toBe("oc_123");
expect(normalizeFeishuTarget("feishu:group:oc_123")).toBe("oc_123");
expect(normalizeFeishuTarget("channel:oc_456")).toBe("oc_456");
expect(normalizeFeishuTarget("lark:channel:oc_456")).toBe("oc_456");
});
it("accepts provider-prefixed raw ids", () => {
expect(normalizeFeishuTarget("feishu:ou_123")).toBe("ou_123");
});
it("strips provider and dm prefixes", () => {
expect(normalizeFeishuTarget("lark:dm:ou_123")).toBe("ou_123");
});
});
describe("looksLikeFeishuId", () => {
it("accepts provider-prefixed user targets", () => {
expect(looksLikeFeishuId("feishu:user:ou_123")).toBe(true);
});
it("accepts provider-prefixed chat targets", () => {
expect(looksLikeFeishuId("lark:chat:oc_123")).toBe(true);
});
it("accepts group/channel targets", () => {
expect(looksLikeFeishuId("feishu:group:oc_123")).toBe(true);
expect(looksLikeFeishuId("group:oc_123")).toBe(true);
expect(looksLikeFeishuId("channel:oc_456")).toBe(true);
});
});