import assert from 'node:assert/strict'; import { readFile } from 'node:fs/promises'; import test from 'node:test'; import React from 'react'; import { renderToStaticMarkup } from 'react-dom/server'; import { act, create } from 'react-test-renderer'; import { FEISHU_ENDPOINTS } from '../../../plugin-src/client/channels/feishu/api.js'; import { BotCard, FeishuSettingsTab, } from '../../../plugin-src/client/channels/feishu/index.js'; import { en, setImTranslator, } from '../../../plugin-src/client/i18n.js'; const flushMicrotasks = () => new Promise((resolve) => setImmediate(resolve)); function textOf(node) { if (typeof node === 'string' || typeof node === 'number') return String(node); if (!node) return ''; const children = node.children ?? node.props?.children ?? []; return (Array.isArray(children) ? children : [children]).map(textOf).join(''); } test('Feishu connection check requests and displays test-message feedback', async () => { const source = await readFile(new URL( '../../../plugin-src/client/channels/feishu/index.js', import.meta.url, ), 'utf8'); assert.match(source, /FEISHU_ENDPOINTS\.reconnectBot, \{ botId, sendTest: true \}/); assert.match(source, /机器人尚未收到可用于测试的私聊消息/); assert.doesNotMatch(source, /请先私聊机器人发送 \/status/); const cardProps = { connection: { botId: 'bot-feishu-test', state: 'connected', connected: true, bot: { name: '飞书测试机器人', appIdMasked: 'cli_test••••1234' }, health: { summary: '长连接运行正常', lastCheckedAt: Date.now() }, }, testNotice: '测试消息已发送,请到飞书会话中确认。', onReconnect() {}, onRequestRemove() {}, onConfirmRemove() {}, onCancelRemove() {}, }; const markup = renderToStaticMarkup(React.createElement(BotCard, cardProps)); assert.match(markup, /role="status"[^>]*>测试消息已发送/); assert.match(markup, /class="dim-cardFooterLayout"[^]*class="bxf-actions bxf-botActions dim-cardActions"[^]*class="bxf-healthSummary dim-cardFeedback"/); let renderer; await act(async () => { renderer = create(React.createElement(BotCard, cardProps)); }); const footerChildren = renderer.root.findByProps({ className: 'dim-cardFooterLayout' }).children; assert.match(footerChildren[0].props.className, /\bbxf-botActions\b/); assert.equal(footerChildren[1].props.role, 'status'); assert.match(footerChildren[1].props.className, /\bdim-cardFeedback\b/); await act(async () => renderer.unmount()); assert.match(markup, /修复卡片按钮/); assert.match(markup, /aria-label="修复飞书测试机器人的卡片按钮"/); assert.match(markup, /]*aria-label="群聊响应方式"/); assert.match(markup, /仅在 @机器人时响应(推荐)/); assert.match(markup, /响应所有群消息/); assert.match(markup, /选择全部消息后会打开飞书官方授权流程/); }); test('Feishu bot card saves group response mode from a dropdown', async () => { const saved = []; let renderer; await act(async () => { renderer = create(React.createElement(BotCard, { connection: { botId: 'bot-mode-test', state: 'connected', connected: true, groupResponseMode: 'mention', bot: { name: '响应模式机器人', appIdMasked: 'cli_mode••••test' }, health: { summary: '长连接运行正常', lastCheckedAt: Date.now() }, }, onGroupResponseModeSave: async (value) => saved.push(value), onReconnect() {}, onRequestRemove() {}, onConfirmRemove() {}, onCancelRemove() {}, })); }); const select = renderer.root.findByProps({ 'aria-label': '群聊响应方式' }); assert.equal(select.type, 'select'); assert.equal(select.props.value, 'mention'); assert.deepEqual(select.findAllByType('option').map((option) => option.props.value), [ 'mention', 'all', ]); await act(async () => { select.props.onChange({ target: { value: 'all' } }); await flushMicrotasks(); }); assert.deepEqual(saved, ['all']); await act(async () => renderer.unmount()); }); test('Feishu bot card offers authorization recovery for all-message mode', () => { const baseConnection = { botId: 'bot-permission-recovery', state: 'connected', connected: true, groupResponseMode: 'all', bot: { name: '权限恢复机器人', appIdMasked: 'cli_reco••••very' }, health: { summary: '长连接运行正常', lastCheckedAt: Date.now() }, }; const reauthorizeMarkup = renderToStaticMarkup(React.createElement(BotCard, { connection: { ...baseConnection, groupMessagePermissionGranted: true }, onReconnect() {}, onRequestRemove() {}, onConfirmRemove() {}, onCancelRemove() {}, })); assert.match(reauthorizeMarkup, /aria-label="重新授权群消息权限"/); assert.match(reauthorizeMarkup, />重新授权去授权 { const previousWindow = globalThis.window; let nextTimer = 0; const frames = new Map(); globalThis.window = { setInterval() { return ++nextTimer; }, clearInterval() {}, setTimeout() { return ++nextTimer; }, clearTimeout() {}, requestAnimationFrame(callback) { const id = ++nextTimer; frames.set(id, callback); queueMicrotask(() => { const pending = frames.get(id); if (!pending) return; frames.delete(id); pending(); }); return id; }, cancelAnimationFrame(id) { frames.delete(id); }, }; t.after(() => { if (previousWindow === undefined) delete globalThis.window; else globalThis.window = previousWindow; }); const snapshot = { schemaVersion: 2, revision: 1, state: 'connected', bots: [{ botId: 'bot_before_permission', state: 'connected', connected: true, configured: true, groupResponseMode: 'mention', groupMessagePermissionGranted: false, bot: { name: '前一个机器人', appIdMasked: 'cli_bef••••ore' }, health: { status: 'healthy', summary: '长连接运行正常' }, }, { botId: 'bot_group_permission', state: 'connected', connected: true, configured: true, groupResponseMode: 'mention', groupMessagePermissionGranted: false, bot: { name: '权限机器人', appIdMasked: 'cli_per••••sion' }, health: { status: 'healthy', summary: '长连接运行正常' }, }], }; const calls = []; const rpcCall = async (endpoint, payload) => { calls.push({ endpoint, payload }); if (endpoint === FEISHU_ENDPOINTS.status) return { ok: true, value: snapshot }; if (endpoint === FEISHU_ENDPOINTS.beginGroupMessagePermission) { return { ok: true, value: { attemptId: 'reg_group_permission', operation: 'group_message_permission', botId: 'bot_group_permission', verificationUrl: 'https://open.feishu.cn/page/launcher?tp=sdk&clientID=cli_permission&addons=encoded', qrCodeDataUrl: 'data:image/png;base64,AAAA', expiresAt: Date.now() + 60_000, pollIntervalMs: 800, }, }; } throw new Error(`Unexpected endpoint: ${endpoint}`); }; let renderer; await act(async () => { renderer = create(React.createElement(FeishuSettingsTab, { rpcCall })); await flushMicrotasks(); }); const targetCard = () => renderer.root.findByProps({ 'data-bot-id': 'bot_group_permission' }); const select = targetCard().findByProps({ 'aria-label': '群聊响应方式' }); await act(async () => { select.props.onChange({ target: { value: 'all' } }); await flushMicrotasks(); }); assert.ok(calls.some(({ endpoint, payload }) => ( endpoint === FEISHU_ENDPOINTS.beginGroupMessagePermission && payload.botId === 'bot_group_permission' ))); assert.equal(calls.some(({ endpoint }) => endpoint === FEISHU_ENDPOINTS.setGroupResponseMode), false); const permissionPanel = targetCard().findByProps({ 'data-provision-for': 'bot_group_permission', }); assert.equal(permissionPanel.findByType('a').props.href, 'https://open.feishu.cn/page/launcher?tp=sdk&clientID=cli_permission&addons=encoded'); assert.match(textOf(permissionPanel), /只增量开通“获取群组中所有消息”权限/); assert.equal(renderer.root.findByProps({ 'data-bot-id': 'bot_before_permission' }) .findAllByProps({ 'data-provision-for': 'bot_group_permission' }).length, 0); assert.equal(targetCard().findByProps({ 'aria-label': '群聊响应方式' }).props.value, 'mention'); await act(async () => renderer.unmount()); }); test('reauthorizing all-message mode starts the same bot-scoped permission flow', async (t) => { const previousWindow = globalThis.window; let nextTimer = 0; const frames = new Map(); globalThis.window = { setInterval() { return ++nextTimer; }, clearInterval() {}, setTimeout() { return ++nextTimer; }, clearTimeout() {}, requestAnimationFrame(callback) { const id = ++nextTimer; frames.set(id, callback); queueMicrotask(() => { const pending = frames.get(id); if (!pending) return; frames.delete(id); pending(); }); return id; }, cancelAnimationFrame(id) { frames.delete(id); }, }; t.after(() => { if (previousWindow === undefined) delete globalThis.window; else globalThis.window = previousWindow; }); const snapshot = { schemaVersion: 2, revision: 1, state: 'connected', bots: [{ botId: 'bot_reauthorize', state: 'connected', connected: true, configured: true, groupResponseMode: 'all', groupMessagePermissionGranted: true, bot: { name: '重新授权机器人', appIdMasked: 'cli_reau••••thorize' }, health: { status: 'healthy', summary: '长连接运行正常' }, }], }; const calls = []; const rpcCall = async (endpoint, payload) => { calls.push({ endpoint, payload }); if (endpoint === FEISHU_ENDPOINTS.status) return { ok: true, value: snapshot }; if (endpoint === FEISHU_ENDPOINTS.beginGroupMessagePermission) { return { ok: true, value: { attemptId: 'reg_reauthorize', operation: 'group_message_permission', botId: 'bot_reauthorize', verificationUrl: 'https://open.feishu.cn/page/launcher?tp=sdk&clientID=cli_reauthorize&addons=encoded', qrCodeDataUrl: 'data:image/png;base64,AAAA', expiresAt: Date.now() + 60_000, pollIntervalMs: 800, }, }; } throw new Error(`Unexpected endpoint: ${endpoint}`); }; let renderer; await act(async () => { renderer = create(React.createElement(FeishuSettingsTab, { rpcCall })); await flushMicrotasks(); }); const targetCard = () => renderer.root.findByProps({ 'data-bot-id': 'bot_reauthorize' }); await act(async () => { targetCard().findByProps({ 'aria-label': '重新授权群消息权限' }).props.onClick(); await flushMicrotasks(); }); assert.ok(calls.some(({ endpoint, payload }) => ( endpoint === FEISHU_ENDPOINTS.beginGroupMessagePermission && payload.botId === 'bot_reauthorize' ))); assert.equal(calls.some(({ endpoint }) => endpoint === FEISHU_ENDPOINTS.setGroupResponseMode), false); assert.match(textOf(targetCard().findByProps({ 'data-provision-for': 'bot_reauthorize' })), /正在为「重新授权机器人」开通群消息权限/); await act(async () => renderer.unmount()); }); test('Feishu callback repair keeps a Host-submitted attempt when a stale QR cancel races saving', async (t) => { const previousWindow = globalThis.window; let nextTimer = 0; const timeouts = new Map(); const frames = new Map(); globalThis.window = { setInterval() { return ++nextTimer; }, clearInterval() {}, setTimeout(callback) { const id = ++nextTimer; timeouts.set(id, callback); return id; }, clearTimeout(id) { timeouts.delete(id); }, requestAnimationFrame(callback) { const id = ++nextTimer; frames.set(id, callback); queueMicrotask(() => { const pending = frames.get(id); if (!pending) return; frames.delete(id); pending(); }); return id; }, cancelAnimationFrame(id) { frames.delete(id); }, }; t.after(() => { if (previousWindow === undefined) delete globalThis.window; else globalThis.window = previousWindow; }); const snapshot = { schemaVersion: 2, revision: 1, state: 'connected', bots: [{ botId: 'bot_target', state: 'connected', connected: true, configured: true, workspace: '/workspace/current', bot: { name: '目标机器人', appIdMasked: 'cli_tar••••rget' }, health: { status: 'healthy', summary: '长连接运行正常' }, }], }; const calls = []; const rpcCall = async (endpoint, payload) => { calls.push({ endpoint, payload }); if (endpoint === FEISHU_ENDPOINTS.status) return { ok: true, value: snapshot }; if (endpoint === FEISHU_ENDPOINTS.beginCallbackRepair) { return { ok: true, value: { attemptId: 'reg_repair', operation: 'callback_repair', botId: 'bot_target', verificationUrl: 'https://open.feishu.cn/page/launcher?tp=sdk&clientID=cli_target', qrCodeDataUrl: 'data:image/png;base64,AAAA', expiresAt: Date.now() + 60_000, pollIntervalMs: 800, }, }; } if (endpoint === FEISHU_ENDPOINTS.pollProvisioning) { return { ok: true, value: { status: 'connecting', operation: 'callback_repair', botId: 'bot_target', }, }; } if (endpoint === FEISHU_ENDPOINTS.cancelProvisioning) { return { ok: true, value: { status: 'connecting', operation: 'callback_repair', botId: 'bot_target', message: 'Callback repair was already submitted and is still being verified.', }, }; } throw new Error(`Unexpected endpoint: ${endpoint}`); }; let renderer; await act(async () => { renderer = create(React.createElement(FeishuSettingsTab, { rpcCall })); await flushMicrotasks(); }); const card = renderer.root.findByProps({ 'data-bot-id': 'bot_target' }); await act(async () => { card.findAllByType('button') .find((button) => textOf(button) === '修复卡片按钮').props.onClick(); await flushMicrotasks(); }); assert.ok(calls.some(({ endpoint, payload }) => endpoint === FEISHU_ENDPOINTS.beginCallbackRepair && payload.botId === 'bot_target')); const officialLink = renderer.root.findByType('a'); assert.equal( officialLink.props.href, 'https://open.feishu.cn/page/launcher?tp=sdk&clientID=cli_target', ); assert.match(textOf(renderer.toJSON()), /不会创建新应用/); const staleCancel = renderer.root.findAllByType('button') .find((button) => textOf(button) === '取消修复'); assert.ok(staleCancel); await act(async () => { staleCancel.props.onClick(); await flushMicrotasks(); }); assert.ok(calls.some(({ endpoint, payload }) => endpoint === FEISHU_ENDPOINTS.cancelProvisioning && payload.attemptId === 'reg_repair')); assert.match(textOf(renderer.toJSON()), /此阶段无法取消/); assert.equal(renderer.root.findAllByType('button').some( (button) => textOf(button) === '取消修复', ), false); assert.ok(timeouts.size > 0, 'submitted repair keeps polling after the refused cancel'); await act(async () => { renderer.unmount(); }); }); test('Feishu callback repair recovers when a Host restart forgets the browser attempt', async (t) => { const previousWindow = globalThis.window; let nextTimer = 0; const frames = new Map(); globalThis.window = { setInterval() { return ++nextTimer; }, clearInterval() {}, setTimeout() { return ++nextTimer; }, clearTimeout() {}, requestAnimationFrame(callback) { const id = ++nextTimer; frames.set(id, callback); queueMicrotask(() => { const pending = frames.get(id); if (!pending) return; frames.delete(id); pending(); }); return id; }, cancelAnimationFrame(id) { frames.delete(id); }, }; t.after(() => { if (previousWindow === undefined) delete globalThis.window; else globalThis.window = previousWindow; }); const snapshot = { schemaVersion: 2, revision: 1, state: 'connected', bots: [{ botId: 'bot_target', state: 'connected', connected: true, configured: true, workspace: '/workspace/current', bot: { name: '目标机器人', appIdMasked: 'cli_tar••••rget' }, health: { status: 'healthy', summary: '长连接运行正常' }, }], }; let beginCount = 0; const calls = []; const rpcCall = async (endpoint, payload) => { calls.push({ endpoint, payload }); if (endpoint === FEISHU_ENDPOINTS.status) return { ok: true, value: snapshot }; if (endpoint === FEISHU_ENDPOINTS.beginCallbackRepair) { beginCount += 1; return { ok: true, value: { attemptId: `reg_repair_${beginCount}`, operation: 'callback_repair', botId: 'bot_target', verificationUrl: `https://open.feishu.cn/page/launcher?tp=sdk&clientID=cli_target&attempt=${beginCount}`, qrCodeDataUrl: 'data:image/png;base64,AAAA', expiresAt: Date.now() + 60_000, pollIntervalMs: 800, }, }; } if (endpoint === FEISHU_ENDPOINTS.cancelProvisioning) { return { ok: false, error: { code: 'bad-request', message: 'The provisioning attempt is no longer active.', }, }; } throw new Error(`Unexpected endpoint: ${endpoint}`); }; let renderer; await act(async () => { renderer = create(React.createElement(FeishuSettingsTab, { rpcCall })); await flushMicrotasks(); }); const repairButton = () => renderer.root.findByProps({ 'data-bot-id': 'bot_target' }) .findAllByType('button') .find((button) => textOf(button) === '修复卡片按钮'); await act(async () => { repairButton().props.onClick(); await flushMicrotasks(); }); await act(async () => { renderer.root.findAllByType('button') .find((button) => textOf(button) === '换一个二维码').props.onClick(); await flushMicrotasks(); }); assert.equal(beginCount, 2, 'a stale cancel cannot block the replacement begin'); assert.match(renderer.root.findByType('a').props.href, /attempt=2$/); await act(async () => { renderer.root.findAllByType('button') .find((button) => textOf(button) === '取消修复').props.onClick(); await flushMicrotasks(); }); assert.match(textOf(renderer.toJSON()), /The provisioning attempt is no longer active/); await act(async () => { renderer.root.find((node) => node.props.role === 'alert') .findAllByType('button') .find((button) => textOf(button) === '关闭').props.onClick(); await flushMicrotasks(); }); assert.equal(renderer.root.findAll((node) => node.props.role === 'alert').length, 0); assert.equal(repairButton().props.disabled, false); assert.ok(calls.some(({ endpoint }) => endpoint === FEISHU_ENDPOINTS.cancelProvisioning)); await act(async () => { renderer.unmount(); }); }); test('Feishu reconnect failures render fixed English-safe feedback', async (t) => { const previousWindow = globalThis.window; let nextFrame = 0; const frames = new Map(); globalThis.window = { setInterval() { return 1; }, clearInterval() {}, requestAnimationFrame(callback) { const id = ++nextFrame; frames.set(id, callback); queueMicrotask(() => { const pending = frames.get(id); if (!pending) return; frames.delete(id); pending(); }); return id; }, cancelAnimationFrame(id) { frames.delete(id); }, }; setImTranslator((key) => en[key] ?? key); t.after(() => { setImTranslator(null); if (previousWindow === undefined) delete globalThis.window; else globalThis.window = previousWindow; }); const snapshot = { schemaVersion: 2, revision: 1, state: 'connected', bots: [{ botId: 'bot_feishu_test', state: 'connected', connected: true, configured: true, workspace: '/workspace/current', bot: { name: '今天是牢梁', appIdMasked: 'cli_test••••1234' }, health: { status: 'healthy', summary: 'Long connection is healthy' }, }], }; const rpcCall = async (endpoint) => { if (endpoint === FEISHU_ENDPOINTS.status) return { ok: true, value: snapshot }; if (endpoint === FEISHU_ENDPOINTS.reconnectBot) { return { ok: false, error: { code: 'FEISHU_UPSTREAM_FAILED', message: '飞书上游操作失败' }, }; } throw new Error(`Unexpected endpoint: ${endpoint}`); }; let renderer; await act(async () => { renderer = create(React.createElement(FeishuSettingsTab, { rpcCall })); await flushMicrotasks(); }); const card = renderer.root.findByProps({ 'data-bot-id': 'bot_feishu_test' }); await act(async () => { card.findAllByType('button') .find((button) => textOf(button) === 'Check connection').props.onClick(); await flushMicrotasks(); }); const announcement = renderer.root.find( (node) => node.props.role === 'status' && node.props['aria-live'] === 'polite', ); assert.equal(textOf(announcement), 'Connection check failed. Try again later.'); assert.doesNotMatch(textOf(announcement), /[\p{Script=Han}]/u); assert.match(textOf(card), /Connection check failed\. Try again later\./); assert.doesNotMatch(textOf(card), /飞书上游操作失败/); await act(async () => { renderer.unmount(); }); });