Files
openclaw/extensions/feishu/src/comment-handler.test.ts
2026-04-03 18:49:43 +09:00

317 lines
9.3 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from "vitest";
import type { ClawdbotConfig, PluginRuntime } from "../runtime-api.js";
import { handleFeishuCommentEvent } from "./comment-handler.js";
import { setFeishuRuntime } from "./runtime.js";
const resolveDriveCommentEventTurnMock = vi.hoisted(() => vi.fn());
const createFeishuCommentReplyDispatcherMock = vi.hoisted(() => vi.fn());
const maybeCreateDynamicAgentMock = vi.hoisted(() => vi.fn());
const createFeishuClientMock = vi.hoisted(() => vi.fn(() => ({ request: vi.fn() })));
const deliverCommentThreadTextMock = vi.hoisted(() => vi.fn());
vi.mock("./monitor.comment.js", () => ({
resolveDriveCommentEventTurn: resolveDriveCommentEventTurnMock,
}));
vi.mock("./comment-dispatcher.js", () => ({
createFeishuCommentReplyDispatcher: createFeishuCommentReplyDispatcherMock,
}));
vi.mock("./dynamic-agent.js", () => ({
maybeCreateDynamicAgent: maybeCreateDynamicAgentMock,
}));
vi.mock("./client.js", () => ({
createFeishuClient: createFeishuClientMock,
}));
vi.mock("./drive.js", () => ({
deliverCommentThreadText: deliverCommentThreadTextMock,
}));
function buildConfig(overrides?: Partial<ClawdbotConfig>): ClawdbotConfig {
return {
channels: {
feishu: {
enabled: true,
dmPolicy: "open",
},
},
...overrides,
} as ClawdbotConfig;
}
function buildResolvedRoute(matchedBy: "binding.channel" | "default" = "binding.channel") {
return {
agentId: "main",
channel: "feishu",
accountId: "default",
sessionKey: "agent:main:feishu:direct:ou_sender",
mainSessionKey: "agent:main:feishu",
lastRoutePolicy: "session" as const,
matchedBy,
};
}
function createTestRuntime(overrides?: {
readAllowFromStore?: () => Promise<unknown[]>;
upsertPairingRequest?: () => Promise<{ code: string; created: boolean }>;
resolveAgentRoute?: () => ReturnType<typeof buildResolvedRoute>;
dispatchReplyFromConfig?: PluginRuntime["channel"]["reply"]["dispatchReplyFromConfig"];
withReplyDispatcher?: PluginRuntime["channel"]["reply"]["withReplyDispatcher"];
}) {
const finalizeInboundContext = vi.fn((ctx: Record<string, unknown>) => ctx);
const dispatchReplyFromConfig =
overrides?.dispatchReplyFromConfig ??
vi.fn(async () => ({
queuedFinal: true,
counts: { tool: 0, block: 0, final: 1 },
}));
const withReplyDispatcher =
overrides?.withReplyDispatcher ??
vi.fn(
async ({
run,
onSettled,
}: {
run: () => Promise<unknown>;
onSettled?: () => Promise<void> | void;
}) => {
try {
return await run();
} finally {
await onSettled?.();
}
},
);
const recordInboundSession = vi.fn(async () => {});
return {
channel: {
routing: {
buildAgentSessionKey: vi.fn(
({
agentId,
channel,
peer,
}: {
agentId: string;
channel: string;
peer?: { kind?: string; id?: string };
}) => `agent:${agentId}:${channel}:${peer?.kind ?? "direct"}:${peer?.id ?? "peer"}`,
),
resolveAgentRoute: vi.fn(overrides?.resolveAgentRoute ?? (() => buildResolvedRoute())),
},
reply: {
finalizeInboundContext,
dispatchReplyFromConfig,
withReplyDispatcher,
},
session: {
resolveStorePath: vi.fn(() => "/tmp/feishu-session-store.json"),
recordInboundSession,
},
pairing: {
readAllowFromStore: vi.fn(overrides?.readAllowFromStore ?? (async () => [])),
upsertPairingRequest: vi.fn(
overrides?.upsertPairingRequest ??
(async () => ({
code: "TESTCODE",
created: true,
})),
),
buildPairingReply: vi.fn((code: string) => `Pairing code: ${code}`),
},
},
} as unknown as PluginRuntime;
}
describe("handleFeishuCommentEvent", () => {
beforeEach(() => {
vi.clearAllMocks();
maybeCreateDynamicAgentMock.mockResolvedValue({ created: false });
resolveDriveCommentEventTurnMock.mockResolvedValue({
eventId: "evt_1",
messageId: "drive-comment:evt_1",
commentId: "comment_1",
replyId: "reply_1",
noticeType: "add_comment",
fileToken: "doc_token_1",
fileType: "docx",
isWholeComment: false,
senderId: "ou_sender",
senderUserId: "on_sender_user",
timestamp: "1774951528000",
isMentioned: true,
documentTitle: "Project review",
prompt: "prompt body",
preview: "prompt body",
rootCommentText: "root comment",
targetReplyText: "latest reply",
});
deliverCommentThreadTextMock.mockResolvedValue({
delivery_mode: "reply_comment",
reply_id: "r1",
});
const runtime = createTestRuntime();
setFeishuRuntime(runtime);
createFeishuCommentReplyDispatcherMock.mockReturnValue({
dispatcher: {
markComplete: vi.fn(),
waitForIdle: vi.fn(async () => {}),
},
replyOptions: {},
markDispatchIdle: vi.fn(),
});
});
it("records a comment-thread inbound context with a routable Feishu origin", async () => {
await handleFeishuCommentEvent({
cfg: buildConfig(),
accountId: "default",
event: { event_id: "evt_1" },
botOpenId: "ou_bot",
runtime: {
log: vi.fn(),
error: vi.fn(),
} as never,
});
const runtime = (await import("./runtime.js")).getFeishuRuntime();
const finalizeInboundContext = runtime.channel.reply.finalizeInboundContext as ReturnType<
typeof vi.fn
>;
const recordInboundSession = runtime.channel.session.recordInboundSession as ReturnType<
typeof vi.fn
>;
const dispatchReplyFromConfig = runtime.channel.reply.dispatchReplyFromConfig as ReturnType<
typeof vi.fn
>;
expect(finalizeInboundContext).toHaveBeenCalledWith(
expect.objectContaining({
From: "feishu:ou_sender",
To: "comment:docx:doc_token_1:comment_1",
Surface: "feishu-comment",
OriginatingChannel: "feishu",
OriginatingTo: "comment:docx:doc_token_1:comment_1",
MessageSid: "drive-comment:evt_1",
}),
);
expect(recordInboundSession).toHaveBeenCalledTimes(1);
expect(dispatchReplyFromConfig).toHaveBeenCalledTimes(1);
});
it("allows comment senders matched by user_id allowlist entries", async () => {
const runtime = createTestRuntime();
setFeishuRuntime(runtime);
await handleFeishuCommentEvent({
cfg: buildConfig({
channels: {
feishu: {
enabled: true,
dmPolicy: "allowlist",
allowFrom: ["on_sender_user"],
},
},
}),
accountId: "default",
event: { event_id: "evt_1" },
botOpenId: "ou_bot",
runtime: {
log: vi.fn(),
error: vi.fn(),
} as never,
});
const dispatchReplyFromConfig = runtime.channel.reply.dispatchReplyFromConfig as ReturnType<
typeof vi.fn
>;
expect(dispatchReplyFromConfig).toHaveBeenCalledTimes(1);
expect(deliverCommentThreadTextMock).not.toHaveBeenCalled();
});
it("issues a pairing challenge in the comment thread when dmPolicy=pairing", async () => {
const runtime = createTestRuntime();
setFeishuRuntime(runtime);
await handleFeishuCommentEvent({
cfg: buildConfig({
channels: {
feishu: {
enabled: true,
dmPolicy: "pairing",
allowFrom: [],
},
},
}),
accountId: "default",
event: { event_id: "evt_1" },
botOpenId: "ou_bot",
runtime: {
log: vi.fn(),
error: vi.fn(),
} as never,
});
expect(deliverCommentThreadTextMock).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
file_token: "doc_token_1",
file_type: "docx",
comment_id: "comment_1",
is_whole_comment: false,
}),
);
const dispatchReplyFromConfig = runtime.channel.reply.dispatchReplyFromConfig as ReturnType<
typeof vi.fn
>;
expect(dispatchReplyFromConfig).not.toHaveBeenCalled();
});
it("passes whole-comment metadata to the comment reply dispatcher", async () => {
resolveDriveCommentEventTurnMock.mockResolvedValueOnce({
eventId: "evt_whole",
messageId: "drive-comment:evt_whole",
commentId: "comment_whole",
replyId: "reply_whole",
noticeType: "add_reply",
fileToken: "doc_token_1",
fileType: "docx",
isWholeComment: true,
senderId: "ou_sender",
senderUserId: "on_sender_user",
timestamp: "1774951528000",
isMentioned: false,
documentTitle: "Project review",
prompt: "prompt body",
preview: "prompt body",
rootCommentText: "root comment",
targetReplyText: "reply text",
});
await handleFeishuCommentEvent({
cfg: buildConfig(),
accountId: "default",
event: { event_id: "evt_whole" },
botOpenId: "ou_bot",
runtime: {
log: vi.fn(),
error: vi.fn(),
} as never,
});
expect(createFeishuCommentReplyDispatcherMock).toHaveBeenCalledWith(
expect.objectContaining({
commentId: "comment_whole",
fileToken: "doc_token_1",
fileType: "docx",
isWholeComment: true,
}),
);
});
});