mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-09 08:11:09 +00:00
feat: feishu comment event (#58497)
Merged via squash.
Prepared head SHA: a9dfeb0d62
Co-authored-by: wittam-01 <271711640+wittam-01@users.noreply.github.com>
Co-authored-by: odysseus0 <8635094+odysseus0@users.noreply.github.com>
Reviewed-by: @odysseus0
This commit is contained in:
248
extensions/feishu/src/comment-handler.test.ts
Normal file
248
extensions/feishu/src/comment-handler.test.ts
Normal file
@@ -0,0 +1,248 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { createPluginRuntimeMock } from "../../../test/helpers/plugins/plugin-runtime-mock.js";
|
||||
import type { ClawdbotConfig } 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 replyCommentMock = 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", () => ({
|
||||
replyComment: replyCommentMock,
|
||||
}));
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
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",
|
||||
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",
|
||||
});
|
||||
replyCommentMock.mockResolvedValue({ reply_id: "r1" });
|
||||
|
||||
const runtime = createPluginRuntimeMock({
|
||||
channel: {
|
||||
routing: {
|
||||
resolveAgentRoute: vi.fn(() => buildResolvedRoute()),
|
||||
},
|
||||
reply: {
|
||||
dispatchReplyFromConfig: vi.fn(async () => ({
|
||||
queuedFinal: true,
|
||||
counts: { tool: 0, block: 0, final: 1 },
|
||||
})),
|
||||
withReplyDispatcher: vi.fn(async ({ run, onSettled }) => {
|
||||
try {
|
||||
return await run();
|
||||
} finally {
|
||||
await onSettled?.();
|
||||
}
|
||||
}),
|
||||
},
|
||||
},
|
||||
});
|
||||
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 = createPluginRuntimeMock({
|
||||
channel: {
|
||||
pairing: {
|
||||
readAllowFromStore: vi.fn(async () => []),
|
||||
},
|
||||
routing: {
|
||||
resolveAgentRoute: vi.fn(() => buildResolvedRoute()),
|
||||
},
|
||||
reply: {
|
||||
dispatchReplyFromConfig: vi.fn(async () => ({
|
||||
queuedFinal: true,
|
||||
counts: { tool: 0, block: 0, final: 1 },
|
||||
})),
|
||||
withReplyDispatcher: vi.fn(async ({ run, onSettled }) => {
|
||||
try {
|
||||
return await run();
|
||||
} finally {
|
||||
await onSettled?.();
|
||||
}
|
||||
}),
|
||||
},
|
||||
},
|
||||
});
|
||||
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(replyCommentMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("issues a pairing challenge in the comment thread when dmPolicy=pairing", async () => {
|
||||
const runtime = createPluginRuntimeMock({
|
||||
channel: {
|
||||
pairing: {
|
||||
readAllowFromStore: vi.fn(async () => []),
|
||||
upsertPairingRequest: vi.fn(async () => ({ code: "TESTCODE", created: true })),
|
||||
},
|
||||
routing: {
|
||||
resolveAgentRoute: vi.fn(() => buildResolvedRoute()),
|
||||
},
|
||||
},
|
||||
});
|
||||
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(replyCommentMock).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.objectContaining({
|
||||
file_token: "doc_token_1",
|
||||
file_type: "docx",
|
||||
comment_id: "comment_1",
|
||||
}),
|
||||
);
|
||||
const dispatchReplyFromConfig = runtime.channel.reply.dispatchReplyFromConfig as ReturnType<
|
||||
typeof vi.fn
|
||||
>;
|
||||
expect(dispatchReplyFromConfig).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user