@kodelyth/feishu 2026.5.39 → 2026.5.42

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