@openclaw/feishu 2026.3.13 → 2026.5.2-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.
Files changed (187) hide show
  1. package/api.ts +31 -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/index.ts +70 -53
  6. package/openclaw.plugin.json +1827 -4
  7. package/package.json +32 -7
  8. package/runtime-api.ts +55 -0
  9. package/secret-contract-api.ts +5 -0
  10. package/security-contract-api.ts +1 -0
  11. package/session-key-api.ts +1 -0
  12. package/setup-api.ts +3 -0
  13. package/setup-entry.test.ts +14 -0
  14. package/setup-entry.ts +13 -0
  15. package/src/accounts.test.ts +95 -7
  16. package/src/accounts.ts +199 -117
  17. package/src/app-registration.ts +331 -0
  18. package/src/approval-auth.test.ts +24 -0
  19. package/src/approval-auth.ts +25 -0
  20. package/src/async.test.ts +35 -0
  21. package/src/async.ts +43 -1
  22. package/src/audio-preflight.runtime.ts +9 -0
  23. package/src/bitable.test.ts +131 -0
  24. package/src/bitable.ts +59 -22
  25. package/src/bot-content.ts +474 -0
  26. package/src/bot-group-name.test.ts +108 -0
  27. package/src/bot-runtime-api.ts +12 -0
  28. package/src/bot-sender-name.ts +125 -0
  29. package/src/bot.broadcast.test.ts +463 -0
  30. package/src/bot.card-action.test.ts +519 -5
  31. package/src/bot.checkBotMentioned.test.ts +92 -20
  32. package/src/bot.helpers.test.ts +118 -0
  33. package/src/bot.stripBotMention.test.ts +13 -21
  34. package/src/bot.test.ts +1334 -401
  35. package/src/bot.ts +778 -775
  36. package/src/card-action.ts +408 -40
  37. package/src/card-interaction.test.ts +129 -0
  38. package/src/card-interaction.ts +159 -0
  39. package/src/card-test-helpers.ts +47 -0
  40. package/src/card-ux-approval.ts +65 -0
  41. package/src/card-ux-launcher.test.ts +99 -0
  42. package/src/card-ux-launcher.ts +121 -0
  43. package/src/card-ux-shared.ts +33 -0
  44. package/src/channel-runtime-api.ts +16 -0
  45. package/src/channel.runtime.ts +47 -0
  46. package/src/channel.test.ts +914 -3
  47. package/src/channel.ts +1253 -309
  48. package/src/chat-schema.ts +5 -4
  49. package/src/chat.test.ts +135 -28
  50. package/src/chat.ts +68 -10
  51. package/src/client.test.ts +212 -103
  52. package/src/client.ts +115 -21
  53. package/src/comment-dispatcher-runtime-api.ts +6 -0
  54. package/src/comment-dispatcher.test.ts +169 -0
  55. package/src/comment-dispatcher.ts +107 -0
  56. package/src/comment-handler-runtime-api.ts +3 -0
  57. package/src/comment-handler.test.ts +486 -0
  58. package/src/comment-handler.ts +309 -0
  59. package/src/comment-reaction.test.ts +166 -0
  60. package/src/comment-reaction.ts +259 -0
  61. package/src/comment-shared.test.ts +182 -0
  62. package/src/comment-shared.ts +406 -0
  63. package/src/comment-target.ts +44 -0
  64. package/src/config-schema.test.ts +63 -1
  65. package/src/config-schema.ts +31 -4
  66. package/src/conversation-id.test.ts +18 -0
  67. package/src/conversation-id.ts +199 -0
  68. package/src/dedup-runtime-api.ts +1 -0
  69. package/src/dedup.ts +33 -95
  70. package/src/directory.static.ts +61 -0
  71. package/src/directory.test.ts +116 -20
  72. package/src/directory.ts +60 -92
  73. package/src/doc-schema.ts +1 -1
  74. package/src/docx-batch-insert.test.ts +39 -38
  75. package/src/docx-batch-insert.ts +55 -19
  76. package/src/docx-color-text.ts +9 -4
  77. package/src/docx-table-ops.test.ts +53 -0
  78. package/src/docx-table-ops.ts +52 -34
  79. package/src/docx-types.ts +38 -0
  80. package/src/docx.account-selection.test.ts +12 -3
  81. package/src/docx.test.ts +314 -74
  82. package/src/docx.ts +278 -122
  83. package/src/drive-schema.ts +47 -1
  84. package/src/drive.test.ts +1219 -0
  85. package/src/drive.ts +614 -13
  86. package/src/dynamic-agent.ts +10 -4
  87. package/src/event-types.ts +45 -0
  88. package/src/external-keys.ts +1 -1
  89. package/src/lifecycle.test-support.ts +220 -0
  90. package/src/media.test.ts +403 -26
  91. package/src/media.ts +509 -132
  92. package/src/mention-target.types.ts +5 -0
  93. package/src/mention.ts +32 -51
  94. package/src/message-action-contract.ts +13 -0
  95. package/src/monitor-state-runtime-api.ts +7 -0
  96. package/src/monitor-transport-runtime-api.ts +7 -0
  97. package/src/monitor.account.ts +218 -312
  98. package/src/monitor.acp-init-failure.lifecycle.test-support.ts +219 -0
  99. package/src/monitor.bot-identity.ts +86 -0
  100. package/src/monitor.bot-menu-handler.ts +165 -0
  101. package/src/monitor.bot-menu.lifecycle.test-support.ts +224 -0
  102. package/src/monitor.bot-menu.test.ts +178 -0
  103. package/src/monitor.broadcast.reply-once.lifecycle.test-support.ts +264 -0
  104. package/src/monitor.card-action.lifecycle.test-support.ts +373 -0
  105. package/src/monitor.cleanup.test.ts +376 -0
  106. package/src/monitor.comment-notice-handler.ts +105 -0
  107. package/src/monitor.comment.test.ts +937 -0
  108. package/src/monitor.comment.ts +1386 -0
  109. package/src/monitor.lifecycle.test.ts +4 -0
  110. package/src/monitor.message-handler.ts +339 -0
  111. package/src/monitor.reaction.lifecycle.test-support.ts +68 -0
  112. package/src/monitor.reaction.test.ts +108 -48
  113. package/src/monitor.startup.test.ts +11 -9
  114. package/src/monitor.startup.ts +26 -16
  115. package/src/monitor.state.ts +20 -5
  116. package/src/monitor.synthetic-error.ts +18 -0
  117. package/src/monitor.test-mocks.ts +2 -2
  118. package/src/monitor.transport.ts +220 -60
  119. package/src/monitor.ts +15 -10
  120. package/src/monitor.webhook-e2e.test.ts +65 -7
  121. package/src/monitor.webhook-security.test.ts +122 -0
  122. package/src/monitor.webhook.test-helpers.ts +44 -26
  123. package/src/outbound-runtime-api.ts +1 -0
  124. package/src/outbound.test.ts +616 -37
  125. package/src/outbound.ts +623 -81
  126. package/src/perm-schema.ts +1 -1
  127. package/src/perm.ts +1 -7
  128. package/src/pins.ts +108 -0
  129. package/src/policy.test.ts +297 -117
  130. package/src/policy.ts +142 -29
  131. package/src/post.ts +7 -6
  132. package/src/probe.test.ts +14 -9
  133. package/src/probe.ts +26 -16
  134. package/src/processing-claims.ts +59 -0
  135. package/src/qr-terminal.ts +1 -0
  136. package/src/reactions.ts +4 -34
  137. package/src/reasoning-preview.test.ts +59 -0
  138. package/src/reasoning-preview.ts +20 -0
  139. package/src/reply-dispatcher-runtime-api.ts +7 -0
  140. package/src/reply-dispatcher.test.ts +660 -29
  141. package/src/reply-dispatcher.ts +407 -154
  142. package/src/runtime.ts +6 -3
  143. package/src/secret-contract.ts +145 -0
  144. package/src/secret-input.ts +1 -13
  145. package/src/security-audit-shared.ts +69 -0
  146. package/src/security-audit.test.ts +61 -0
  147. package/src/security-audit.ts +1 -0
  148. package/src/send-result.ts +1 -1
  149. package/src/send-target.test.ts +9 -3
  150. package/src/send-target.ts +10 -4
  151. package/src/send.reply-fallback.test.ts +105 -2
  152. package/src/send.test.ts +386 -4
  153. package/src/send.ts +414 -95
  154. package/src/sequential-key.test.ts +72 -0
  155. package/src/sequential-key.ts +28 -0
  156. package/src/sequential-queue.test.ts +92 -0
  157. package/src/sequential-queue.ts +16 -0
  158. package/src/session-conversation.ts +42 -0
  159. package/src/session-route.ts +48 -0
  160. package/src/setup-core.ts +51 -0
  161. package/src/{onboarding.test.ts → setup-surface.test.ts} +52 -21
  162. package/src/setup-surface.ts +581 -0
  163. package/src/streaming-card.test.ts +138 -2
  164. package/src/streaming-card.ts +134 -18
  165. package/src/subagent-hooks.test.ts +603 -0
  166. package/src/subagent-hooks.ts +397 -0
  167. package/src/targets.ts +3 -13
  168. package/src/test-support/lifecycle-test-support.ts +453 -0
  169. package/src/thread-bindings.test.ts +143 -0
  170. package/src/thread-bindings.ts +330 -0
  171. package/src/tool-account-routing.test.ts +66 -8
  172. package/src/tool-account.test.ts +44 -0
  173. package/src/tool-account.ts +40 -17
  174. package/src/tool-factory-test-harness.ts +11 -8
  175. package/src/tool-result.ts +3 -1
  176. package/src/tools-config.ts +1 -1
  177. package/src/types.ts +16 -15
  178. package/src/typing.ts +10 -6
  179. package/src/wiki-schema.ts +1 -1
  180. package/src/wiki.ts +1 -7
  181. package/subagent-hooks-api.ts +31 -0
  182. package/tsconfig.json +16 -0
  183. package/src/feishu-command-handler.ts +0 -59
  184. package/src/onboarding.status.test.ts +0 -25
  185. package/src/onboarding.ts +0 -489
  186. package/src/send-message.ts +0 -71
  187. package/src/targets.test.ts +0 -70
