@kodelyth/feishu 2026.5.42 → 2026.6.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/klaw.plugin.json +1712 -47
- package/package.json +17 -4
- package/api.ts +0 -32
- package/channel-entry.ts +0 -20
- package/channel-plugin-api.ts +0 -1
- package/contract-api.ts +0 -16
- package/index.ts +0 -82
- package/runtime-api.ts +0 -52
- package/secret-contract-api.ts +0 -5
- package/security-contract-api.ts +0 -1
- package/session-key-api.ts +0 -1
- package/setup-api.ts +0 -3
- package/setup-entry.test.ts +0 -19
- package/setup-entry.ts +0 -13
- package/src/accounts.test.ts +0 -480
- package/src/accounts.ts +0 -333
- package/src/agent-config.ts +0 -21
- package/src/app-registration.ts +0 -331
- package/src/approval-auth.test.ts +0 -24
- package/src/approval-auth.ts +0 -25
- package/src/async.test.ts +0 -35
- package/src/async.ts +0 -104
- package/src/audio-preflight.runtime.ts +0 -9
- package/src/bitable.test.ts +0 -136
- package/src/bitable.ts +0 -762
- package/src/bot-content.ts +0 -485
- package/src/bot-group-name.test.ts +0 -116
- package/src/bot-runtime-api.ts +0 -12
- package/src/bot-sender-name.ts +0 -125
- package/src/bot.broadcast.test.ts +0 -523
- package/src/bot.card-action.test.ts +0 -552
- package/src/bot.checkBotMentioned.test.ts +0 -265
- package/src/bot.helpers.test.ts +0 -135
- package/src/bot.stripBotMention.test.ts +0 -126
- package/src/bot.test.ts +0 -3671
- package/src/bot.ts +0 -1703
- package/src/card-action.ts +0 -447
- package/src/card-interaction.test.ts +0 -131
- package/src/card-interaction.ts +0 -159
- package/src/card-test-helpers.ts +0 -54
- package/src/card-ux-approval.ts +0 -65
- package/src/card-ux-launcher.test.ts +0 -106
- package/src/card-ux-launcher.ts +0 -121
- package/src/card-ux-shared.ts +0 -33
- package/src/channel-runtime-api.ts +0 -16
- package/src/channel.runtime.ts +0 -47
- package/src/channel.test.ts +0 -1151
- package/src/channel.ts +0 -1423
- package/src/chat-schema.ts +0 -25
- package/src/chat.test.ts +0 -240
- package/src/chat.ts +0 -188
- package/src/client-timeout.ts +0 -42
- package/src/client.test.ts +0 -447
- package/src/client.ts +0 -262
- package/src/comment-dispatcher-runtime-api.ts +0 -6
- package/src/comment-dispatcher.test.ts +0 -185
- package/src/comment-dispatcher.ts +0 -107
- package/src/comment-handler-runtime-api.ts +0 -3
- package/src/comment-handler.test.ts +0 -592
- package/src/comment-handler.ts +0 -303
- package/src/comment-reaction.test.ts +0 -138
- package/src/comment-reaction.ts +0 -259
- package/src/comment-shared.test.ts +0 -183
- package/src/comment-shared.ts +0 -406
- package/src/comment-target.ts +0 -44
- package/src/config-schema.test.ts +0 -326
- package/src/config-schema.ts +0 -335
- package/src/conversation-id.test.ts +0 -18
- package/src/conversation-id.ts +0 -199
- package/src/dedup-runtime-api.ts +0 -1
- package/src/dedup.ts +0 -141
- package/src/dedupe-key.ts +0 -72
- package/src/directory.static.ts +0 -61
- package/src/directory.test.ts +0 -141
- package/src/directory.ts +0 -124
- package/src/doc-schema.ts +0 -182
- package/src/docx-batch-insert.test.ts +0 -116
- package/src/docx-batch-insert.ts +0 -223
- package/src/docx-color-text.ts +0 -154
- package/src/docx-table-ops.test.ts +0 -53
- package/src/docx-table-ops.ts +0 -316
- package/src/docx-types.ts +0 -38
- package/src/docx.account-selection.test.ts +0 -95
- package/src/docx.test.ts +0 -701
- package/src/docx.ts +0 -1596
- package/src/drive-schema.ts +0 -92
- package/src/drive.test.ts +0 -1237
- package/src/drive.ts +0 -829
- package/src/dynamic-agent.test.ts +0 -155
- package/src/dynamic-agent.ts +0 -143
- package/src/event-types.ts +0 -45
- package/src/external-keys.test.ts +0 -20
- package/src/external-keys.ts +0 -19
- package/src/lifecycle.test-support.ts +0 -220
- package/src/media.test.ts +0 -955
- package/src/media.ts +0 -1105
- package/src/mention-target.types.ts +0 -5
- package/src/mention.ts +0 -114
- package/src/message-action-contract.ts +0 -13
- package/src/monitor-state-runtime-api.ts +0 -7
- package/src/monitor-transport-runtime-api.ts +0 -10
- package/src/monitor.account.ts +0 -492
- package/src/monitor.acp-init-failure.lifecycle.test-support.ts +0 -219
- package/src/monitor.bot-identity.ts +0 -86
- package/src/monitor.bot-menu-handler.ts +0 -165
- package/src/monitor.bot-menu.lifecycle.test-support.ts +0 -224
- package/src/monitor.bot-menu.test.ts +0 -188
- package/src/monitor.broadcast.reply-once.lifecycle.test-support.ts +0 -264
- package/src/monitor.card-action.lifecycle.test-support.ts +0 -421
- package/src/monitor.cleanup.test.ts +0 -383
- package/src/monitor.comment-notice-handler.ts +0 -105
- package/src/monitor.comment.test.ts +0 -967
- package/src/monitor.comment.ts +0 -1386
- package/src/monitor.lifecycle.test.ts +0 -4
- package/src/monitor.message-handler.ts +0 -350
- package/src/monitor.reaction.lifecycle.test-support.ts +0 -68
- package/src/monitor.reaction.test.ts +0 -739
- package/src/monitor.startup.test.ts +0 -213
- package/src/monitor.startup.ts +0 -74
- package/src/monitor.state.defaults.test.ts +0 -46
- package/src/monitor.state.ts +0 -170
- package/src/monitor.synthetic-error.ts +0 -18
- package/src/monitor.test-mocks.ts +0 -46
- package/src/monitor.transport.ts +0 -451
- package/src/monitor.ts +0 -100
- package/src/monitor.webhook-e2e.test.ts +0 -279
- package/src/monitor.webhook-security.test.ts +0 -389
- package/src/monitor.webhook.test-helpers.ts +0 -116
- package/src/outbound-runtime-api.ts +0 -1
- package/src/outbound.test.ts +0 -1118
- package/src/outbound.ts +0 -785
- package/src/perm-schema.ts +0 -52
- package/src/perm.ts +0 -170
- package/src/pins.ts +0 -108
- package/src/policy.test.ts +0 -223
- package/src/policy.ts +0 -318
- package/src/post.test.ts +0 -105
- package/src/post.ts +0 -275
- package/src/probe.test.ts +0 -283
- package/src/probe.ts +0 -166
- package/src/processing-claims.ts +0 -59
- package/src/qr-terminal.ts +0 -1
- package/src/reactions.ts +0 -123
- package/src/reasoning-preview.test.ts +0 -113
- package/src/reasoning-preview.ts +0 -28
- package/src/reply-dispatcher-runtime-api.ts +0 -7
- package/src/reply-dispatcher.test.ts +0 -1513
- package/src/reply-dispatcher.ts +0 -748
- package/src/runtime.ts +0 -9
- package/src/secret-contract.ts +0 -145
- package/src/secret-input.ts +0 -1
- package/src/security-audit-shared.ts +0 -69
- package/src/security-audit.test.ts +0 -59
- package/src/security-audit.ts +0 -1
- package/src/send-result.ts +0 -80
- package/src/send-target.test.ts +0 -86
- package/src/send-target.ts +0 -35
- package/src/send.reply-fallback.test.ts +0 -417
- package/src/send.test.ts +0 -621
- package/src/send.ts +0 -861
- package/src/sequential-key.test.ts +0 -72
- package/src/sequential-key.ts +0 -25
- package/src/sequential-queue.test.ts +0 -165
- package/src/sequential-queue.ts +0 -86
- package/src/session-conversation.ts +0 -42
- package/src/session-route.ts +0 -48
- package/src/setup-core.ts +0 -51
- package/src/setup-surface.test.ts +0 -484
- package/src/setup-surface.ts +0 -618
- package/src/streaming-card.test.ts +0 -397
- package/src/streaming-card.ts +0 -571
- package/src/subagent-hooks.test.ts +0 -627
- package/src/subagent-hooks.ts +0 -413
- package/src/targets.ts +0 -97
- package/src/test-support/lifecycle-test-support.ts +0 -454
- package/src/thread-bindings.test.ts +0 -180
- package/src/thread-bindings.ts +0 -331
- package/src/tool-account-routing.test.ts +0 -250
- package/src/tool-account.test.ts +0 -44
- package/src/tool-account.ts +0 -93
- package/src/tool-factory-test-harness.ts +0 -79
- package/src/tool-result.test.ts +0 -32
- package/src/tool-result.ts +0 -16
- package/src/tools-config.test.ts +0 -21
- package/src/tools-config.ts +0 -22
- package/src/types.ts +0 -106
- package/src/typing.test.ts +0 -144
- package/src/typing.ts +0 -214
- package/src/wiki-schema.ts +0 -69
- package/src/wiki.ts +0 -270
- package/subagent-hooks-api.ts +0 -31
- package/tsconfig.json +0 -16
|
@@ -1,552 +0,0 @@
|
|
|
1
|
-
import { createRuntimeEnv } from "klaw/plugin-sdk/plugin-test-runtime";
|
|
2
|
-
import { afterAll, afterEach, describe, it, expect, vi, beforeEach } from "vitest";
|
|
3
|
-
import type { ClawdbotConfig, RuntimeEnv } from "../runtime-api.js";
|
|
4
|
-
import {
|
|
5
|
-
FeishuRetryableCardActionError,
|
|
6
|
-
handleFeishuCardAction,
|
|
7
|
-
resetProcessedFeishuCardActionTokensForTests,
|
|
8
|
-
type FeishuCardActionEvent,
|
|
9
|
-
} from "./card-action.js";
|
|
10
|
-
import { createFeishuCardInteractionEnvelope } from "./card-interaction.js";
|
|
11
|
-
import {
|
|
12
|
-
expectFirstSentCardUsesFillWidthOnly,
|
|
13
|
-
expectSentCardHasP2pAction,
|
|
14
|
-
} from "./card-test-helpers.js";
|
|
15
|
-
import {
|
|
16
|
-
FEISHU_APPROVAL_CANCEL_ACTION,
|
|
17
|
-
FEISHU_APPROVAL_CONFIRM_ACTION,
|
|
18
|
-
FEISHU_APPROVAL_REQUEST_ACTION,
|
|
19
|
-
} from "./card-ux-approval.js";
|
|
20
|
-
|
|
21
|
-
// Mock account resolution
|
|
22
|
-
vi.mock("./accounts.js", () => ({
|
|
23
|
-
resolveFeishuAccount: vi.fn().mockReturnValue({ accountId: "mock-account" }),
|
|
24
|
-
resolveFeishuRuntimeAccount: vi.fn().mockReturnValue({ accountId: "mock-account" }),
|
|
25
|
-
}));
|
|
26
|
-
|
|
27
|
-
// Mock bot.js to verify handleFeishuMessage call
|
|
28
|
-
vi.mock("./bot.js", () => ({
|
|
29
|
-
handleFeishuMessage: vi.fn(),
|
|
30
|
-
}));
|
|
31
|
-
|
|
32
|
-
const createFeishuClientMock = vi.hoisted(() => vi.fn());
|
|
33
|
-
const sendCardFeishuMock = vi.hoisted(() => vi.fn());
|
|
34
|
-
const sendMessageFeishuMock = vi.hoisted(() => vi.fn());
|
|
35
|
-
|
|
36
|
-
vi.mock("./client.js", () => ({
|
|
37
|
-
createFeishuClient: createFeishuClientMock,
|
|
38
|
-
}));
|
|
39
|
-
|
|
40
|
-
vi.mock("./send.js", () => ({
|
|
41
|
-
sendCardFeishu: sendCardFeishuMock,
|
|
42
|
-
sendMessageFeishu: sendMessageFeishuMock,
|
|
43
|
-
}));
|
|
44
|
-
|
|
45
|
-
import { handleFeishuMessage } from "./bot.js";
|
|
46
|
-
|
|
47
|
-
describe("Feishu Card Action Handler", () => {
|
|
48
|
-
const cfg: ClawdbotConfig = {};
|
|
49
|
-
const runtime: RuntimeEnv = createRuntimeEnv();
|
|
50
|
-
|
|
51
|
-
afterAll(() => {
|
|
52
|
-
vi.doUnmock("./accounts.js");
|
|
53
|
-
vi.doUnmock("./bot.js");
|
|
54
|
-
vi.doUnmock("./client.js");
|
|
55
|
-
vi.doUnmock("./send.js");
|
|
56
|
-
vi.resetModules();
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
afterEach(() => {
|
|
60
|
-
vi.useRealTimers();
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
function createCardActionEvent(params: {
|
|
64
|
-
token: string;
|
|
65
|
-
actionValue: Record<string, unknown>;
|
|
66
|
-
chatId?: string;
|
|
67
|
-
openId?: string;
|
|
68
|
-
userId?: string;
|
|
69
|
-
unionId?: string;
|
|
70
|
-
}): FeishuCardActionEvent {
|
|
71
|
-
const openId = params.openId ?? "u123";
|
|
72
|
-
const userId = params.userId ?? "uid1";
|
|
73
|
-
return {
|
|
74
|
-
operator: { open_id: openId, user_id: userId, union_id: params.unionId ?? "un1" },
|
|
75
|
-
token: params.token,
|
|
76
|
-
action: {
|
|
77
|
-
value: params.actionValue,
|
|
78
|
-
tag: "button",
|
|
79
|
-
},
|
|
80
|
-
context: { open_id: openId, user_id: userId, chat_id: params.chatId ?? "chat1" },
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function createStructuredQuickActionEvent(params: {
|
|
85
|
-
token: string;
|
|
86
|
-
action: string;
|
|
87
|
-
command?: string;
|
|
88
|
-
chatId?: string;
|
|
89
|
-
chatType?: "group" | "p2p";
|
|
90
|
-
operatorOpenId?: string;
|
|
91
|
-
actionOpenId?: string;
|
|
92
|
-
}): FeishuCardActionEvent {
|
|
93
|
-
return createCardActionEvent({
|
|
94
|
-
token: params.token,
|
|
95
|
-
chatId: params.chatId,
|
|
96
|
-
openId: params.operatorOpenId,
|
|
97
|
-
actionValue: createFeishuCardInteractionEnvelope({
|
|
98
|
-
k: "quick",
|
|
99
|
-
a: params.action,
|
|
100
|
-
...(params.command ? { q: params.command } : {}),
|
|
101
|
-
c: {
|
|
102
|
-
u: params.actionOpenId ?? params.operatorOpenId ?? "u123",
|
|
103
|
-
h: params.chatId ?? "chat1",
|
|
104
|
-
t: params.chatType ?? "group",
|
|
105
|
-
e: Date.now() + 60_000,
|
|
106
|
-
},
|
|
107
|
-
}),
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
beforeEach(() => {
|
|
112
|
-
vi.clearAllMocks();
|
|
113
|
-
createFeishuClientMock.mockReset().mockReturnValue({
|
|
114
|
-
im: {
|
|
115
|
-
chat: {
|
|
116
|
-
get: vi.fn().mockResolvedValue({ code: 0, data: { chat_type: "group" } }),
|
|
117
|
-
},
|
|
118
|
-
},
|
|
119
|
-
});
|
|
120
|
-
vi.mocked(handleFeishuMessage)
|
|
121
|
-
.mockReset()
|
|
122
|
-
.mockResolvedValue(undefined as never);
|
|
123
|
-
resetProcessedFeishuCardActionTokensForTests();
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
function mockCallArg(
|
|
127
|
-
mock: { mock: { calls: unknown[][] } },
|
|
128
|
-
index: number,
|
|
129
|
-
label: string,
|
|
130
|
-
): unknown {
|
|
131
|
-
const call = mock.mock.calls[index];
|
|
132
|
-
if (!call) {
|
|
133
|
-
throw new Error(`Expected ${label} call ${index + 1}`);
|
|
134
|
-
}
|
|
135
|
-
return call[0];
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function requireRecord(value: unknown, label: string): Record<string, unknown> {
|
|
139
|
-
if (!value || typeof value !== "object") {
|
|
140
|
-
throw new Error(`Expected ${label}`);
|
|
141
|
-
}
|
|
142
|
-
return value as Record<string, unknown>;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
function handleMessageEvent(callIndex = 0) {
|
|
146
|
-
const arg = requireRecord(
|
|
147
|
-
mockCallArg(vi.mocked(handleFeishuMessage), callIndex, "handleFeishuMessage"),
|
|
148
|
-
"handleFeishuMessage args",
|
|
149
|
-
);
|
|
150
|
-
return requireRecord(arg.event, "Feishu message event");
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
function handleMessage(callIndex = 0) {
|
|
154
|
-
return requireRecord(handleMessageEvent(callIndex).message, "Feishu message");
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
function sendMessageCall(callIndex = 0) {
|
|
158
|
-
return requireRecord(
|
|
159
|
-
mockCallArg(sendMessageFeishuMock, callIndex, "sendMessageFeishu"),
|
|
160
|
-
"sendMessageFeishu args",
|
|
161
|
-
);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
function sendCardCall(callIndex = 0) {
|
|
165
|
-
return requireRecord(
|
|
166
|
-
mockCallArg(sendCardFeishuMock, callIndex, "sendCardFeishu"),
|
|
167
|
-
"sendCardFeishu args",
|
|
168
|
-
);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
it("handles card action with text payload", async () => {
|
|
172
|
-
const event: FeishuCardActionEvent = {
|
|
173
|
-
operator: { open_id: "u123", user_id: "uid1", union_id: "un1" },
|
|
174
|
-
token: "tok1",
|
|
175
|
-
action: { value: { text: "/ping" }, tag: "button" },
|
|
176
|
-
context: { open_id: "u123", user_id: "uid1", chat_id: "chat1" },
|
|
177
|
-
};
|
|
178
|
-
|
|
179
|
-
await handleFeishuCardAction({ cfg, event, runtime });
|
|
180
|
-
|
|
181
|
-
const message = handleMessage();
|
|
182
|
-
expect(message.content).toBe('{"text":"/ping"}');
|
|
183
|
-
expect(message.chat_id).toBe("chat1");
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
it("handles card action with JSON object payload", async () => {
|
|
187
|
-
const event: FeishuCardActionEvent = {
|
|
188
|
-
operator: { open_id: "u123", user_id: "uid1", union_id: "un1" },
|
|
189
|
-
token: "tok2",
|
|
190
|
-
action: { value: { key: "val" }, tag: "button" },
|
|
191
|
-
context: { open_id: "u123", user_id: "uid1", chat_id: "" },
|
|
192
|
-
};
|
|
193
|
-
|
|
194
|
-
await handleFeishuCardAction({ cfg, event, runtime });
|
|
195
|
-
|
|
196
|
-
const message = handleMessage();
|
|
197
|
-
expect(message.content).toBe('{"text":"{\\"key\\":\\"val\\"}"}');
|
|
198
|
-
expect(message.chat_id).toBe("u123"); // Fallback to open_id
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
it("routes quick command actions with operator and conversation context", async () => {
|
|
202
|
-
const event = createStructuredQuickActionEvent({
|
|
203
|
-
token: "tok3",
|
|
204
|
-
action: "feishu.quick_actions.help",
|
|
205
|
-
command: "/help",
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
await handleFeishuCardAction({ cfg, event, runtime });
|
|
209
|
-
|
|
210
|
-
const eventArg = handleMessageEvent();
|
|
211
|
-
const sender = requireRecord(eventArg.sender, "Feishu sender");
|
|
212
|
-
const senderId = requireRecord(sender.sender_id, "Feishu sender id");
|
|
213
|
-
expect(senderId.open_id).toBe("u123");
|
|
214
|
-
expect(senderId.user_id).toBe("uid1");
|
|
215
|
-
expect(senderId.union_id).toBe("un1");
|
|
216
|
-
const message = requireRecord(eventArg.message, "Feishu message");
|
|
217
|
-
expect(message.chat_id).toBe("chat1");
|
|
218
|
-
expect(message.content).toBe('{"text":"/help"}');
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
it("opens an approval card for metadata actions", async () => {
|
|
222
|
-
const event: FeishuCardActionEvent = {
|
|
223
|
-
operator: { open_id: "u123", user_id: "uid1", union_id: "un1" },
|
|
224
|
-
token: "tok4",
|
|
225
|
-
action: {
|
|
226
|
-
value: createFeishuCardInteractionEnvelope({
|
|
227
|
-
k: "meta",
|
|
228
|
-
a: FEISHU_APPROVAL_REQUEST_ACTION,
|
|
229
|
-
m: {
|
|
230
|
-
command: "/new",
|
|
231
|
-
prompt: "Start a fresh session?",
|
|
232
|
-
},
|
|
233
|
-
c: {
|
|
234
|
-
u: "u123",
|
|
235
|
-
h: "chat1",
|
|
236
|
-
t: "group",
|
|
237
|
-
s: "agent:codex:feishu:chat:chat1",
|
|
238
|
-
e: Date.now() + 60_000,
|
|
239
|
-
},
|
|
240
|
-
}),
|
|
241
|
-
tag: "button",
|
|
242
|
-
},
|
|
243
|
-
context: { open_id: "u123", user_id: "uid1", chat_id: "chat1" },
|
|
244
|
-
};
|
|
245
|
-
|
|
246
|
-
await handleFeishuCardAction({ cfg, event, runtime, accountId: "main" });
|
|
247
|
-
|
|
248
|
-
const cardCall = sendCardCall();
|
|
249
|
-
expect(cardCall.to).toBe("chat:chat1");
|
|
250
|
-
expect(cardCall.accountId).toBe("main");
|
|
251
|
-
const card = requireRecord(cardCall.card, "Feishu card");
|
|
252
|
-
expect(requireRecord(card.config, "Feishu card config").width_mode).toBe("fill");
|
|
253
|
-
const header = requireRecord(card.header, "Feishu card header");
|
|
254
|
-
expect(requireRecord(header.title, "Feishu card title").content).toBe("Confirm action");
|
|
255
|
-
const body = requireRecord(card.body, "Feishu card body");
|
|
256
|
-
const elements = body.elements as Array<Record<string, unknown>>;
|
|
257
|
-
const actionElement = elements.find((element) => element.tag === "action");
|
|
258
|
-
if (!actionElement) {
|
|
259
|
-
throw new Error("Expected action element");
|
|
260
|
-
}
|
|
261
|
-
const actions = actionElement.actions as Array<Record<string, unknown>>;
|
|
262
|
-
const actionValue = requireRecord(actions[0]?.value, "Feishu approval action value");
|
|
263
|
-
const approvalContext = requireRecord(actionValue.c, "Feishu approval context");
|
|
264
|
-
expect(approvalContext.u).toBe("u123");
|
|
265
|
-
expect(approvalContext.h).toBe("chat1");
|
|
266
|
-
expect(approvalContext.t).toBe("group");
|
|
267
|
-
expect(approvalContext.s).toBe("agent:codex:feishu:chat:chat1");
|
|
268
|
-
expect(typeof approvalContext.e).toBe("number");
|
|
269
|
-
expectFirstSentCardUsesFillWidthOnly(sendCardFeishuMock);
|
|
270
|
-
expect(handleFeishuMessage).not.toHaveBeenCalled();
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
it("runs approval confirmation through the normal message path", async () => {
|
|
274
|
-
const event = createStructuredQuickActionEvent({
|
|
275
|
-
token: "tok5",
|
|
276
|
-
action: FEISHU_APPROVAL_CONFIRM_ACTION,
|
|
277
|
-
command: "/new",
|
|
278
|
-
});
|
|
279
|
-
|
|
280
|
-
await handleFeishuCardAction({ cfg, event, runtime });
|
|
281
|
-
|
|
282
|
-
expect(handleMessage().content).toBe('{"text":"/new"}');
|
|
283
|
-
});
|
|
284
|
-
|
|
285
|
-
it("safely rejects stale structured actions", async () => {
|
|
286
|
-
const event = createCardActionEvent({
|
|
287
|
-
token: "tok6",
|
|
288
|
-
actionValue: createFeishuCardInteractionEnvelope({
|
|
289
|
-
k: "quick",
|
|
290
|
-
a: "feishu.quick_actions.help",
|
|
291
|
-
q: "/help",
|
|
292
|
-
c: { u: "u123", h: "chat1", t: "group", e: Date.now() - 1 },
|
|
293
|
-
}),
|
|
294
|
-
});
|
|
295
|
-
|
|
296
|
-
await handleFeishuCardAction({ cfg, event, runtime });
|
|
297
|
-
|
|
298
|
-
const sendMessage = sendMessageCall();
|
|
299
|
-
expect(sendMessage.to).toBe("chat:chat1");
|
|
300
|
-
expect(String(sendMessage.text)).toContain("expired");
|
|
301
|
-
expect(handleFeishuMessage).not.toHaveBeenCalled();
|
|
302
|
-
});
|
|
303
|
-
|
|
304
|
-
it("safely rejects wrong-user structured actions", async () => {
|
|
305
|
-
const event = createStructuredQuickActionEvent({
|
|
306
|
-
token: "tok7",
|
|
307
|
-
action: "feishu.quick_actions.help",
|
|
308
|
-
command: "/help",
|
|
309
|
-
operatorOpenId: "u999",
|
|
310
|
-
actionOpenId: "u123",
|
|
311
|
-
});
|
|
312
|
-
|
|
313
|
-
await handleFeishuCardAction({ cfg, event, runtime });
|
|
314
|
-
|
|
315
|
-
expect(String(sendMessageCall().text)).toContain("different user");
|
|
316
|
-
expect(handleFeishuMessage).not.toHaveBeenCalled();
|
|
317
|
-
});
|
|
318
|
-
|
|
319
|
-
it("sends a lightweight cancellation notice", async () => {
|
|
320
|
-
const event: FeishuCardActionEvent = {
|
|
321
|
-
operator: { open_id: "u123", user_id: "uid1", union_id: "un1" },
|
|
322
|
-
token: "tok8",
|
|
323
|
-
action: {
|
|
324
|
-
value: createFeishuCardInteractionEnvelope({
|
|
325
|
-
k: "button",
|
|
326
|
-
a: FEISHU_APPROVAL_CANCEL_ACTION,
|
|
327
|
-
c: { u: "u123", h: "chat1", t: "group", e: Date.now() + 60_000 },
|
|
328
|
-
}),
|
|
329
|
-
tag: "button",
|
|
330
|
-
},
|
|
331
|
-
context: { open_id: "u123", user_id: "uid1", chat_id: "chat1" },
|
|
332
|
-
};
|
|
333
|
-
|
|
334
|
-
await handleFeishuCardAction({ cfg, event, runtime });
|
|
335
|
-
|
|
336
|
-
const sendMessage = sendMessageCall();
|
|
337
|
-
expect(sendMessage.to).toBe("chat:chat1");
|
|
338
|
-
expect(sendMessage.text).toBe("Cancelled.");
|
|
339
|
-
});
|
|
340
|
-
|
|
341
|
-
it("preserves p2p callbacks for DM quick actions", async () => {
|
|
342
|
-
const event = createStructuredQuickActionEvent({
|
|
343
|
-
token: "tok9",
|
|
344
|
-
action: "feishu.quick_actions.help",
|
|
345
|
-
command: "/help",
|
|
346
|
-
chatId: "p2p-chat-1",
|
|
347
|
-
chatType: "p2p",
|
|
348
|
-
});
|
|
349
|
-
|
|
350
|
-
await handleFeishuCardAction({ cfg, event, runtime });
|
|
351
|
-
|
|
352
|
-
const message = handleMessage();
|
|
353
|
-
expect(message.chat_id).toBe("p2p-chat-1");
|
|
354
|
-
expect(message.chat_type).toBe("p2p");
|
|
355
|
-
});
|
|
356
|
-
|
|
357
|
-
it("resolves DM chat type from the Feishu chat API when card context omits it", async () => {
|
|
358
|
-
createFeishuClientMock.mockReturnValueOnce({
|
|
359
|
-
im: {
|
|
360
|
-
chat: {
|
|
361
|
-
get: vi.fn().mockResolvedValue({ code: 0, data: { chat_type: "p2p" } }),
|
|
362
|
-
},
|
|
363
|
-
},
|
|
364
|
-
});
|
|
365
|
-
const event = createCardActionEvent({
|
|
366
|
-
token: "tok9b",
|
|
367
|
-
chatId: "oc_dm_chat_123",
|
|
368
|
-
actionValue: { text: "/help" },
|
|
369
|
-
});
|
|
370
|
-
|
|
371
|
-
await handleFeishuCardAction({ cfg, event, runtime });
|
|
372
|
-
|
|
373
|
-
const message = handleMessage();
|
|
374
|
-
expect(message.chat_id).toBe("oc_dm_chat_123");
|
|
375
|
-
expect(message.chat_type).toBe("p2p");
|
|
376
|
-
expect(createFeishuClientMock).toHaveBeenCalledTimes(1);
|
|
377
|
-
});
|
|
378
|
-
|
|
379
|
-
it("uses resolved DM chat type when building approval cards without stored context", async () => {
|
|
380
|
-
createFeishuClientMock.mockReturnValueOnce({
|
|
381
|
-
im: {
|
|
382
|
-
chat: {
|
|
383
|
-
get: vi.fn().mockResolvedValue({ code: 0, data: { chat_mode: "p2p" } }),
|
|
384
|
-
},
|
|
385
|
-
},
|
|
386
|
-
});
|
|
387
|
-
const event = createCardActionEvent({
|
|
388
|
-
token: "tok9c",
|
|
389
|
-
chatId: "oc_dm_chat_234",
|
|
390
|
-
actionValue: createFeishuCardInteractionEnvelope({
|
|
391
|
-
k: "meta",
|
|
392
|
-
a: FEISHU_APPROVAL_REQUEST_ACTION,
|
|
393
|
-
m: {
|
|
394
|
-
command: "/new",
|
|
395
|
-
prompt: "Start a fresh session?",
|
|
396
|
-
},
|
|
397
|
-
c: {
|
|
398
|
-
u: "u123",
|
|
399
|
-
h: "oc_dm_chat_234",
|
|
400
|
-
e: Date.now() + 60_000,
|
|
401
|
-
},
|
|
402
|
-
}),
|
|
403
|
-
});
|
|
404
|
-
|
|
405
|
-
await handleFeishuCardAction({ cfg, event, runtime, accountId: "main" });
|
|
406
|
-
|
|
407
|
-
expectSentCardHasP2pAction(sendCardFeishuMock);
|
|
408
|
-
expect(createFeishuClientMock).toHaveBeenCalledTimes(1);
|
|
409
|
-
});
|
|
410
|
-
|
|
411
|
-
it("falls back to p2p when Feishu chat API returns an error", async () => {
|
|
412
|
-
createFeishuClientMock.mockReturnValueOnce({
|
|
413
|
-
im: {
|
|
414
|
-
chat: {
|
|
415
|
-
get: vi.fn().mockResolvedValue({ code: 99, msg: "not found" }),
|
|
416
|
-
},
|
|
417
|
-
},
|
|
418
|
-
});
|
|
419
|
-
const event = createCardActionEvent({
|
|
420
|
-
token: "tok9d",
|
|
421
|
-
chatId: "oc_unknown_chat_456",
|
|
422
|
-
actionValue: { text: "/help" },
|
|
423
|
-
});
|
|
424
|
-
|
|
425
|
-
await handleFeishuCardAction({ cfg, event, runtime });
|
|
426
|
-
|
|
427
|
-
expect(handleMessage().chat_type).toBe("p2p");
|
|
428
|
-
});
|
|
429
|
-
|
|
430
|
-
it("falls back to p2p when Feishu chat API throws", async () => {
|
|
431
|
-
createFeishuClientMock.mockReturnValueOnce({
|
|
432
|
-
im: {
|
|
433
|
-
chat: {
|
|
434
|
-
get: vi.fn().mockRejectedValue(new Error("network failure")),
|
|
435
|
-
},
|
|
436
|
-
},
|
|
437
|
-
});
|
|
438
|
-
const event = createCardActionEvent({
|
|
439
|
-
token: "tok9e",
|
|
440
|
-
chatId: "oc_broken_chat_789",
|
|
441
|
-
actionValue: { text: "/help" },
|
|
442
|
-
});
|
|
443
|
-
|
|
444
|
-
await handleFeishuCardAction({ cfg, event, runtime });
|
|
445
|
-
|
|
446
|
-
expect(handleMessage().chat_type).toBe("p2p");
|
|
447
|
-
});
|
|
448
|
-
|
|
449
|
-
it("drops duplicate structured callback tokens", async () => {
|
|
450
|
-
const event = createStructuredQuickActionEvent({
|
|
451
|
-
token: "tok10",
|
|
452
|
-
action: "feishu.quick_actions.help",
|
|
453
|
-
command: "/help",
|
|
454
|
-
});
|
|
455
|
-
|
|
456
|
-
await handleFeishuCardAction({ cfg, event, runtime });
|
|
457
|
-
await handleFeishuCardAction({ cfg, event, runtime });
|
|
458
|
-
|
|
459
|
-
expect(handleFeishuMessage).toHaveBeenCalledTimes(1);
|
|
460
|
-
});
|
|
461
|
-
|
|
462
|
-
it("rejects empty callback tokens before dispatch", async () => {
|
|
463
|
-
const log = vi.fn();
|
|
464
|
-
const event = createStructuredQuickActionEvent({
|
|
465
|
-
token: " ",
|
|
466
|
-
action: "feishu.quick_actions.help",
|
|
467
|
-
command: "/help",
|
|
468
|
-
});
|
|
469
|
-
|
|
470
|
-
await handleFeishuCardAction({
|
|
471
|
-
cfg,
|
|
472
|
-
event,
|
|
473
|
-
runtime: {
|
|
474
|
-
...runtime,
|
|
475
|
-
log,
|
|
476
|
-
},
|
|
477
|
-
});
|
|
478
|
-
|
|
479
|
-
expect(handleFeishuMessage).not.toHaveBeenCalled();
|
|
480
|
-
expect(log).toHaveBeenCalledWith(
|
|
481
|
-
"feishu[mock-account]: rejected card action from u123: missing token",
|
|
482
|
-
);
|
|
483
|
-
});
|
|
484
|
-
|
|
485
|
-
it("keeps a claimed token completed after a non-retryable dispatch failure", async () => {
|
|
486
|
-
const event = createStructuredQuickActionEvent({
|
|
487
|
-
token: "tok11",
|
|
488
|
-
action: "feishu.quick_actions.help",
|
|
489
|
-
command: "/help",
|
|
490
|
-
});
|
|
491
|
-
vi.mocked(handleFeishuMessage)
|
|
492
|
-
.mockRejectedValueOnce(new Error("transient"))
|
|
493
|
-
.mockResolvedValueOnce(undefined as never);
|
|
494
|
-
|
|
495
|
-
await expect(handleFeishuCardAction({ cfg, event, runtime })).rejects.toThrow("transient");
|
|
496
|
-
await handleFeishuCardAction({ cfg, event, runtime });
|
|
497
|
-
|
|
498
|
-
expect(handleFeishuMessage).toHaveBeenCalledTimes(1);
|
|
499
|
-
});
|
|
500
|
-
|
|
501
|
-
it("releases a claimed token for explicit retryable dispatch failures", async () => {
|
|
502
|
-
const event = createStructuredQuickActionEvent({
|
|
503
|
-
token: "tok11-retryable",
|
|
504
|
-
action: "feishu.quick_actions.help",
|
|
505
|
-
command: "/help",
|
|
506
|
-
});
|
|
507
|
-
vi.mocked(handleFeishuMessage)
|
|
508
|
-
.mockRejectedValueOnce(new FeishuRetryableCardActionError("retry me"))
|
|
509
|
-
.mockResolvedValueOnce(undefined as never);
|
|
510
|
-
|
|
511
|
-
await expect(handleFeishuCardAction({ cfg, event, runtime })).rejects.toThrow("retry me");
|
|
512
|
-
await handleFeishuCardAction({ cfg, event, runtime });
|
|
513
|
-
|
|
514
|
-
expect(handleFeishuMessage).toHaveBeenCalledTimes(2);
|
|
515
|
-
});
|
|
516
|
-
|
|
517
|
-
it("keeps an in-flight token claimed while a slow dispatch is still running", async () => {
|
|
518
|
-
vi.useFakeTimers();
|
|
519
|
-
const event: FeishuCardActionEvent = {
|
|
520
|
-
operator: { open_id: "u123", user_id: "uid1", union_id: "un1" },
|
|
521
|
-
token: "tok12",
|
|
522
|
-
action: {
|
|
523
|
-
value: createFeishuCardInteractionEnvelope({
|
|
524
|
-
k: "quick",
|
|
525
|
-
a: "feishu.quick_actions.help",
|
|
526
|
-
q: "/help",
|
|
527
|
-
c: { u: "u123", h: "chat1", t: "group", e: Date.now() + 60_000 },
|
|
528
|
-
}),
|
|
529
|
-
tag: "button",
|
|
530
|
-
},
|
|
531
|
-
context: { open_id: "u123", user_id: "uid1", chat_id: "chat1" },
|
|
532
|
-
};
|
|
533
|
-
|
|
534
|
-
let resolveDispatch: (() => void) | undefined;
|
|
535
|
-
vi.mocked(handleFeishuMessage).mockImplementation(
|
|
536
|
-
() =>
|
|
537
|
-
new Promise<void>((resolve) => {
|
|
538
|
-
resolveDispatch = resolve;
|
|
539
|
-
}) as never,
|
|
540
|
-
);
|
|
541
|
-
|
|
542
|
-
const first = handleFeishuCardAction({ cfg, event, runtime });
|
|
543
|
-
await vi.advanceTimersByTimeAsync(61_000);
|
|
544
|
-
await handleFeishuCardAction({ cfg, event, runtime });
|
|
545
|
-
|
|
546
|
-
expect(handleFeishuMessage).toHaveBeenCalledTimes(1);
|
|
547
|
-
|
|
548
|
-
resolveDispatch?.();
|
|
549
|
-
await first;
|
|
550
|
-
vi.useRealTimers();
|
|
551
|
-
});
|
|
552
|
-
});
|