Files
claude-code-haha/adapters/feishu/__tests__/feishu.test.ts
T
程序员阿江(Relakkes) 82e6e27687 feat: add IM adapter integration (Telegram + Feishu) with web settings UI
Implement IM adapters allowing users to chat with Claude Code from Telegram
and Feishu/Lark. Includes persistent session management (chatId→sessionId
mapping), project selection via /projects command, and a web UI settings page
for configuring bot tokens, allowed users, and default project directory.

Key changes:
- adapters/: Telegram and Feishu adapter scripts with shared common modules
  (WsBridge, MessageBuffer, SessionStore, HttpClient, config, formatting)
- Backend: adapterService + REST API (GET/PUT /api/adapters) with secret masking
- Frontend: AdapterSettings page in Settings tab with i18n support
- DirectoryPicker: use React Portal for dropdown to fix overflow clipping

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 19:38:51 +08:00

235 lines
6.9 KiB
TypeScript

/**
* 飞书 Adapter 翻译逻辑测试
*
* 不启动真实 Bot,只测试事件解析和消息翻译逻辑。
*/
import { describe, it, expect } from 'bun:test'
// ---------- helpers extracted from feishu/index.ts for testability ----------
function extractText(content: string, msgType: string): string | null {
try {
const parsed = JSON.parse(content)
if (msgType === 'text') {
return parsed.text ?? null
}
if (msgType === 'post') {
const zhContent = parsed.zh_cn?.content ?? parsed.en_us?.content ?? []
return zhContent
.flat()
.filter((n: any) => n.tag === 'text' || n.tag === 'md')
.map((n: any) => n.text ?? n.content ?? '')
.join('')
.trim() || null
}
return null
} catch {
return null
}
}
function isBotMentioned(
mentions: Array<{ id?: { open_id?: string } }> | undefined,
botOpenId: string,
): boolean {
if (!mentions || !botOpenId) return false
return mentions.some((m) => m.id?.open_id === botOpenId)
}
function stripMentions(text: string): string {
return text.replace(/@_user_\d+/g, '').trim()
}
function buildPermissionCard(toolName: string, input: unknown, requestId: string): Record<string, unknown> {
const preview = typeof input === 'string' ? input : JSON.stringify(input, null, 2)
const truncated = preview.length > 300 ? preview.slice(0, 300) + '…' : preview
return {
schema: '2.0',
config: { wide_screen_mode: true },
header: {
title: { tag: 'plain_text', content: '🔐 需要权限确认' },
template: 'orange',
},
elements: [
{
tag: 'markdown',
content: `**工具**: ${toolName}\n**内容**:\n\`\`\`\n${truncated}\n\`\`\``,
},
{
tag: 'action',
actions: [
{
tag: 'button',
text: { tag: 'plain_text', content: '✅ 允许' },
type: 'primary',
value: { action: 'permit', requestId, allowed: true },
},
{
tag: 'button',
text: { tag: 'plain_text', content: '❌ 拒绝' },
type: 'danger',
value: { action: 'permit', requestId, allowed: false },
},
],
},
],
}
}
// ---------- tests ----------
describe('Feishu: event parsing', () => {
describe('extractText', () => {
it('extracts text from text message', () => {
const content = JSON.stringify({ text: 'hello world' })
expect(extractText(content, 'text')).toBe('hello world')
})
it('extracts text from post message (zh_cn)', () => {
const content = JSON.stringify({
zh_cn: {
content: [[
{ tag: 'text', text: 'Hello ' },
{ tag: 'text', text: 'World' },
]],
},
})
expect(extractText(content, 'post')).toBe('Hello World')
})
it('extracts text from post message with md tag', () => {
const content = JSON.stringify({
zh_cn: {
content: [[{ tag: 'md', text: '**bold** text' }]],
},
})
expect(extractText(content, 'post')).toBe('**bold** text')
})
it('returns null for unsupported message types', () => {
expect(extractText('{}', 'image')).toBeNull()
expect(extractText('{}', 'audio')).toBeNull()
})
it('returns null for malformed content', () => {
expect(extractText('not-json', 'text')).toBeNull()
})
it('returns null for empty text', () => {
const content = JSON.stringify({ text: '' })
// empty string is falsy, so ?? null returns ''
expect(extractText(content, 'text')).toBe('')
})
})
describe('isBotMentioned', () => {
const botId = 'ou_bot_123'
it('returns true when bot is mentioned', () => {
const mentions = [
{ id: { open_id: 'ou_user_1' } },
{ id: { open_id: 'ou_bot_123' } },
]
expect(isBotMentioned(mentions, botId)).toBe(true)
})
it('returns false when bot is not mentioned', () => {
const mentions = [
{ id: { open_id: 'ou_user_1' } },
{ id: { open_id: 'ou_user_2' } },
]
expect(isBotMentioned(mentions, botId)).toBe(false)
})
it('returns false for undefined mentions', () => {
expect(isBotMentioned(undefined, botId)).toBe(false)
})
it('returns false for empty mentions', () => {
expect(isBotMentioned([], botId)).toBe(false)
})
})
describe('stripMentions', () => {
it('removes @_user_N patterns', () => {
expect(stripMentions('@_user_1 hello world')).toBe('hello world')
})
it('removes multiple mentions', () => {
expect(stripMentions('@_user_1 @_user_2 test')).toBe('test')
})
it('leaves text without mentions unchanged', () => {
expect(stripMentions('hello world')).toBe('hello world')
})
it('trims whitespace', () => {
expect(stripMentions(' @_user_1 hello ')).toBe('hello')
})
})
})
describe('Feishu: permission card', () => {
it('builds valid card structure', () => {
const card = buildPermissionCard('Bash', { command: 'npm test' }, 'abcde')
expect(card.schema).toBe('2.0')
expect((card.header as any).title.content).toContain('权限确认')
expect((card.elements as any[]).length).toBe(2) // markdown + action
const actionElement = (card.elements as any[])[1]
expect(actionElement.tag).toBe('action')
expect(actionElement.actions.length).toBe(2) // allow + deny buttons
})
it('allow button has correct value', () => {
const card = buildPermissionCard('Read', {}, 'xyz12')
const allowBtn = (card.elements as any[])[1].actions[0]
expect(allowBtn.value.action).toBe('permit')
expect(allowBtn.value.requestId).toBe('xyz12')
expect(allowBtn.value.allowed).toBe(true)
})
it('deny button has correct value', () => {
const card = buildPermissionCard('Read', {}, 'xyz12')
const denyBtn = (card.elements as any[])[1].actions[1]
expect(denyBtn.value.action).toBe('permit')
expect(denyBtn.value.requestId).toBe('xyz12')
expect(denyBtn.value.allowed).toBe(false)
})
it('truncates long input preview', () => {
const longInput = { command: 'x'.repeat(500) }
const card = buildPermissionCard('Bash', longInput, 'abc')
const mdElement = (card.elements as any[])[0]
expect(mdElement.content).toContain('…')
})
})
describe('Feishu: card.action.trigger parsing', () => {
it('parses permit action from event', () => {
const event = {
operator: { open_id: 'ou_user_1' },
action: { value: { action: 'permit', requestId: 'abcde', allowed: true } },
context: { open_chat_id: 'oc_chat_123' },
}
expect(event.action.value.action).toBe('permit')
expect(event.action.value.requestId).toBe('abcde')
expect(event.action.value.allowed).toBe(true)
expect(event.context.open_chat_id).toBe('oc_chat_123')
})
it('ignores non-permit actions', () => {
const event = {
action: { value: { action: 'other_action' } },
}
expect(event.action.value.action).not.toBe('permit')
})
})