@@ -0,0 +1,72 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import type { FeishuMessageEvent } from "./bot.js";
3
+ import { getFeishuSequentialKey } from "./sequential-key.js";
4
+
5
+ function createTextEvent(params: {
6
+ text: string;
7
+ messageId?: string;
8
+ chatId?: string;
9
+ }): FeishuMessageEvent {
10
+ return {
11
+ sender: {
12
+ sender_id: {
13
+ open_id: "ou_sender_1",
14
+ user_id: "ou_user_1",
15
+ },
16
+ sender_type: "user",
17
+ },
18
+ message: {
19
+ message_id: params.messageId ?? "om_message_1",
20
+ chat_id: params.chatId ?? "oc_dm_chat",
21
+ chat_type: "p2p",
22
+ message_type: "text",
23
+ content: JSON.stringify({ text: params.text }),
24
+ },
25
+ } as FeishuMessageEvent;
26
+ }
27
+
28
+ describe("getFeishuSequentialKey", () => {
29
+ it.each([
30
+ [createTextEvent({ text: "hello" }), "feishu:default:oc_dm_chat"],
31
+ [createTextEvent({ text: "/status" }), "feishu:default:oc_dm_chat"],
32
+ [createTextEvent({ text: "/stop" }), "feishu:default:oc_dm_chat:control"],
33
+ [createTextEvent({ text: "/btw what changed?" }), "feishu:default:oc_dm_chat:btw"],
34
+ ])("resolves sequential key %#", (event, expected) => {
35
+ expect(
36
+ getFeishuSequentialKey({
37
+ accountId: "default",
38
+ event,
39
+ }),
40
+ ).toBe(expected);
41
+ });
42
+
43
+ it("keeps /btw on a stable per-chat lane across different message ids", () => {
44
+ const first = createTextEvent({ text: "/btw one", messageId: "om_message_1" });
45
+ const second = createTextEvent({ text: "/btw two", messageId: "om_message_2" });
46
+
47
+ expect(
48
+ getFeishuSequentialKey({
49
+ accountId: "default",
50
+ event: first,
51
+ }),
52
+ ).toBe("feishu:default:oc_dm_chat:btw");
53
+ expect(
54
+ getFeishuSequentialKey({
55
+ accountId: "default",
56
+ event: second,
57
+ }),
58
+ ).toBe("feishu:default:oc_dm_chat:btw");
59
+ });
60
+
61
+ it("falls back to a stable btw lane when the message id is unavailable", () => {
62
+ const event = createTextEvent({ text: "/btw what changed?" });
63
+ delete (event.message as { message_id?: string }).message_id;
64
+
65
+ expect(
66
+ getFeishuSequentialKey({
67
+ accountId: "default",
68
+ event,
69
+ }),
70
+ ).toBe("feishu:default:oc_dm_chat:btw");
71
+ });
72
+ });
@@ -0,0 +1,28 @@
1
+ import {
2
+ isAbortRequestText,
3
+ isBtwRequestText,
4
+ } from "openclaw/plugin-sdk/command-primitives-runtime";
5
+ import { parseFeishuMessageEvent, type FeishuMessageEvent } from "./bot.js";
6
+
7
+ export function getFeishuSequentialKey(params: {
8
+ accountId: string;
9
+ event: FeishuMessageEvent;
10
+ botOpenId?: string;
11
+ botName?: string;
12
+ }): string {
13
+ const { accountId, event, botOpenId, botName } = params;
14
+ const chatId = event.message.chat_id?.trim() || "unknown";
15
+ const baseKey = `feishu:${accountId}:${chatId}`;
16
+ const parsed = parseFeishuMessageEvent(event, botOpenId, botName);
17
+ const text = parsed.content.trim();
18
+
19
+ if (isAbortRequestText(text)) {
20
+ return `${baseKey}:control`;
21
+ }
22
+
23
+ if (isBtwRequestText(text)) {
24
+ return `${baseKey}:btw`;
25
+ }
26
+
27
+ return baseKey;
28
+ }
@@ -0,0 +1,92 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { createSequentialQueue } from "./sequential-queue.js";
3
+
4
+ function createDeferred() {
5
+ let resolve!: () => void;
6
+ const promise = new Promise<void>((res) => {
7
+ resolve = res;
8
+ });
9
+ return { promise, resolve };
10
+ }
11
+
12
+ describe("createSequentialQueue", () => {
13
+ it("serializes tasks for the same key", async () => {
14
+ const enqueue = createSequentialQueue();
15
+ const gate = createDeferred();
16
+ const order: string[] = [];
17
+
18
+ const first = enqueue("feishu:default:chat-1", async () => {
19
+ order.push("first:start");
20
+ await gate.promise;
21
+ order.push("first:end");
22
+ });
23
+ const second = enqueue("feishu:default:chat-1", async () => {
24
+ order.push("second:start");
25
+ order.push("second:end");
26
+ });
27
+
28
+ await Promise.resolve();
29
+ expect(order).toEqual(["first:start"]);
30
+
31
+ gate.resolve();
32
+ await Promise.all([first, second]);
33
+
34
+ expect(order).toEqual(["first:start", "first:end", "second:start", "second:end"]);
35
+ });
36
+
37
+ it("allows different keys to run concurrently", async () => {
38
+ const enqueue = createSequentialQueue();
39
+ const gateA = createDeferred();
40
+ const gateB = createDeferred();
41
+ const order: string[] = [];
42
+
43
+ const first = enqueue("feishu:default:chat-1", async () => {
44
+ order.push("chat-1:start");
45
+ await gateA.promise;
46
+ order.push("chat-1:end");
47
+ });
48
+ const second = enqueue("feishu:default:chat-1:btw:om_2", async () => {
49
+ order.push("btw:start");
50
+ await gateB.promise;
51
+ order.push("btw:end");
52
+ });
53
+
54
+ await Promise.resolve();
55
+ expect(order).toEqual(["chat-1:start", "btw:start"]);
56
+
57
+ gateA.resolve();
58
+ gateB.resolve();
59
+ await Promise.all([first, second]);
60
+
61
+ expect(order).toContain("chat-1:end");
62
+ expect(order).toContain("btw:end");
63
+ });
64
+
65
+ it("does not leak unhandled rejections when a queued task fails", async () => {
66
+ const enqueue = createSequentialQueue();
67
+ const unhandled: unknown[] = [];
68
+ const onUnhandledRejection = (reason: unknown) => {
69
+ unhandled.push(reason);
70
+ };
71
+ process.on("unhandledRejection", onUnhandledRejection);
72
+
73
+ try {
74
+ await expect(
75
+ enqueue("feishu:default:chat-1", async () => {
76
+ throw new Error("boom");
77
+ }),
78
+ ).rejects.toThrow("boom");
79
+
80
+ await new Promise((resolve) => setTimeout(resolve, 0));
81
+ expect(unhandled).toEqual([]);
82
+
83
+ await expect(
84
+ enqueue("feishu:default:chat-1", async () => {
85
+ return;
86
+ }),
87
+ ).resolves.toBeUndefined();
88
+ } finally {
89
+ process.off("unhandledRejection", onUnhandledRejection);
90
+ }
91
+ });
92
+ });
@@ -0,0 +1,16 @@
1
+ export function createSequentialQueue() {
2
+ const queues = new Map<string, Promise<void>>();
3
+
4
+ return (key: string, task: () => Promise<void>): Promise<void> => {
5
+ const previous = queues.get(key) ?? Promise.resolve();
6
+ const next = previous.then(task, task);
7
+ queues.set(key, next);
8
+ const cleanup = () => {
9
+ if (queues.get(key) === next) {
10
+ queues.delete(key);
11
+ }
12
+ };
13
+ next.then(cleanup, cleanup);
14
+ return next;
15
+ };
16
+ }
@@ -0,0 +1,42 @@
1
+ import { buildFeishuConversationId, parseFeishuConversationId } from "./conversation-id.js";
2
+
3
+ function resolveFeishuParentConversationCandidates(rawId: string): string[] {
4
+ const parsed = parseFeishuConversationId({ conversationId: rawId });
5
+ if (!parsed) {
6
+ return [];
7
+ }
8
+ switch (parsed.scope) {
9
+ case "group_topic_sender":
10
+ return [
11
+ buildFeishuConversationId({
12
+ chatId: parsed.chatId,
13
+ scope: "group_topic",
14
+ topicId: parsed.topicId,
15
+ }),
16
+ parsed.chatId,
17
+ ];
18
+ case "group_topic":
19
+ case "group_sender":
20
+ return [parsed.chatId];
21
+ case "group":
22
+ default:
23
+ return [];
24
+ }
25
+ }
26
+
27
+ export function resolveFeishuSessionConversation(params: {
28
+ kind: "group" | "channel";
29
+ rawId: string;
30
+ }) {
31
+ const parsed = parseFeishuConversationId({ conversationId: params.rawId });
32
+ if (!parsed) {
33
+ return null;
34
+ }
35
+ return {
36
+ id: parsed.canonicalConversationId,
37
+ baseConversationId: parsed.chatId,
38
+ parentConversationCandidates: resolveFeishuParentConversationCandidates(
39
+ parsed.canonicalConversationId,
40
+ ),
41
+ };
42
+ }
@@ -0,0 +1,48 @@
1
+ import {
2
+ buildChannelOutboundSessionRoute,
3
+ stripChannelTargetPrefix,
4
+ type ChannelOutboundSessionRouteParams,
5
+ } from "openclaw/plugin-sdk/channel-core";
6
+ import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
7
+
8
+ export function resolveFeishuOutboundSessionRoute(params: ChannelOutboundSessionRouteParams) {
9
+ let trimmed = stripChannelTargetPrefix(params.target, "feishu", "lark");
10
+ if (!trimmed) {
11
+ return null;
12
+ }
13
+
14
+ const lower = normalizeLowercaseStringOrEmpty(trimmed);
15
+ let isGroup = false;
16
+ let typeExplicit = false;
17
+
18
+ if (lower.startsWith("group:") || lower.startsWith("chat:") || lower.startsWith("channel:")) {
19
+ trimmed = trimmed.replace(/^(group|chat|channel):/i, "").trim();
20
+ isGroup = true;
21
+ typeExplicit = true;
22
+ } else if (lower.startsWith("user:") || lower.startsWith("dm:")) {
23
+ trimmed = trimmed.replace(/^(user|dm):/i, "").trim();
24
+ isGroup = false;
25
+ typeExplicit = true;
26
+ }
27
+
28
+ if (!typeExplicit) {
29
+ const idLower = normalizeLowercaseStringOrEmpty(trimmed);
30
+ if (idLower.startsWith("ou_") || idLower.startsWith("on_")) {
31
+ isGroup = false;
32
+ }
33
+ }
34
+
35
+ return buildChannelOutboundSessionRoute({
36
+ cfg: params.cfg,
37
+ agentId: params.agentId,
38
+ channel: "feishu",
39
+ accountId: params.accountId,
40
+ peer: {
41
+ kind: isGroup ? "group" : "direct",
42
+ id: trimmed,
43
+ },
44
+ chatType: isGroup ? "group" : "direct",
45
+ from: isGroup ? `feishu:group:${trimmed}` : `feishu:${trimmed}`,
46
+ to: trimmed,
47
+ });
48
+ }
@@ -0,0 +1,51 @@
1
+ import {
2
+ DEFAULT_ACCOUNT_ID,
3
+ type ChannelSetupAdapter,
4
+ type OpenClawConfig,
5
+ } from "openclaw/plugin-sdk/setup";
6
+ import { resolveDefaultFeishuAccountId } from "./accounts.js";
7
+ import type { FeishuConfig } from "./types.js";
8
+
9
+ export function setFeishuNamedAccountEnabled(
10
+ cfg: OpenClawConfig,
11
+ accountId: string,
12
+ enabled: boolean,
13
+ ): OpenClawConfig {
14
+ const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
15
+ return {
16
+ ...cfg,
17
+ channels: {
18
+ ...cfg.channels,
19
+ feishu: {
20
+ ...feishuCfg,
21
+ accounts: {
22
+ ...feishuCfg?.accounts,
23
+ [accountId]: {
24
+ ...feishuCfg?.accounts?.[accountId],
25
+ enabled,
26
+ },
27
+ },
28
+ },
29
+ },
30
+ };
31
+ }
32
+
33
+ export const feishuSetupAdapter: ChannelSetupAdapter = {
34
+ resolveAccountId: ({ cfg, accountId }) => accountId?.trim() || resolveDefaultFeishuAccountId(cfg),
35
+ applyAccountConfig: ({ cfg, accountId }) => {
36
+ const isDefault = !accountId || accountId === DEFAULT_ACCOUNT_ID;
37
+ if (isDefault) {
38
+ return {
39
+ ...cfg,
40
+ channels: {
41
+ ...cfg.channels,
42
+ feishu: {
43
+ ...cfg.channels?.feishu,
44
+ enabled: true,
45
+ },
46
+ },
47
+ };
48
+ }
49
+ return setFeishuNamedAccountEnabled(cfg, accountId, true);
50
+ },
51
+ };
@@ -1,17 +1,27 @@
1
+ import {
2
+ createNonExitingRuntimeEnv,
3
+ createPluginSetupWizardConfigure,
4
+ createPluginSetupWizardStatus,
5
+ createTestWizardPrompter,
6
+ runSetupWizardConfigure,
7
+ } from "openclaw/plugin-sdk/plugin-test-runtime";
1
8
  import { describe, expect, it, vi } from "vitest";
