@actagent/feishu 2026.6.2

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.
Files changed (207) hide show
  1. package/README.md +11 -0
  2. package/actagent.plugin.json +224 -0
  3. package/api.ts +33 -0
  4. package/channel-entry.ts +21 -0
  5. package/channel-plugin-api.ts +2 -0
  6. package/contract-api.ts +17 -0
  7. package/index.ts +83 -0
  8. package/legacy-state-migrations-api.ts +2 -0
  9. package/npm-shrinkwrap.json +539 -0
  10. package/package.json +64 -0
  11. package/runtime-api.ts +58 -0
  12. package/runtime-setter-api.ts +3 -0
  13. package/secret-contract-api.ts +6 -0
  14. package/security-contract-api.ts +2 -0
  15. package/session-key-api.ts +2 -0
  16. package/setup-api.ts +4 -0
  17. package/setup-entry.test.ts +33 -0
  18. package/setup-entry.ts +25 -0
  19. package/skills/feishu-doc/SKILL.md +211 -0
  20. package/skills/feishu-doc/references/block-types.md +103 -0
  21. package/skills/feishu-drive/SKILL.md +97 -0
  22. package/skills/feishu-perm/SKILL.md +119 -0
  23. package/skills/feishu-wiki/SKILL.md +113 -0
  24. package/src/accounts.test.ts +481 -0
  25. package/src/accounts.ts +380 -0
  26. package/src/agent-config.ts +22 -0
  27. package/src/app-registration.test.ts +62 -0
  28. package/src/app-registration.ts +355 -0
  29. package/src/approval-auth.test.ts +25 -0
  30. package/src/approval-auth.ts +26 -0
  31. package/src/async.test.ts +68 -0
  32. package/src/async.ts +109 -0
  33. package/src/audio-preflight.runtime.ts +10 -0
  34. package/src/bitable.test.ts +174 -0
  35. package/src/bitable.ts +781 -0
  36. package/src/bot-content.ts +488 -0
  37. package/src/bot-group-name.test.ts +148 -0
  38. package/src/bot-runtime-api.ts +13 -0
  39. package/src/bot-sender-name.test.ts +68 -0
  40. package/src/bot-sender-name.ts +137 -0
  41. package/src/bot.broadcast.test.ts +643 -0
  42. package/src/bot.card-action.test.ts +647 -0
  43. package/src/bot.checkBotMentioned.test.ts +266 -0
  44. package/src/bot.helpers.test.ts +136 -0
  45. package/src/bot.stripBotMention.test.ts +127 -0
  46. package/src/bot.test.ts +3817 -0
  47. package/src/bot.ts +1788 -0
  48. package/src/card-action.ts +515 -0
  49. package/src/card-interaction.test.ts +132 -0
  50. package/src/card-interaction.ts +160 -0
  51. package/src/card-test-helpers.ts +55 -0
  52. package/src/card-ux-approval.ts +66 -0
  53. package/src/card-ux-launcher.test.ts +126 -0
  54. package/src/card-ux-launcher.ts +136 -0
  55. package/src/card-ux-shared.ts +34 -0
  56. package/src/channel-runtime-api.ts +17 -0
  57. package/src/channel.runtime.ts +48 -0
  58. package/src/channel.test.ts +1337 -0
  59. package/src/channel.ts +1401 -0
  60. package/src/chat-schema.ts +30 -0
  61. package/src/chat.test.ts +295 -0
  62. package/src/chat.ts +198 -0
  63. package/src/client-timeout.ts +44 -0
  64. package/src/client.test.ts +463 -0
  65. package/src/client.ts +263 -0
  66. package/src/comment-dispatcher-runtime-api.ts +7 -0
  67. package/src/comment-dispatcher.test.ts +186 -0
  68. package/src/comment-dispatcher.ts +108 -0
  69. package/src/comment-handler-runtime-api.ts +4 -0
  70. package/src/comment-handler.test.ts +588 -0
  71. package/src/comment-handler.ts +304 -0
  72. package/src/comment-reaction.test.ts +139 -0
  73. package/src/comment-reaction.ts +260 -0
  74. package/src/comment-shared.test.ts +184 -0
  75. package/src/comment-shared.ts +405 -0
  76. package/src/comment-target.ts +45 -0
  77. package/src/config-schema.test.ts +327 -0
  78. package/src/config-schema.ts +338 -0
  79. package/src/conversation-id.test.ts +19 -0
  80. package/src/conversation-id.ts +199 -0
  81. package/src/dedup-migrations.test.ts +90 -0
  82. package/src/dedup-migrations.ts +103 -0
  83. package/src/dedup.test.ts +95 -0
  84. package/src/dedup.ts +304 -0
  85. package/src/dedupe-key.ts +68 -0
  86. package/src/directory.static.ts +62 -0
  87. package/src/directory.test.ts +142 -0
  88. package/src/directory.ts +125 -0
  89. package/src/doc-schema.ts +183 -0
  90. package/src/doctor.test.ts +382 -0
  91. package/src/doctor.ts +876 -0
  92. package/src/docx-batch-insert.test.ts +117 -0
  93. package/src/docx-batch-insert.ts +223 -0
  94. package/src/docx-color-text.ts +154 -0
  95. package/src/docx-table-ops.test.ts +54 -0
  96. package/src/docx-table-ops.ts +316 -0
  97. package/src/docx-types.ts +39 -0
  98. package/src/docx.account-selection.test.ts +96 -0
  99. package/src/docx.test.ts +706 -0
  100. package/src/docx.ts +1598 -0
  101. package/src/drive-schema.ts +93 -0
  102. package/src/drive.test.ts +1240 -0
  103. package/src/drive.ts +830 -0
  104. package/src/dynamic-agent.test.ts +156 -0
  105. package/src/dynamic-agent.ts +144 -0
  106. package/src/event-types.ts +46 -0
  107. package/src/external-keys.test.ts +21 -0
  108. package/src/external-keys.ts +20 -0
  109. package/src/lifecycle.test-support.ts +223 -0
  110. package/src/media.test.ts +956 -0
  111. package/src/media.ts +1106 -0
  112. package/src/mention-target.types.ts +6 -0
  113. package/src/mention.ts +115 -0
  114. package/src/message-action-contract.ts +14 -0
  115. package/src/monitor-state-runtime-api.ts +8 -0
  116. package/src/monitor-transport-runtime-api.ts +11 -0
  117. package/src/monitor.account.ts +501 -0
  118. package/src/monitor.acp-init-failure.lifecycle.test-support.ts +215 -0
  119. package/src/monitor.bot-identity.ts +87 -0
  120. package/src/monitor.bot-menu-handler.ts +164 -0
  121. package/src/monitor.bot-menu.lifecycle.test-support.ts +221 -0
  122. package/src/monitor.bot-menu.test.ts +200 -0
  123. package/src/monitor.broadcast.reply-once.lifecycle.test-support.ts +265 -0
  124. package/src/monitor.card-action.lifecycle.test-support.ts +418 -0
  125. package/src/monitor.cleanup.test.ts +384 -0
  126. package/src/monitor.comment-notice-handler.ts +106 -0
  127. package/src/monitor.comment.test.ts +968 -0
  128. package/src/monitor.comment.ts +1386 -0
  129. package/src/monitor.lifecycle.test.ts +5 -0
  130. package/src/monitor.message-handler.ts +346 -0
  131. package/src/monitor.reaction.test.ts +770 -0
  132. package/src/monitor.startup.test.ts +232 -0
  133. package/src/monitor.startup.ts +76 -0
  134. package/src/monitor.state.defaults.test.ts +47 -0
  135. package/src/monitor.state.ts +171 -0
  136. package/src/monitor.synthetic-error.ts +19 -0
  137. package/src/monitor.test-mocks.ts +47 -0
  138. package/src/monitor.transport.ts +451 -0
  139. package/src/monitor.ts +104 -0
  140. package/src/monitor.webhook-e2e.test.ts +284 -0
  141. package/src/monitor.webhook-security.test.ts +394 -0
  142. package/src/monitor.webhook.test-helpers.ts +138 -0
  143. package/src/outbound-runtime-api.ts +2 -0
  144. package/src/outbound.test.ts +1255 -0
  145. package/src/outbound.ts +742 -0
  146. package/src/perm-schema.ts +53 -0
  147. package/src/perm.ts +171 -0
  148. package/src/pins.ts +109 -0
  149. package/src/policy.test.ts +224 -0
  150. package/src/policy.ts +322 -0
  151. package/src/post.test.ts +106 -0
  152. package/src/post.ts +276 -0
  153. package/src/presentation-card.ts +204 -0
  154. package/src/probe.test.ts +310 -0
  155. package/src/probe.ts +181 -0
  156. package/src/processing-claims.ts +60 -0
  157. package/src/qr-terminal.ts +2 -0
  158. package/src/reactions.ts +124 -0
  159. package/src/reasoning-preview.test.ts +114 -0
  160. package/src/reasoning-preview.ts +29 -0
  161. package/src/reply-dispatcher-runtime-api.ts +8 -0
  162. package/src/reply-dispatcher.test.ts +2009 -0
  163. package/src/reply-dispatcher.ts +865 -0
  164. package/src/runtime.ts +10 -0
  165. package/src/secret-contract.ts +146 -0
  166. package/src/secret-input.ts +2 -0
  167. package/src/security-audit-shared.ts +70 -0
  168. package/src/security-audit.test.ts +60 -0
  169. package/src/security-audit.ts +2 -0
  170. package/src/send-result.ts +81 -0
  171. package/src/send-target.test.ts +87 -0
  172. package/src/send-target.ts +36 -0
  173. package/src/send.reply-fallback.test.ts +418 -0
  174. package/src/send.test.ts +661 -0
  175. package/src/send.ts +860 -0
  176. package/src/sequential-key.test.ts +73 -0
  177. package/src/sequential-key.ts +29 -0
  178. package/src/sequential-queue.test.ts +184 -0
  179. package/src/sequential-queue.ts +90 -0
  180. package/src/session-conversation.ts +42 -0
  181. package/src/session-route.ts +49 -0
  182. package/src/setup-core.ts +52 -0
  183. package/src/setup-surface.test.ts +485 -0
  184. package/src/setup-surface.ts +620 -0
  185. package/src/streaming-card.test.ts +549 -0
  186. package/src/streaming-card.ts +611 -0
  187. package/src/subagent-hooks.test.ts +632 -0
  188. package/src/subagent-hooks.ts +414 -0
  189. package/src/targets.ts +98 -0
  190. package/src/test-support/lifecycle-test-support.ts +459 -0
  191. package/src/thread-bindings.test.ts +181 -0
  192. package/src/thread-bindings.ts +332 -0
  193. package/src/tool-account-routing.test.ts +419 -0
  194. package/src/tool-account.test.ts +45 -0
  195. package/src/tool-account.ts +98 -0
  196. package/src/tool-factory-test-harness.ts +83 -0
  197. package/src/tool-result.test.ts +33 -0
  198. package/src/tool-result.ts +17 -0
  199. package/src/tools-config.test.ts +52 -0
  200. package/src/tools-config.ts +29 -0
  201. package/src/types.ts +111 -0
  202. package/src/typing.test.ts +145 -0
  203. package/src/typing.ts +215 -0
  204. package/src/wiki-schema.ts +70 -0
  205. package/src/wiki.ts +271 -0
  206. package/subagent-hooks-api.ts +22 -0
  207. package/tsconfig.json +16 -0
