import assert from 'node:assert/strict'; import test from 'node:test'; import { FEISHU_ENDPOINTS, normalizeBotsSnapshot, normalizeConnectionSnapshot, normalizePollResult, normalizeProvisioning, presentError, reconnectBot, retryConnection, screenFromSnapshot, unwrapRpcResult, } from '../plugin-src/client/channels/feishu/api.js'; test('multi-bot endpoints are bot-scoped and keep legacy operations separate', () => { assert.equal(FEISHU_ENDPOINTS.reconnectBot, 'bot.reconnect'); assert.equal(FEISHU_ENDPOINTS.disconnectBot, 'bot.disconnect'); assert.equal(FEISHU_ENDPOINTS.deleteBot, 'bot.delete'); assert.equal(FEISHU_ENDPOINTS.testConnection, 'connection.test'); }); test('client normalizes multiple independent bots and derives authoritative totals', () => { const snapshot = normalizeBotsSnapshot({ schemaVersion: 2, revision: 9, totals: { configured: 99, connected: 99 }, bots: [ { botId: 'bot-a', state: 'connected', connected: true, configured: true, bot: { name: '销售助手', appIdMasked: 'cli_aaaa••••1111', domain: 'feishu', clientSecret: 'must-not-leak', }, health: { status: 'healthy', summary: '长连接运行正常' }, }, { botId: 'bot-b', // A stale state label must not make an offline bot appear connected. state: 'connected', connected: false, configured: true, bot: { name: '研发助手', domain: 'lark' }, health: { status: 'offline', summary: '等待重连' }, error: { code: 'connection_failed', message: '连接失败' }, }, ], }); assert.equal(snapshot.schemaVersion, 2); assert.equal(snapshot.revision, 9); assert.deepEqual(snapshot.totals, { configured: 2, connected: 1 }); assert.equal(snapshot.bots[0].state, 'connected'); assert.equal(snapshot.bots[1].state, 'connecting'); assert.equal(snapshot.bots[1].bot.domain, 'lark'); assert.equal(snapshot.bots[1].error.message, '连接失败'); assert.equal('clientSecret' in snapshot.bots[0].bot, false); }); test('multi-bot snapshot rejects entries without an opaque botId', () => { assert.throws( () => normalizeBotsSnapshot({ schemaVersion: 2, bots: [{ connected: true }] }), /botId/, ); }); test('provision polling preserves the newly connected botId', () => { assert.deepEqual(normalizePollResult({ status: 'connected', botId: 'bot-new', }), { status: 'connected', botId: 'bot-new', message: undefined, connection: undefined, provisioning: undefined, }); }); test('reconnect helper scopes the mutation to one bot before refreshing the list', async () => { const calls = []; const status = { schemaVersion: 2, bots: [] }; const value = await reconnectBot(async (endpoint, payload) => { calls.push({ endpoint, payload }); return endpoint === 'connection.status' ? status : { accepted: true }; }, 'bot-a'); assert.deepEqual(calls, [ { endpoint: 'bot.reconnect', payload: { botId: 'bot-a' } }, { endpoint: 'connection.status', payload: {} }, ]); assert.equal(value, status); }); test('client only shows connected when the Host confirms connected=true', () => { assert.equal(normalizeConnectionSnapshot({ state: 'connected', connected: false, }).state, 'connecting'); assert.equal(normalizeConnectionSnapshot({ state: 'connecting', connected: true, }).state, 'connected'); }); test('client accepts a Host-rendered QR code without exposing credentials', () => { const provisioning = normalizeProvisioning({ attemptId: '7', verificationUrl: 'https://accounts.feishu.cn/device', qrCodeDataUrl: 'data:image/png;base64,AAAA', expiresAt: 100_000, pollIntervalMs: 1_800, }, 1_000); assert.equal(provisioning.attemptId, '7'); assert.equal(provisioning.qrCodeDataUrl, 'data:image/png;base64,AAAA'); assert.equal('clientSecret' in provisioning, false); }); test('client unwraps RpcResult and redacts credential-shaped error text', () => { assert.deepEqual(unwrapRpcResult({ ok: true, value: { connected: true } }), { connected: true, }); assert.throws( () => unwrapRpcResult({ ok: false, error: { code: 'internal', message: 'failed' }, }), /failed/, ); assert.doesNotMatch( presentError(new Error('app_secret=do-not-show token=also-hidden')).message, /do-not-show|also-hidden/, ); }); test('configured offline and startup errors never become the create screen', () => { const offline = normalizeConnectionSnapshot({ state: 'disconnected', connected: false, configured: true, bot: { name: '北汇星河助手' }, health: { status: 'offline', summary: '长连接已断开' }, }); assert.equal(offline.configured, true); assert.equal(screenFromSnapshot(offline).phase, 'offline'); const failed = screenFromSnapshot(normalizeConnectionSnapshot({ state: 'error', connected: false, configured: true, error: { message: '启动失败' }, })); assert.equal(failed.phase, 'error'); assert.equal(failed.retry, 'test'); assert.equal(failed.configured, true); const failedBeforeConfiguration = screenFromSnapshot(normalizeConnectionSnapshot({ state: 'error', connected: false, configured: false, error: { message: '二维码已过期' }, })); assert.equal(failedBeforeConfiguration.phase, 'error'); assert.equal(failedBeforeConfiguration.retry, 'begin'); const fresh = screenFromSnapshot(normalizeConnectionSnapshot({ state: 'disconnected', connected: false, configured: false, })); assert.deepEqual(fresh, { phase: 'disconnected', configured: false }); }); test('retry connection calls connection.test before authoritative connection.status', async () => { const calls = []; const status = { state: 'connected', connected: true, configured: true }; const value = await retryConnection(async (endpoint, payload) => { calls.push({ endpoint, payload }); return endpoint === 'connection.status' ? status : { accepted: true }; }); assert.deepEqual(calls, [ { endpoint: 'connection.test', payload: {} }, { endpoint: 'connection.status', payload: {} }, ]); assert.equal(value, status); });