2
9
 
3
10
  vi.mock("./probe.js", () => ({
4
11
  probeFeishu: vi.fn(async () => ({ ok: false, error: "mocked" })),
5
12
  }));
6
13
 
7
- import { feishuOnboardingAdapter } from "./onboarding.js";
14
+ vi.mock("./app-registration.js", () => ({
15
+ initAppRegistration: vi.fn(async () => {
16
+ throw new Error("mocked: scan-to-create not available");
17
+ }),
18
+ beginAppRegistration: vi.fn(),
19
+ pollAppRegistration: vi.fn(),
20
+ printQrCode: vi.fn(async () => {}),
21
+ getAppOwnerOpenId: vi.fn(async () => undefined),
22
+ }));
8
23
 
9
- const baseConfigureContext = {
10
- runtime: {} as never,
11
- accountOverrides: {},
12
- shouldPromptAccountIds: false,
13
- forceAllowFrom: false,
14
- };
24
+ import { feishuPlugin } from "./channel.js";
15
25
 
16
26
  const baseStatusContext = {
17
27
  accountOverrides: {},
@@ -42,7 +52,7 @@ async function withEnvVars(values: Record<string, string | undefined>, run: () =
42
52
  }
43
53
 
44
54
  async function getStatusWithEnvRefs(params: { appIdKey: string; appSecretKey: string }) {
45
- return await feishuOnboardingAdapter.getStatus({
55
+ return await feishuGetStatus({
46
56
  cfg: {
47
57
  channels: {
48
58
  feishu: {
@@ -55,25 +65,26 @@ async function getStatusWithEnvRefs(params: { appIdKey: string; appSecretKey: st
55
65
  });
56
66
  }
57
67
 
58
- describe("feishuOnboardingAdapter.configure", () => {
68
+ const feishuConfigure = createPluginSetupWizardConfigure(feishuPlugin);
69
+ const feishuGetStatus = createPluginSetupWizardStatus(feishuPlugin);
70
+
71
+ describe("feishu setup wizard", () => {
59
72
  it("does not throw when config appId/appSecret are SecretRef objects", async () => {
60
73
  const text = vi
61
74
  .fn()
62
75
  .mockResolvedValueOnce("cli_from_prompt")
63
- .mockResolvedValueOnce("secret_from_prompt")
64
- .mockResolvedValueOnce("oc_group_1");
65
-
66
- const prompter = {
67
- note: vi.fn(async () => undefined),
76
+ .mockResolvedValueOnce("secret_from_prompt");
77
+ const prompter = createTestWizardPrompter({
68
78
  text,
69
79
  confirm: vi.fn(async () => true),
70
80
  select: vi.fn(
71
- async ({ initialValue }: { initialValue?: string }) => initialValue ?? "allowlist",
72
- ),
73
- } as never;
81
+ async ({ initialValue }: { initialValue?: string }) => initialValue ?? "bot",
82
+ ) as never,
83
+ });
74
84
 
75
85
  await expect(
76
- feishuOnboardingAdapter.configure({
86
+ runSetupWizardConfigure({
87
+ configure: feishuConfigure,
77
88
  cfg: {
78
89
  channels: {
79
90
  feishu: {
@@ -83,15 +94,35 @@ describe("feishuOnboardingAdapter.configure", () => {
83
94
  },
84
95
  } as never,
85
96
  prompter,
86
- ...baseConfigureContext,
97
+ runtime: createNonExitingRuntimeEnv(),
87
98
  }),
88
99
  ).resolves.toBeTruthy();
89
100
  });
90
101
  });
91
102
 
92
- describe("feishuOnboardingAdapter.getStatus", () => {
103
+ describe("feishu setup wizard status", () => {
104
+ it("treats SecretRef appSecret as configured when appId is present", async () => {
105
+ const status = await feishuGetStatus({
106
+ cfg: {
107
+ channels: {
108
+ feishu: {
109
+ appId: "cli_a123456",
110
+ appSecret: {
111
+ source: "env",
112
+ provider: "default",
113
+ id: "FEISHU_APP_SECRET",
114
+ },
115
+ },
116
+ },
117
+ } as never,
118
+ accountOverrides: {},
119
+ });
120
+
121
+ expect(status.configured).toBe(true);
122
+ });
123
+
93
124
  it("does not fallback to top-level appId when account explicitly sets empty appId", async () => {
94
- const status = await feishuOnboardingAdapter.getStatus({
125
+ const status = await feishuGetStatus({
95
126
  cfg: {
96
127
  channels: {
97
128
  feishu: {