@@ -0,0 +1,485 @@
1
+ // Feishu tests cover setup surface plugin behavior.
2
+ import {
3
+ createNonExitingRuntimeEnv,
4
+ createPluginSetupWizardConfigure,
5
+ createPluginSetupWizardStatus,
6
+ createTestWizardPrompter,
7
+ runSetupWizardConfigure,
8
+ } from "actagent/plugin-sdk/plugin-test-runtime";
9
+ import { afterAll, beforeEach, describe, expect, it, vi } from "vitest";
10
+ import type { FeishuProbeResult } from "./types.js";
11
+
12
+ const {
13
+ beginAppRegistrationMock,
14
+ getAppOwnerOpenIdMock,
15
+ initAppRegistrationMock,
16
+ pollAppRegistrationMock,
17
+ printQrCodeMock,
18
+ probeFeishuMock,
19
+ } = vi.hoisted(() => ({
20
+ beginAppRegistrationMock: vi.fn(),
21
+ getAppOwnerOpenIdMock: vi.fn(),
22
+ initAppRegistrationMock: vi.fn(),
23
+ pollAppRegistrationMock: vi.fn(),
24
+ printQrCodeMock: vi.fn(),
25
+ probeFeishuMock: vi.fn<() => Promise<FeishuProbeResult>>(async () => ({
26
+ ok: false,
27
+ error: "mocked",
28
+ })),
29
+ }));
30
+
31
+ vi.mock("./probe.js", () => ({
32
+ probeFeishu: probeFeishuMock,
33
+ }));
34
+
35
+ vi.mock("./app-registration.js", () => ({
36
+ initAppRegistration: initAppRegistrationMock,
37
+ beginAppRegistration: beginAppRegistrationMock,
38
+ pollAppRegistration: pollAppRegistrationMock,
39
+ printQrCode: printQrCodeMock,
40
+ getAppOwnerOpenId: getAppOwnerOpenIdMock,
41
+ }));
42
+
43
+ import { feishuPlugin } from "./channel.js";
44
+
45
+ const baseStatusContext = {
46
+ accountOverrides: {},
47
+ };
48
+
49
+ async function withEnvVars(values: Record<string, string | undefined>, run: () => Promise<void>) {
50
+ const previous = new Map<string, string | undefined>();
51
+ for (const [key, value] of Object.entries(values)) {
52
+ previous.set(key, process.env[key]);
53
+ if (value === undefined) {
54
+ delete process.env[key];
55
+ } else {
56
+ process.env[key] = value;
57
+ }
58
+ }
59
+
60
+ try {
61
+ await run();
62
+ } finally {
63
+ for (const [key, prior] of previous.entries()) {
64
+ if (prior === undefined) {
65
+ delete process.env[key];
66
+ } else {
67
+ process.env[key] = prior;
68
+ }
69
+ }
70
+ }
71
+ }
72
+
73
+ async function getStatusWithEnvRefs(params: { appIdKey: string; appSecretKey: string }) {
74
+ return await feishuGetStatus({
75
+ cfg: {
76
+ channels: {
77
+ feishu: {
78
+ appId: { source: "env", id: params.appIdKey, provider: "default" },
79
+ appSecret: { source: "env", id: params.appSecretKey, provider: "default" },
80
+ },
81
+ },
82
+ } as never,
83
+ ...baseStatusContext,
84
+ });
85
+ }
86
+
87
+ const feishuConfigure = createPluginSetupWizardConfigure(feishuPlugin);
88
+ const feishuGetStatus = createPluginSetupWizardStatus(feishuPlugin);
89
+
90
+ afterAll(() => {
91
+ vi.doUnmock("./probe.js");
92
+ vi.doUnmock("./app-registration.js");
93
+ vi.resetModules();
94
+ });
95
+
96
+ describe("feishu setup wizard", () => {
97
+ beforeEach(() => {
98
+ probeFeishuMock.mockReset();
99
+ probeFeishuMock.mockResolvedValue({ ok: false, error: "mocked" });
100
+ initAppRegistrationMock.mockReset();
101
+ initAppRegistrationMock.mockRejectedValue(new Error("mocked: scan-to-create not available"));
102
+ beginAppRegistrationMock.mockReset();
103
+ pollAppRegistrationMock.mockReset();
104
+ printQrCodeMock.mockReset();
105
+ printQrCodeMock.mockResolvedValue(undefined);
106
+ getAppOwnerOpenIdMock.mockReset();
107
+ getAppOwnerOpenIdMock.mockResolvedValue(undefined);
108
+ });
109
+
110
+ it("uses manual credentials by default instead of starting scan-to-create", async () => {
111
+ const text = vi.fn().mockResolvedValueOnce("cli_manual").mockResolvedValueOnce("secret_manual");
112
+ const prompter = createTestWizardPrompter({ text });
113
+
114
+ const result = await runSetupWizardConfigure({
115
+ configure: feishuConfigure,
116
+ cfg: {} as never,
117
+ prompter,
118
+ runtime: createNonExitingRuntimeEnv(),
119
+ });
120
+
121
+ expect(initAppRegistrationMock).not.toHaveBeenCalled();
122
+ expect(beginAppRegistrationMock).not.toHaveBeenCalled();
123
+ const feishuConfig = result.cfg.channels?.feishu;
124
+ expect(feishuConfig?.appId).toBe("cli_manual");
125
+ expect(feishuConfig?.appSecret).toBe("secret_manual");
126
+ expect(feishuConfig?.connectionMode).toBe("websocket");
127
+ expect(feishuConfig?.domain).toBe("feishu");
128
+ });
129
+
130
+ it("passes selected domain through scan-to-create and poll", async () => {
131
+ initAppRegistrationMock.mockResolvedValueOnce(undefined);
132
+ beginAppRegistrationMock.mockResolvedValueOnce({
133
+ deviceCode: "device-code",
134
+ qrUrl: "https://accounts.larksuite.com/qr",
135
+ userCode: "user-code",
136
+ interval: 1,
137
+ expireIn: 10,
138
+ });
139
+ pollAppRegistrationMock.mockResolvedValueOnce({
140
+ status: "success",
141
+ result: {
142
+ appId: "cli_lark",
143
+ appSecret: "secret_lark",
144
+ domain: "lark",
145
+ openId: "ou_owner",
146
+ },
147
+ });
148
+ const prompter = createTestWizardPrompter({
149
+ select: vi
150
+ .fn()
151
+ .mockResolvedValueOnce("scan")
152
+ .mockResolvedValueOnce("lark")
153
+ .mockResolvedValueOnce("open") as never,
154
+ });
155
+
156
+ const result = await runSetupWizardConfigure({
157
+ configure: feishuConfigure,
158
+ cfg: {} as never,
159
+ prompter,
160
+ runtime: createNonExitingRuntimeEnv(),
161
+ });
162
+
163
+ expect(initAppRegistrationMock).toHaveBeenCalledWith("lark");
164
+ expect(beginAppRegistrationMock).toHaveBeenCalledWith("lark");
165
+ const [pollOptions] = pollAppRegistrationMock.mock.calls.at(0) ?? [];
166
+ expect(pollOptions?.deviceCode).toBe("device-code");
167
+ expect(pollOptions?.initialDomain).toBe("lark");
168
+ expect(pollOptions?.tp).toBe("ob_cli_app");
169
+ const feishuConfig = result.cfg.channels?.feishu;
170
+ expect(feishuConfig?.appId).toBe("cli_lark");
171
+ expect(feishuConfig?.appSecret).toBe("secret_lark");
172
+ expect(feishuConfig?.domain).toBe("lark");
173
+ expect(feishuConfig?.groupPolicy).toBe("open");
174
+ expect(feishuConfig?.requireMention).toBe(true);
175
+ });
176
+
177
+ it("falls back to manual credentials when selected scan-to-create is unavailable", async () => {
178
+ const text = vi
179
+ .fn()
180
+ .mockResolvedValueOnce("cli_from_fallback")
181
+ .mockResolvedValueOnce("secret_from_fallback");
182
+ const prompter = createTestWizardPrompter({
183
+ text,
184
+ select: vi
185
+ .fn()
186
+ .mockResolvedValueOnce("scan")
187
+ .mockResolvedValueOnce("feishu")
188
+ .mockResolvedValueOnce("allowlist") as never,
189
+ });
190
+
191
+ const result = await runSetupWizardConfigure({
192
+ configure: feishuConfigure,
193
+ cfg: {} as never,
194
+ prompter,
195
+ runtime: createNonExitingRuntimeEnv(),
196
+ });
197
+
198
+ expect(initAppRegistrationMock).toHaveBeenCalledWith("feishu");
199
+ expect(beginAppRegistrationMock).not.toHaveBeenCalled();
200
+ const feishuConfig = result.cfg.channels?.feishu;
201
+ expect(feishuConfig?.appId).toBe("cli_from_fallback");
202
+ expect(feishuConfig?.appSecret).toBe("secret_from_fallback");
203
+ expect(feishuConfig?.domain).toBe("feishu");
204
+ });
205
+
206
+ it("prompts over SecretRef appId/appSecret config objects", async () => {
207
+ const text = vi
208
+ .fn()
209
+ .mockResolvedValueOnce("cli_from_prompt")
210
+ .mockResolvedValueOnce("secret_from_prompt");
211
+ const prompter = createTestWizardPrompter({
212
+ text,
213
+ confirm: vi.fn(async () => true),
214
+ select: vi.fn(
215
+ async ({ initialValue }: { initialValue?: string }) => initialValue ?? "bot",
216
+ ) as never,
217
+ });
218
+
219
+ const result = await runSetupWizardConfigure({
220
+ configure: feishuConfigure,
221
+ cfg: {
222
+ channels: {
223
+ feishu: {
224
+ appId: { source: "env", id: "FEISHU_APP_ID", provider: "default" },
225
+ appSecret: { source: "env", id: "FEISHU_APP_SECRET", provider: "default" },
226
+ },
227
+ },
228
+ } as never,
229
+ prompter,
230
+ runtime: createNonExitingRuntimeEnv(),
231
+ });
232
+
233
+ expect(result.cfg.channels?.feishu).toEqual({
234
+ appId: "cli_from_prompt",
235
+ appSecret: "secret_from_prompt",
236
+ enabled: true,
237
+ domain: "feishu",
238
+ connectionMode: "websocket",
239
+ groupPolicy: "allowlist",
240
+ });
241
+ });
242
+ });
243
+
244
+ describe("feishu setup wizard status", () => {
245
+ beforeEach(() => {
246
+ probeFeishuMock.mockReset();
247
+ probeFeishuMock.mockResolvedValue({ ok: false, error: "mocked" });
248
+ });
249
+
250
+ it("treats SecretRef appSecret as configured when appId is present", async () => {
251
+ const status = await feishuGetStatus({
252
+ cfg: {
253
+ channels: {
254
+ feishu: {
255
+ appId: "cli_a123456",
256
+ appSecret: {
257
+ source: "env",
258
+ provider: "default",
259
+ id: "FEISHU_APP_SECRET",
260
+ },
261
+ },
262
+ },
263
+ } as never,
264
+ accountOverrides: {},
265
+ });
266
+
267
+ expect(status.configured).toBe(true);
268
+ });
269
+
270
+ it("probes the resolved default account in multi-account config", async () => {
271
+ probeFeishuMock.mockResolvedValueOnce({ ok: true, botName: "Feishu Main" });
272
+
273
+ const status = await feishuGetStatus({
274
+ cfg: {
275
+ channels: {
276
+ feishu: {
277
+ enabled: true,
278
+ defaultAccount: "main-bot",
279
+ accounts: {
280
+ "main-bot": {
281
+ appId: "cli_main",
282
+ appSecret: "main-app-secret", // pragma: allowlist secret
283
+ connectionMode: "websocket",
284
+ },
285
+ },
286
+ },
287
+ },
288
+ } as never,
289
+ ...baseStatusContext,
290
+ });
291
+
292
+ expect(status.configured).toBe(true);
293
+ expect(status.statusLines).toEqual(["Feishu: connected as Feishu Main"]);
294
+ expect(probeFeishuMock).toHaveBeenCalledWith({
295
+ accountId: "main-bot",
296
+ selectionSource: "explicit-default",
297
+ enabled: true,
298
+ configured: true,
299
+ name: undefined,
300
+ appId: "cli_main",
301
+ appSecret: "main-app-secret", // pragma: allowlist secret
302
+ encryptKey: undefined,
303
+ verificationToken: undefined,
304
+ domain: "feishu",
305
+ config: {
306
+ enabled: true,
307
+ appId: "cli_main",
308
+ appSecret: "main-app-secret", // pragma: allowlist secret
309
+ connectionMode: "websocket",
310
+ },
311
+ });
312
+ });
313
+
314
+ it("localizes existing bot setup prompts and status lines", async () => {
315
+ const previousLocale = process.env.ACTAGENT_LOCALE;
316
+ process.env.ACTAGENT_LOCALE = "zh-CN";
317
+ const confirm = vi.fn(async () => true);
318
+ const note = vi.fn(async () => {});
319
+ const prompter = createTestWizardPrompter({
320
+ confirm,
321
+ note,
322
+ });
323
+
324
+ try {
325
+ await runSetupWizardConfigure({
326
+ configure: feishuConfigure,
327
+ cfg: {
328
+ channels: {
329
+ feishu: {
330
+ appId: "cli_a123456",
331
+ appSecret: "sample-app-credential", // pragma: allowlist secret
332
+ },
333
+ },
334
+ } as never,
335
+ prompter,
336
+ runtime: createNonExitingRuntimeEnv(),
337
+ });
338
+
339
+ expect(confirm).toHaveBeenCalledWith(
340
+ expect.objectContaining({
341
+ message: "发现已有 bot(App ID:cli_a123456)。用于本次设置?",
342
+ }),
343
+ );
344
+ expect(note).toHaveBeenCalledWith("Bot 已配置。", "");
345
+ } finally {
346
+ if (previousLocale === undefined) {
347
+ delete process.env.ACTAGENT_LOCALE;
348
+ } else {
349
+ process.env.ACTAGENT_LOCALE = previousLocale;
350
+ }
351
+ }
352
+ });
353
+
354
+ it("localizes new bot setup prompts and progress", async () => {
355
+ const previousLocale = process.env.ACTAGENT_LOCALE;
356
+ process.env.ACTAGENT_LOCALE = "zh-CN";
357
+ const note = vi.fn(async () => {});
358
+ const stop = vi.fn();
359
+ const progress = vi.fn(() => ({ update: vi.fn(), stop }));
360
+ const select = vi.fn(async ({ message }: { message: string }) => {
361
+ if (message === "你想如何连接 Feishu?") {
362
+ return "manual";
363
+ }
364
+ if (message === "选择 Feishu 域名?") {
365
+ return "feishu";
366
+ }
367
+ if (message === "群聊策略") {
368
+ return "allowlist";
369
+ }
370
+ return "feishu";
371
+ });
372
+ const text = vi
373
+ .fn()
374
+ .mockResolvedValueOnce("cli_from_prompt")
375
+ .mockResolvedValueOnce("secret_from_prompt");
376
+ const prompter = createTestWizardPrompter({
377
+ note,
378
+ progress,
379
+ select: select as never,
380
+ text,
381
+ });
382
+
383
+ try {
384
+ await runSetupWizardConfigure({
385
+ configure: feishuConfigure,
386
+ cfg: {} as never,
387
+ prompter,
388
+ runtime: createNonExitingRuntimeEnv(),
389
+ });
390
+
391
+ expect(select).toHaveBeenCalledWith(
392
+ expect.objectContaining({
393
+ message: "你想如何连接 Feishu?",
394
+ options: [
395
+ { value: "manual", label: "手动输入 App ID 和 App Secret" },
396
+ { value: "scan", label: "扫描二维码自动创建 bot" },
397
+ ],
398
+ }),
399
+ );
400
+ expect(select).toHaveBeenCalledWith(
401
+ expect.objectContaining({
402
+ message: "选择 Feishu 域名?",
403
+ options: [
404
+ { value: "feishu", label: "Feishu (feishu.cn) - 中国" },
405
+ { value: "lark", label: "Lark (larksuite.com) - 国际版" },
406
+ ],
407
+ }),
408
+ );
409
+ expect(text).toHaveBeenCalledWith(
410
+ expect.objectContaining({
411
+ message: "输入 Feishu App ID",
412
+ }),
413
+ );
414
+ expect(select).toHaveBeenCalledWith(
415
+ expect.objectContaining({
416
+ message: "群聊策略",
417
+ options: [
418
+ { value: "allowlist", label: "允许列表 - 只在指定群中响应" },
419
+ { value: "open", label: "开放 - 在所有群中响应(需要提及)" },
420
+ { value: "disabled", label: "禁用 - 不响应群聊" },
421
+ ],
422
+ }),
423
+ );
424
+ expect(progress).toHaveBeenCalledWith("正在配置...");
425
+ expect(stop).toHaveBeenCalledWith("Bot 已配置。");
426
+ } finally {
427
+ if (previousLocale === undefined) {
428
+ delete process.env.ACTAGENT_LOCALE;
429
+ } else {
430
+ process.env.ACTAGENT_LOCALE = previousLocale;
431
+ }
432
+ }
433
+ });
434
+
435
+ it("does not fallback to top-level appId when account explicitly sets empty appId", async () => {
436
+ const status = await feishuGetStatus({
437
+ cfg: {
438
+ channels: {
439
+ feishu: {
440
+ appId: "top_level_app",
441
+ accounts: {
442
+ main: {
443
+ appId: "",
444
+ appSecret: "sample-app-credential", // pragma: allowlist secret
445
+ },
446
+ },
447
+ },
448
+ },
449
+ } as never,
450
+ ...baseStatusContext,
451
+ });
452
+
453
+ expect(status.configured).toBe(false);
454
+ });
455
+
456
+ it("treats env SecretRef appId as not configured when env var is missing", async () => {
457
+ const appIdKey = "FEISHU_APP_ID_STATUS_MISSING_TEST";
458
+ const appSecretKey = "FEISHU_APP_CREDENTIAL_STATUS_MISSING_TEST"; // pragma: allowlist secret
459
+ await withEnvVars(
460
+ {
461
+ [appIdKey]: undefined,
462
+ [appSecretKey]: "env-credential-456", // pragma: allowlist secret
463
+ },
464
+ async () => {
465
+ const status = await getStatusWithEnvRefs({ appIdKey, appSecretKey });
466
+ expect(status.configured).toBe(false);
467
+ },
468
+ );
469
+ });
470
+
471
+ it("treats env SecretRef appId/appSecret as configured in status", async () => {
472
+ const appIdKey = "FEISHU_APP_ID_STATUS_TEST";
473
+ const appSecretKey = "FEISHU_APP_CREDENTIAL_STATUS_TEST"; // pragma: allowlist secret
474
+ await withEnvVars(
475
+ {
476
+ [appIdKey]: "cli_env_123",
477
+ [appSecretKey]: "env-credential-456", // pragma: allowlist secret
478
+ },
479
+ async () => {
480
+ const status = await getStatusWithEnvRefs({ appIdKey, appSecretKey });
481
+ expect(status.configured).toBe(true);
482
+ },
483
+ );
484
+ });
485
+ });