mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-07 07:11:06 +00:00
317 lines
9.3 KiB
TypeScript
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,
|
|
}),
|
|
);
|
|
});
|
|
});
|