@openclaw/feishu 2026.3.13 → 2026.5.1-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/api.ts +31 -0
- package/channel-entry.ts +20 -0
- package/channel-plugin-api.ts +1 -0
- package/contract-api.ts +16 -0
- package/index.ts +70 -53
- package/openclaw.plugin.json +1653 -4
- package/package.json +32 -7
- package/runtime-api.ts +55 -0
- package/secret-contract-api.ts +5 -0
- package/security-contract-api.ts +1 -0
- package/session-key-api.ts +1 -0
- package/setup-api.ts +3 -0
- package/setup-entry.test.ts +14 -0
- package/setup-entry.ts +13 -0
- package/src/accounts.test.ts +95 -7
- package/src/accounts.ts +199 -117
- package/src/app-registration.ts +331 -0
- package/src/approval-auth.test.ts +24 -0
- package/src/approval-auth.ts +25 -0
- package/src/async.test.ts +35 -0
- package/src/async.ts +43 -1
- package/src/audio-preflight.runtime.ts +9 -0
- package/src/bitable.test.ts +131 -0
- package/src/bitable.ts +59 -22
- package/src/bot-content.ts +474 -0
- package/src/bot-group-name.test.ts +108 -0
- package/src/bot-runtime-api.ts +12 -0
- package/src/bot-sender-name.ts +125 -0
- package/src/bot.broadcast.test.ts +463 -0
- package/src/bot.card-action.test.ts +519 -5
- package/src/bot.checkBotMentioned.test.ts +92 -20
- package/src/bot.helpers.test.ts +118 -0
- package/src/bot.stripBotMention.test.ts +13 -21
- package/src/bot.test.ts +1334 -401
- package/src/bot.ts +778 -775
- package/src/card-action.ts +408 -40
- package/src/card-interaction.test.ts +129 -0
- package/src/card-interaction.ts +159 -0
- package/src/card-test-helpers.ts +47 -0
- package/src/card-ux-approval.ts +65 -0
- package/src/card-ux-launcher.test.ts +99 -0
- package/src/card-ux-launcher.ts +121 -0
- package/src/card-ux-shared.ts +33 -0
- package/src/channel-runtime-api.ts +16 -0
- package/src/channel.runtime.ts +47 -0
- package/src/channel.test.ts +914 -3
- package/src/channel.ts +1252 -309
- package/src/chat-schema.ts +5 -4
- package/src/chat.test.ts +84 -28
- package/src/chat.ts +68 -10
- package/src/client.test.ts +212 -103
- package/src/client.ts +115 -21
- package/src/comment-dispatcher-runtime-api.ts +6 -0
- package/src/comment-dispatcher.test.ts +169 -0
- package/src/comment-dispatcher.ts +107 -0
- package/src/comment-handler-runtime-api.ts +3 -0
- package/src/comment-handler.test.ts +486 -0
- package/src/comment-handler.ts +309 -0
- package/src/comment-reaction.test.ts +166 -0
- package/src/comment-reaction.ts +259 -0
- package/src/comment-shared.test.ts +182 -0
- package/src/comment-shared.ts +365 -0
- package/src/comment-target.ts +44 -0
- package/src/config-schema.test.ts +63 -1
- package/src/config-schema.ts +31 -4
- package/src/conversation-id.test.ts +18 -0
- package/src/conversation-id.ts +199 -0
- package/src/dedup-runtime-api.ts +1 -0
- package/src/dedup.ts +32 -94
- package/src/directory.static.ts +61 -0
- package/src/directory.test.ts +119 -20
- package/src/directory.ts +61 -91
- package/src/doc-schema.ts +1 -1
- package/src/docx-batch-insert.test.ts +39 -38
- package/src/docx-batch-insert.ts +55 -19
- package/src/docx-color-text.ts +9 -4
- package/src/docx-table-ops.test.ts +53 -0
- package/src/docx-table-ops.ts +52 -34
- package/src/docx-types.ts +38 -0
- package/src/docx.account-selection.test.ts +12 -3
- package/src/docx.test.ts +314 -74
- package/src/docx.ts +278 -122
- package/src/drive-schema.ts +47 -1
- package/src/drive.test.ts +1219 -0
- package/src/drive.ts +614 -13
- package/src/dynamic-agent.ts +10 -4
- package/src/event-types.ts +45 -0
- package/src/external-keys.ts +1 -1
- package/src/lifecycle.test-support.ts +220 -0
- package/src/media.test.ts +375 -26
- package/src/media.ts +434 -88
- package/src/mention-target.types.ts +5 -0
- package/src/mention.ts +32 -51
- package/src/message-action-contract.ts +13 -0
- package/src/monitor-state-runtime-api.ts +7 -0
- package/src/monitor-transport-runtime-api.ts +7 -0
- package/src/monitor.account.ts +218 -312
- package/src/monitor.acp-init-failure.lifecycle.test-support.ts +219 -0
- package/src/monitor.bot-identity.ts +86 -0
- package/src/monitor.bot-menu-handler.ts +165 -0
- package/src/monitor.bot-menu.lifecycle.test-support.ts +224 -0
- package/src/monitor.bot-menu.test.ts +178 -0
- package/src/monitor.broadcast.reply-once.lifecycle.test-support.ts +264 -0
- package/src/monitor.card-action.lifecycle.test-support.ts +373 -0
- package/src/monitor.cleanup.test.ts +376 -0
- package/src/monitor.comment-notice-handler.ts +105 -0
- package/src/monitor.comment.test.ts +937 -0
- package/src/monitor.comment.ts +1386 -0
- package/src/monitor.lifecycle.test.ts +4 -0
- package/src/monitor.message-handler.ts +339 -0
- package/src/monitor.reaction.lifecycle.test-support.ts +68 -0
- package/src/monitor.reaction.test.ts +108 -48
- package/src/monitor.reply-once.lifecycle.test-support.ts +190 -0
- package/src/monitor.startup.test.ts +11 -9
- package/src/monitor.startup.ts +26 -16
- package/src/monitor.state.ts +20 -5
- package/src/monitor.synthetic-error.ts +18 -0
- package/src/monitor.test-mocks.ts +2 -2
- package/src/monitor.transport.ts +220 -60
- package/src/monitor.ts +15 -10
- package/src/monitor.webhook-e2e.test.ts +65 -7
- package/src/monitor.webhook-security.test.ts +122 -0
- package/src/monitor.webhook.test-helpers.ts +44 -26
- package/src/outbound-runtime-api.ts +1 -0
- package/src/outbound.test.ts +616 -37
- package/src/outbound.ts +623 -81
- package/src/perm-schema.ts +1 -1
- package/src/perm.ts +1 -7
- package/src/pins.ts +108 -0
- package/src/policy.test.ts +297 -117
- package/src/policy.ts +142 -29
- package/src/post.ts +7 -6
- package/src/probe.test.ts +14 -9
- package/src/probe.ts +26 -16
- package/src/processing-claims.ts +59 -0
- package/src/qr-terminal.ts +1 -0
- package/src/reactions.ts +4 -34
- package/src/reasoning-preview.test.ts +59 -0
- package/src/reasoning-preview.ts +20 -0
- package/src/reply-dispatcher-runtime-api.ts +7 -0
- package/src/reply-dispatcher.test.ts +660 -29
- package/src/reply-dispatcher.ts +407 -154
- package/src/runtime.ts +6 -3
- package/src/secret-contract.ts +145 -0
- package/src/secret-input.ts +1 -13
- package/src/security-audit-shared.ts +69 -0
- package/src/security-audit.test.ts +61 -0
- package/src/security-audit.ts +1 -0
- package/src/send-result.ts +1 -1
- package/src/send-target.test.ts +9 -3
- package/src/send-target.ts +10 -4
- package/src/send.reply-fallback.test.ts +77 -2
- package/src/send.test.ts +386 -4
- package/src/send.ts +399 -86
- package/src/sequential-key.test.ts +72 -0
- package/src/sequential-key.ts +28 -0
- package/src/sequential-queue.test.ts +92 -0
- package/src/sequential-queue.ts +16 -0
- package/src/session-conversation.ts +42 -0
- package/src/session-route.ts +48 -0
- package/src/setup-core.ts +51 -0
- package/src/{onboarding.test.ts → setup-surface.test.ts} +52 -21
- package/src/setup-surface.ts +581 -0
- package/src/streaming-card.test.ts +138 -2
- package/src/streaming-card.ts +134 -18
- package/src/subagent-hooks.test.ts +603 -0
- package/src/subagent-hooks.ts +397 -0
- package/src/targets.ts +3 -13
- package/src/test-support/lifecycle-test-support.ts +479 -0
- package/src/thread-bindings.test.ts +143 -0
- package/src/thread-bindings.ts +330 -0
- package/src/tool-account-routing.test.ts +66 -8
- package/src/tool-account.test.ts +44 -0
- package/src/tool-account.ts +40 -17
- package/src/tool-factory-test-harness.ts +11 -8
- package/src/tool-result.ts +3 -1
- package/src/tools-config.ts +1 -1
- package/src/types.ts +16 -15
- package/src/typing.ts +10 -6
- package/src/wiki-schema.ts +1 -1
- package/src/wiki.ts +1 -7
- package/subagent-hooks-api.ts +31 -0
- package/tsconfig.json +16 -0
- package/src/feishu-command-handler.ts +0 -59
- package/src/onboarding.status.test.ts +0 -25
- package/src/onboarding.ts +0 -489
- package/src/send-message.ts +0 -71
- package/src/targets.test.ts +0 -70
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import type { ClawdbotConfig, PluginRuntime } from "../runtime-api.js";
|
|
3
|
+
import { handleFeishuCommentEvent } from "./comment-handler.js";
|
|
4
|
+
import { setFeishuRuntime } from "./runtime.js";
|
|
5
|
+
|
|
6
|
+
const resolveDriveCommentEventTurnMock = vi.hoisted(() => vi.fn());
|
|
7
|
+
const createFeishuCommentReplyDispatcherMock = vi.hoisted(() => vi.fn());
|
|
8
|
+
const maybeCreateDynamicAgentMock = vi.hoisted(() => vi.fn());
|
|
9
|
+
const createFeishuClientMock = vi.hoisted(() => vi.fn(() => ({ request: vi.fn() })));
|
|
10
|
+
const deliverCommentThreadTextMock = vi.hoisted(() => vi.fn());
|
|
11
|
+
|
|
12
|
+
vi.mock("./monitor.comment.js", () => ({
|
|
13
|
+
resolveDriveCommentEventTurn: resolveDriveCommentEventTurnMock,
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
vi.mock("./comment-dispatcher.js", () => ({
|
|
17
|
+
createFeishuCommentReplyDispatcher: createFeishuCommentReplyDispatcherMock,
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
vi.mock("./dynamic-agent.js", () => ({
|
|
21
|
+
maybeCreateDynamicAgent: maybeCreateDynamicAgentMock,
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
vi.mock("./client.js", () => ({
|
|
25
|
+
createFeishuClient: createFeishuClientMock,
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
vi.mock("./drive.js", () => ({
|
|
29
|
+
deliverCommentThreadText: deliverCommentThreadTextMock,
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
function buildConfig(overrides?: Partial<ClawdbotConfig>): ClawdbotConfig {
|
|
33
|
+
return {
|
|
34
|
+
channels: {
|
|
35
|
+
feishu: {
|
|
36
|
+
enabled: true,
|
|
37
|
+
dmPolicy: "open",
|
|
38
|
+
allowFrom: ["*"],
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
...overrides,
|
|
42
|
+
} as ClawdbotConfig;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function buildResolvedRoute(matchedBy: "binding.channel" | "default" = "binding.channel") {
|
|
46
|
+
return {
|
|
47
|
+
agentId: "main",
|
|
48
|
+
channel: "feishu",
|
|
49
|
+
accountId: "default",
|
|
50
|
+
sessionKey: "agent:main:feishu:direct:ou_sender",
|
|
51
|
+
mainSessionKey: "agent:main:feishu",
|
|
52
|
+
lastRoutePolicy: "session" as const,
|
|
53
|
+
matchedBy,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function createTestRuntime(overrides?: {
|
|
58
|
+
readAllowFromStore?: () => Promise<unknown[]>;
|
|
59
|
+
upsertPairingRequest?: () => Promise<{ code: string; created: boolean }>;
|
|
60
|
+
resolveAgentRoute?: () => ReturnType<typeof buildResolvedRoute>;
|
|
61
|
+
dispatchReplyFromConfig?: PluginRuntime["channel"]["reply"]["dispatchReplyFromConfig"];
|
|
62
|
+
withReplyDispatcher?: PluginRuntime["channel"]["reply"]["withReplyDispatcher"];
|
|
63
|
+
}) {
|
|
64
|
+
const finalizeInboundContext = vi.fn((ctx: Record<string, unknown>) => ctx);
|
|
65
|
+
const dispatchReplyFromConfig =
|
|
66
|
+
overrides?.dispatchReplyFromConfig ??
|
|
67
|
+
vi.fn(async () => ({
|
|
68
|
+
queuedFinal: true,
|
|
69
|
+
counts: { tool: 0, block: 0, final: 1 },
|
|
70
|
+
}));
|
|
71
|
+
const withReplyDispatcher =
|
|
72
|
+
overrides?.withReplyDispatcher ??
|
|
73
|
+
vi.fn(
|
|
74
|
+
async ({
|
|
75
|
+
run,
|
|
76
|
+
onSettled,
|
|
77
|
+
}: {
|
|
78
|
+
run: () => Promise<unknown>;
|
|
79
|
+
onSettled?: () => Promise<void> | void;
|
|
80
|
+
}) => {
|
|
81
|
+
try {
|
|
82
|
+
return await run();
|
|
83
|
+
} finally {
|
|
84
|
+
await onSettled?.();
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
);
|
|
88
|
+
const recordInboundSession = vi.fn(async () => {});
|
|
89
|
+
const runPrepared = vi.fn(
|
|
90
|
+
async (turn: Parameters<PluginRuntime["channel"]["turn"]["runPrepared"]>[0]) => {
|
|
91
|
+
await turn.recordInboundSession({
|
|
92
|
+
storePath: turn.storePath,
|
|
93
|
+
sessionKey: turn.ctxPayload.SessionKey ?? turn.routeSessionKey,
|
|
94
|
+
ctx: turn.ctxPayload,
|
|
95
|
+
groupResolution: turn.record?.groupResolution,
|
|
96
|
+
createIfMissing: turn.record?.createIfMissing,
|
|
97
|
+
updateLastRoute: turn.record?.updateLastRoute,
|
|
98
|
+
onRecordError: turn.record?.onRecordError ?? (() => undefined),
|
|
99
|
+
});
|
|
100
|
+
const dispatchResult = await turn.runDispatch();
|
|
101
|
+
return {
|
|
102
|
+
admission: { kind: "dispatch" as const },
|
|
103
|
+
dispatched: true,
|
|
104
|
+
ctxPayload: turn.ctxPayload,
|
|
105
|
+
routeSessionKey: turn.routeSessionKey,
|
|
106
|
+
dispatchResult,
|
|
107
|
+
};
|
|
108
|
+
},
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
channel: {
|
|
113
|
+
routing: {
|
|
114
|
+
buildAgentSessionKey: vi.fn(
|
|
115
|
+
({
|
|
116
|
+
agentId,
|
|
117
|
+
channel,
|
|
118
|
+
peer,
|
|
119
|
+
}: {
|
|
120
|
+
agentId: string;
|
|
121
|
+
channel: string;
|
|
122
|
+
peer?: { kind?: string; id?: string };
|
|
123
|
+
}) => `agent:${agentId}:${channel}:${peer?.kind ?? "direct"}:${peer?.id ?? "peer"}`,
|
|
124
|
+
),
|
|
125
|
+
resolveAgentRoute: vi.fn(overrides?.resolveAgentRoute ?? (() => buildResolvedRoute())),
|
|
126
|
+
},
|
|
127
|
+
reply: {
|
|
128
|
+
finalizeInboundContext,
|
|
129
|
+
dispatchReplyFromConfig,
|
|
130
|
+
withReplyDispatcher,
|
|
131
|
+
},
|
|
132
|
+
session: {
|
|
133
|
+
resolveStorePath: vi.fn(() => "/tmp/feishu-session-store.json"),
|
|
134
|
+
recordInboundSession,
|
|
135
|
+
},
|
|
136
|
+
turn: {
|
|
137
|
+
run: vi.fn(async (params: Parameters<PluginRuntime["channel"]["turn"]["run"]>[0]) => {
|
|
138
|
+
const input = await params.adapter.ingest(params.raw);
|
|
139
|
+
if (!input) {
|
|
140
|
+
return {
|
|
141
|
+
admission: { kind: "drop" as const, reason: "ingest-null" },
|
|
142
|
+
dispatched: false,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
const eventClass = {
|
|
146
|
+
kind: "message" as const,
|
|
147
|
+
canStartAgentTurn: true,
|
|
148
|
+
};
|
|
149
|
+
const turn = await params.adapter.resolveTurn(input, eventClass, {});
|
|
150
|
+
if (!("runDispatch" in turn)) {
|
|
151
|
+
throw new Error("feishu comment test runtime only supports prepared turns");
|
|
152
|
+
}
|
|
153
|
+
return await runPrepared(
|
|
154
|
+
turn as Parameters<PluginRuntime["channel"]["turn"]["runPrepared"]>[0],
|
|
155
|
+
);
|
|
156
|
+
}) as unknown as PluginRuntime["channel"]["turn"]["run"],
|
|
157
|
+
runPrepared: runPrepared as unknown as PluginRuntime["channel"]["turn"]["runPrepared"],
|
|
158
|
+
},
|
|
159
|
+
pairing: {
|
|
160
|
+
readAllowFromStore: vi.fn(overrides?.readAllowFromStore ?? (async () => [])),
|
|
161
|
+
upsertPairingRequest: vi.fn(
|
|
162
|
+
overrides?.upsertPairingRequest ??
|
|
163
|
+
(async () => ({
|
|
164
|
+
code: "TESTCODE",
|
|
165
|
+
created: true,
|
|
166
|
+
})),
|
|
167
|
+
),
|
|
168
|
+
buildPairingReply: vi.fn((code: string) => `Pairing code: ${code}`),
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
} as unknown as PluginRuntime;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
describe("handleFeishuCommentEvent", () => {
|
|
175
|
+
beforeEach(() => {
|
|
176
|
+
vi.clearAllMocks();
|
|
177
|
+
maybeCreateDynamicAgentMock.mockResolvedValue({ created: false });
|
|
178
|
+
resolveDriveCommentEventTurnMock.mockResolvedValue({
|
|
179
|
+
eventId: "evt_1",
|
|
180
|
+
messageId: "drive-comment:evt_1",
|
|
181
|
+
commentId: "comment_1",
|
|
182
|
+
replyId: "reply_1",
|
|
183
|
+
noticeType: "add_comment",
|
|
184
|
+
fileToken: "doc_token_1",
|
|
185
|
+
fileType: "docx",
|
|
186
|
+
isWholeComment: false,
|
|
187
|
+
senderId: "ou_sender",
|
|
188
|
+
senderUserId: "on_sender_user",
|
|
189
|
+
timestamp: "1774951528000",
|
|
190
|
+
isMentioned: true,
|
|
191
|
+
documentTitle: "Project review",
|
|
192
|
+
prompt: "prompt body",
|
|
193
|
+
preview: "prompt body",
|
|
194
|
+
rootCommentText: "root comment",
|
|
195
|
+
targetReplyText: "latest reply",
|
|
196
|
+
});
|
|
197
|
+
deliverCommentThreadTextMock.mockResolvedValue({
|
|
198
|
+
delivery_mode: "reply_comment",
|
|
199
|
+
reply_id: "r1",
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
const runtime = createTestRuntime();
|
|
203
|
+
setFeishuRuntime(runtime);
|
|
204
|
+
|
|
205
|
+
createFeishuCommentReplyDispatcherMock.mockReturnValue({
|
|
206
|
+
dispatcher: {
|
|
207
|
+
markComplete: vi.fn(),
|
|
208
|
+
waitForIdle: vi.fn(async () => {}),
|
|
209
|
+
},
|
|
210
|
+
replyOptions: {},
|
|
211
|
+
markDispatchIdle: vi.fn(),
|
|
212
|
+
markRunComplete: vi.fn(),
|
|
213
|
+
startTypingReaction: vi.fn(async () => {}),
|
|
214
|
+
cleanupTypingReaction: vi.fn(async () => {}),
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it("records a comment-thread inbound context with a routable Feishu origin", async () => {
|
|
219
|
+
await handleFeishuCommentEvent({
|
|
220
|
+
cfg: buildConfig(),
|
|
221
|
+
accountId: "default",
|
|
222
|
+
event: { event_id: "evt_1" },
|
|
223
|
+
botOpenId: "ou_bot",
|
|
224
|
+
runtime: {
|
|
225
|
+
log: vi.fn(),
|
|
226
|
+
error: vi.fn(),
|
|
227
|
+
} as never,
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
const runtime = (await import("./runtime.js")).getFeishuRuntime();
|
|
231
|
+
const finalizeInboundContext = runtime.channel.reply.finalizeInboundContext as ReturnType<
|
|
232
|
+
typeof vi.fn
|
|
233
|
+
>;
|
|
234
|
+
const recordInboundSession = runtime.channel.session.recordInboundSession as ReturnType<
|
|
235
|
+
typeof vi.fn
|
|
236
|
+
>;
|
|
237
|
+
const dispatchReplyFromConfig = runtime.channel.reply.dispatchReplyFromConfig as ReturnType<
|
|
238
|
+
typeof vi.fn
|
|
239
|
+
>;
|
|
240
|
+
|
|
241
|
+
expect(finalizeInboundContext).toHaveBeenCalledWith(
|
|
242
|
+
expect.objectContaining({
|
|
243
|
+
From: "feishu:ou_sender",
|
|
244
|
+
To: "comment:docx:doc_token_1:comment_1",
|
|
245
|
+
Surface: "feishu-comment",
|
|
246
|
+
OriginatingChannel: "feishu",
|
|
247
|
+
OriginatingTo: "comment:docx:doc_token_1:comment_1",
|
|
248
|
+
MessageSid: "drive-comment:evt_1",
|
|
249
|
+
MessageThreadId: "reply_1",
|
|
250
|
+
}),
|
|
251
|
+
);
|
|
252
|
+
expect(recordInboundSession).toHaveBeenCalledTimes(1);
|
|
253
|
+
expect(recordInboundSession).toHaveBeenCalledWith(
|
|
254
|
+
expect.objectContaining({
|
|
255
|
+
sessionKey: "agent:main:feishu:direct:comment-doc:docx:doc_token_1",
|
|
256
|
+
}),
|
|
257
|
+
);
|
|
258
|
+
expect(dispatchReplyFromConfig).toHaveBeenCalledTimes(1);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it("allows comment senders matched by user_id allowlist entries", async () => {
|
|
262
|
+
const runtime = createTestRuntime();
|
|
263
|
+
setFeishuRuntime(runtime);
|
|
264
|
+
|
|
265
|
+
await handleFeishuCommentEvent({
|
|
266
|
+
cfg: buildConfig({
|
|
267
|
+
channels: {
|
|
268
|
+
feishu: {
|
|
269
|
+
enabled: true,
|
|
270
|
+
dmPolicy: "allowlist",
|
|
271
|
+
allowFrom: ["on_sender_user"],
|
|
272
|
+
},
|
|
273
|
+
},
|
|
274
|
+
}),
|
|
275
|
+
accountId: "default",
|
|
276
|
+
event: { event_id: "evt_1" },
|
|
277
|
+
botOpenId: "ou_bot",
|
|
278
|
+
runtime: {
|
|
279
|
+
log: vi.fn(),
|
|
280
|
+
error: vi.fn(),
|
|
281
|
+
} as never,
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
const dispatchReplyFromConfig = runtime.channel.reply.dispatchReplyFromConfig as ReturnType<
|
|
285
|
+
typeof vi.fn
|
|
286
|
+
>;
|
|
287
|
+
expect(dispatchReplyFromConfig).toHaveBeenCalledTimes(1);
|
|
288
|
+
expect(deliverCommentThreadTextMock).not.toHaveBeenCalled();
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it("issues a pairing challenge in the comment thread when dmPolicy=pairing", async () => {
|
|
292
|
+
const runtime = createTestRuntime();
|
|
293
|
+
setFeishuRuntime(runtime);
|
|
294
|
+
|
|
295
|
+
await handleFeishuCommentEvent({
|
|
296
|
+
cfg: buildConfig({
|
|
297
|
+
channels: {
|
|
298
|
+
feishu: {
|
|
299
|
+
enabled: true,
|
|
300
|
+
dmPolicy: "pairing",
|
|
301
|
+
allowFrom: [],
|
|
302
|
+
},
|
|
303
|
+
},
|
|
304
|
+
}),
|
|
305
|
+
accountId: "default",
|
|
306
|
+
event: { event_id: "evt_1" },
|
|
307
|
+
botOpenId: "ou_bot",
|
|
308
|
+
runtime: {
|
|
309
|
+
log: vi.fn(),
|
|
310
|
+
error: vi.fn(),
|
|
311
|
+
} as never,
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
expect(deliverCommentThreadTextMock).toHaveBeenCalledWith(
|
|
315
|
+
expect.anything(),
|
|
316
|
+
expect.objectContaining({
|
|
317
|
+
file_token: "doc_token_1",
|
|
318
|
+
file_type: "docx",
|
|
319
|
+
comment_id: "comment_1",
|
|
320
|
+
is_whole_comment: false,
|
|
321
|
+
}),
|
|
322
|
+
);
|
|
323
|
+
const dispatchReplyFromConfig = runtime.channel.reply.dispatchReplyFromConfig as ReturnType<
|
|
324
|
+
typeof vi.fn
|
|
325
|
+
>;
|
|
326
|
+
expect(dispatchReplyFromConfig).not.toHaveBeenCalled();
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it("passes whole-comment metadata to the comment reply dispatcher", async () => {
|
|
330
|
+
resolveDriveCommentEventTurnMock.mockResolvedValueOnce({
|
|
331
|
+
eventId: "evt_whole",
|
|
332
|
+
messageId: "drive-comment:evt_whole",
|
|
333
|
+
commentId: "comment_whole",
|
|
334
|
+
replyId: "reply_whole",
|
|
335
|
+
noticeType: "add_reply",
|
|
336
|
+
fileToken: "doc_token_1",
|
|
337
|
+
fileType: "docx",
|
|
338
|
+
isWholeComment: true,
|
|
339
|
+
senderId: "ou_sender",
|
|
340
|
+
senderUserId: "on_sender_user",
|
|
341
|
+
timestamp: "1774951528000",
|
|
342
|
+
isMentioned: false,
|
|
343
|
+
documentTitle: "Project review",
|
|
344
|
+
prompt: "prompt body",
|
|
345
|
+
preview: "prompt body",
|
|
346
|
+
rootCommentText: "root comment",
|
|
347
|
+
targetReplyText: "reply text",
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
await handleFeishuCommentEvent({
|
|
351
|
+
cfg: buildConfig(),
|
|
352
|
+
accountId: "default",
|
|
353
|
+
event: { event_id: "evt_whole" },
|
|
354
|
+
botOpenId: "ou_bot",
|
|
355
|
+
runtime: {
|
|
356
|
+
log: vi.fn(),
|
|
357
|
+
error: vi.fn(),
|
|
358
|
+
} as never,
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
expect(createFeishuCommentReplyDispatcherMock).toHaveBeenCalledWith(
|
|
362
|
+
expect.objectContaining({
|
|
363
|
+
commentId: "comment_whole",
|
|
364
|
+
fileToken: "doc_token_1",
|
|
365
|
+
fileType: "docx",
|
|
366
|
+
replyId: "reply_whole",
|
|
367
|
+
isWholeComment: true,
|
|
368
|
+
}),
|
|
369
|
+
);
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
it("always finalizes comment typing cleanup even when dispatch fails", async () => {
|
|
373
|
+
const dispatchReplyFromConfig = vi.fn(async () => {
|
|
374
|
+
throw new Error("dispatch failed");
|
|
375
|
+
});
|
|
376
|
+
const runtime = createTestRuntime({ dispatchReplyFromConfig });
|
|
377
|
+
setFeishuRuntime(runtime);
|
|
378
|
+
const markRunComplete = vi.fn();
|
|
379
|
+
const markDispatchIdle = vi.fn();
|
|
380
|
+
const cleanupTypingReaction = vi.fn(async () => {});
|
|
381
|
+
createFeishuCommentReplyDispatcherMock.mockReturnValue({
|
|
382
|
+
dispatcher: {
|
|
383
|
+
markComplete: vi.fn(),
|
|
384
|
+
waitForIdle: vi.fn(async () => {}),
|
|
385
|
+
},
|
|
386
|
+
replyOptions: {},
|
|
387
|
+
markDispatchIdle,
|
|
388
|
+
markRunComplete,
|
|
389
|
+
startTypingReaction: vi.fn(async () => {}),
|
|
390
|
+
cleanupTypingReaction,
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
await expect(
|
|
394
|
+
handleFeishuCommentEvent({
|
|
395
|
+
cfg: buildConfig(),
|
|
396
|
+
accountId: "default",
|
|
397
|
+
event: { event_id: "evt_1" },
|
|
398
|
+
botOpenId: "ou_bot",
|
|
399
|
+
runtime: {
|
|
400
|
+
log: vi.fn(),
|
|
401
|
+
error: vi.fn(),
|
|
402
|
+
} as never,
|
|
403
|
+
}),
|
|
404
|
+
).rejects.toThrow("dispatch failed");
|
|
405
|
+
|
|
406
|
+
expect(markRunComplete).toHaveBeenCalledTimes(1);
|
|
407
|
+
expect(markDispatchIdle).toHaveBeenCalledTimes(1);
|
|
408
|
+
expect(cleanupTypingReaction).toHaveBeenCalledTimes(1);
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
it("does not wait for comment typing cleanup before returning", async () => {
|
|
412
|
+
let resolveCleanup: (() => void) | undefined;
|
|
413
|
+
const cleanupTypingReaction = vi.fn(
|
|
414
|
+
() =>
|
|
415
|
+
new Promise<void>((resolve) => {
|
|
416
|
+
resolveCleanup = resolve;
|
|
417
|
+
}),
|
|
418
|
+
);
|
|
419
|
+
createFeishuCommentReplyDispatcherMock.mockReturnValue({
|
|
420
|
+
dispatcher: {
|
|
421
|
+
markComplete: vi.fn(),
|
|
422
|
+
waitForIdle: vi.fn(async () => {}),
|
|
423
|
+
},
|
|
424
|
+
replyOptions: {},
|
|
425
|
+
markDispatchIdle: vi.fn(),
|
|
426
|
+
markRunComplete: vi.fn(),
|
|
427
|
+
startTypingReaction: vi.fn(async () => {}),
|
|
428
|
+
cleanupTypingReaction,
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
const eventPromise = handleFeishuCommentEvent({
|
|
432
|
+
cfg: buildConfig(),
|
|
433
|
+
accountId: "default",
|
|
434
|
+
event: { event_id: "evt_1" },
|
|
435
|
+
botOpenId: "ou_bot",
|
|
436
|
+
runtime: {
|
|
437
|
+
log: vi.fn(),
|
|
438
|
+
error: vi.fn(),
|
|
439
|
+
} as never,
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
const status = await Promise.race([
|
|
443
|
+
eventPromise.then(() => "done"),
|
|
444
|
+
new Promise<string>((resolve) => setTimeout(() => resolve("pending"), 0)),
|
|
445
|
+
]);
|
|
446
|
+
|
|
447
|
+
expect(status).toBe("done");
|
|
448
|
+
expect(cleanupTypingReaction).toHaveBeenCalledTimes(1);
|
|
449
|
+
|
|
450
|
+
resolveCleanup?.();
|
|
451
|
+
await eventPromise;
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
it("does not start comment typing reaction before dispatch begins", async () => {
|
|
455
|
+
const startTypingReaction = vi.fn(async () => {});
|
|
456
|
+
createFeishuCommentReplyDispatcherMock.mockReturnValue({
|
|
457
|
+
dispatcher: {
|
|
458
|
+
markComplete: vi.fn(),
|
|
459
|
+
waitForIdle: vi.fn(async () => {}),
|
|
460
|
+
},
|
|
461
|
+
replyOptions: {},
|
|
462
|
+
markDispatchIdle: vi.fn(),
|
|
463
|
+
markRunComplete: vi.fn(),
|
|
464
|
+
startTypingReaction,
|
|
465
|
+
cleanupTypingReaction: vi.fn(async () => {}),
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
await handleFeishuCommentEvent({
|
|
469
|
+
cfg: buildConfig(),
|
|
470
|
+
accountId: "default",
|
|
471
|
+
event: { event_id: "evt_1" },
|
|
472
|
+
botOpenId: "ou_bot",
|
|
473
|
+
runtime: {
|
|
474
|
+
log: vi.fn(),
|
|
475
|
+
error: vi.fn(),
|
|
476
|
+
} as never,
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
expect(startTypingReaction).not.toHaveBeenCalled();
|
|
480
|
+
const runtime = (await import("./runtime.js")).getFeishuRuntime();
|
|
481
|
+
const dispatchReplyFromConfig = runtime.channel.reply.dispatchReplyFromConfig as ReturnType<
|
|
482
|
+
typeof vi.fn
|
|
483
|
+
>;
|
|
484
|
+
expect(dispatchReplyFromConfig).toHaveBeenCalledTimes(1);
|
|
485
|
+
});
|
|
486
|
+
});
|