Verified: - pnpm build - pnpm check - pnpm test:macmini Co-authored-by: Takhoffman <781889+Takhoffman@users.noreply.github.com> Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
236 lines
6.4 KiB
TypeScript
236 lines
6.4 KiB
TypeScript
import type { ClawdbotConfig } from "openclaw/plugin-sdk";
|
|
import { describe, expect, it, vi } from "vitest";
|
|
import { resolveReactionSyntheticEvent, type FeishuReactionCreatedEvent } from "./monitor.js";
|
|
|
|
const cfg = {} as ClawdbotConfig;
|
|
|
|
function makeReactionEvent(
|
|
overrides: Partial<FeishuReactionCreatedEvent> = {},
|
|
): FeishuReactionCreatedEvent {
|
|
return {
|
|
message_id: "om_msg1",
|
|
reaction_type: { emoji_type: "THUMBSUP" },
|
|
operator_type: "user",
|
|
user_id: { open_id: "ou_user1" },
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe("resolveReactionSyntheticEvent", () => {
|
|
it("filters app self-reactions", async () => {
|
|
const event = makeReactionEvent({ operator_type: "app" });
|
|
const result = await resolveReactionSyntheticEvent({
|
|
cfg,
|
|
accountId: "default",
|
|
event,
|
|
botOpenId: "ou_bot",
|
|
});
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
it("filters Typing reactions", async () => {
|
|
const event = makeReactionEvent({ reaction_type: { emoji_type: "Typing" } });
|
|
const result = await resolveReactionSyntheticEvent({
|
|
cfg,
|
|
accountId: "default",
|
|
event,
|
|
botOpenId: "ou_bot",
|
|
});
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
it("fails closed when bot open_id is unavailable", async () => {
|
|
const event = makeReactionEvent();
|
|
const result = await resolveReactionSyntheticEvent({
|
|
cfg,
|
|
accountId: "default",
|
|
event,
|
|
});
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
it("drops reactions when reactionNotifications is off", async () => {
|
|
const event = makeReactionEvent();
|
|
const result = await resolveReactionSyntheticEvent({
|
|
cfg: {
|
|
channels: {
|
|
feishu: {
|
|
reactionNotifications: "off",
|
|
},
|
|
},
|
|
} as ClawdbotConfig,
|
|
accountId: "default",
|
|
event,
|
|
botOpenId: "ou_bot",
|
|
fetchMessage: async () => ({
|
|
messageId: "om_msg1",
|
|
chatId: "oc_group",
|
|
senderOpenId: "ou_bot",
|
|
senderType: "app",
|
|
content: "hello",
|
|
contentType: "text",
|
|
}),
|
|
});
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
it("filters reactions on non-bot messages", async () => {
|
|
const event = makeReactionEvent();
|
|
const result = await resolveReactionSyntheticEvent({
|
|
cfg,
|
|
accountId: "default",
|
|
event,
|
|
botOpenId: "ou_bot",
|
|
fetchMessage: async () => ({
|
|
messageId: "om_msg1",
|
|
chatId: "oc_group",
|
|
senderOpenId: "ou_other",
|
|
senderType: "user",
|
|
content: "hello",
|
|
contentType: "text",
|
|
}),
|
|
});
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
it("allows non-bot reactions when reactionNotifications is all", async () => {
|
|
const event = makeReactionEvent();
|
|
const result = await resolveReactionSyntheticEvent({
|
|
cfg: {
|
|
channels: {
|
|
feishu: {
|
|
reactionNotifications: "all",
|
|
},
|
|
},
|
|
} as ClawdbotConfig,
|
|
accountId: "default",
|
|
event,
|
|
botOpenId: "ou_bot",
|
|
fetchMessage: async () => ({
|
|
messageId: "om_msg1",
|
|
chatId: "oc_group",
|
|
senderOpenId: "ou_other",
|
|
senderType: "user",
|
|
content: "hello",
|
|
contentType: "text",
|
|
}),
|
|
uuid: () => "fixed-uuid",
|
|
});
|
|
expect(result?.message.message_id).toBe("om_msg1:reaction:THUMBSUP:fixed-uuid");
|
|
});
|
|
|
|
it("drops unverified reactions when sender verification times out", async () => {
|
|
const event = makeReactionEvent();
|
|
const result = await resolveReactionSyntheticEvent({
|
|
cfg,
|
|
accountId: "default",
|
|
event,
|
|
botOpenId: "ou_bot",
|
|
verificationTimeoutMs: 1,
|
|
fetchMessage: async () =>
|
|
await new Promise<never>(() => {
|
|
// Never resolves
|
|
}),
|
|
});
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
it("uses event chat context when provided", async () => {
|
|
const event = makeReactionEvent({
|
|
chat_id: "oc_group_from_event",
|
|
chat_type: "group",
|
|
});
|
|
const result = await resolveReactionSyntheticEvent({
|
|
cfg,
|
|
accountId: "default",
|
|
event,
|
|
botOpenId: "ou_bot",
|
|
fetchMessage: async () => ({
|
|
messageId: "om_msg1",
|
|
chatId: "oc_group_from_lookup",
|
|
senderOpenId: "ou_bot",
|
|
content: "hello",
|
|
contentType: "text",
|
|
}),
|
|
uuid: () => "fixed-uuid",
|
|
});
|
|
|
|
expect(result).toEqual({
|
|
sender: {
|
|
sender_id: { open_id: "ou_user1" },
|
|
sender_type: "user",
|
|
},
|
|
message: {
|
|
message_id: "om_msg1:reaction:THUMBSUP:fixed-uuid",
|
|
chat_id: "oc_group_from_event",
|
|
chat_type: "group",
|
|
message_type: "text",
|
|
content: JSON.stringify({
|
|
text: "[reacted with THUMBSUP to message om_msg1]",
|
|
}),
|
|
},
|
|
});
|
|
});
|
|
|
|
it("falls back to reacted message chat_id when event chat_id is absent", async () => {
|
|
const event = makeReactionEvent();
|
|
const result = await resolveReactionSyntheticEvent({
|
|
cfg,
|
|
accountId: "default",
|
|
event,
|
|
botOpenId: "ou_bot",
|
|
fetchMessage: async () => ({
|
|
messageId: "om_msg1",
|
|
chatId: "oc_group_from_lookup",
|
|
senderOpenId: "ou_bot",
|
|
content: "hello",
|
|
contentType: "text",
|
|
}),
|
|
uuid: () => "fixed-uuid",
|
|
});
|
|
|
|
expect(result?.message.chat_id).toBe("oc_group_from_lookup");
|
|
expect(result?.message.chat_type).toBe("p2p");
|
|
});
|
|
|
|
it("falls back to sender p2p chat when lookup returns empty chat_id", async () => {
|
|
const event = makeReactionEvent();
|
|
const result = await resolveReactionSyntheticEvent({
|
|
cfg,
|
|
accountId: "default",
|
|
event,
|
|
botOpenId: "ou_bot",
|
|
fetchMessage: async () => ({
|
|
messageId: "om_msg1",
|
|
chatId: "",
|
|
senderOpenId: "ou_bot",
|
|
content: "hello",
|
|
contentType: "text",
|
|
}),
|
|
uuid: () => "fixed-uuid",
|
|
});
|
|
|
|
expect(result?.message.chat_id).toBe("p2p:ou_user1");
|
|
expect(result?.message.chat_type).toBe("p2p");
|
|
});
|
|
|
|
it("logs and drops reactions when lookup throws", async () => {
|
|
const log = vi.fn();
|
|
const event = makeReactionEvent();
|
|
const result = await resolveReactionSyntheticEvent({
|
|
cfg,
|
|
accountId: "acct1",
|
|
event,
|
|
botOpenId: "ou_bot",
|
|
fetchMessage: async () => {
|
|
throw new Error("boom");
|
|
},
|
|
logger: log,
|
|
});
|
|
expect(result).toBeNull();
|
|
expect(log).toHaveBeenCalledWith(
|
|
expect.stringContaining("ignoring reaction on non-bot/unverified message om_msg1"),
|
|
);
|
|
});
|
|
});
|