cc-haha/adapters/common/__tests__/format.test.ts
程序员阿江(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

101 lines
3.1 KiB
TypeScript

import { describe, it, expect } from 'bun:test'
import {
splitMessage,
formatToolUse,
formatPermissionRequest,
truncateInput,
escapeMarkdownV2,
} from '../format.js'
describe('splitMessage', () => {
it('returns single chunk for short text', () => {
expect(splitMessage('hello', 100)).toEqual(['hello'])
})
it('splits at paragraph boundary', () => {
const text = 'First paragraph.\n\nSecond paragraph.'
const chunks = splitMessage(text, 20)
expect(chunks.length).toBeGreaterThan(1)
expect(chunks.join(' ').replace(/\s+/g, ' ')).toContain('First paragraph')
expect(chunks.join(' ').replace(/\s+/g, ' ')).toContain('Second paragraph')
})
it('splits at newline if no paragraph break', () => {
const text = 'Line one\nLine two\nLine three\nLine four'
const chunks = splitMessage(text, 20)
expect(chunks.length).toBeGreaterThan(1)
})
it('hard-splits at limit if no natural break', () => {
const text = 'a'.repeat(50)
const chunks = splitMessage(text, 20)
expect(chunks.length).toBe(3) // 20 + 20 + 10
expect(chunks.every((c) => c.length <= 20)).toBe(true)
})
it('preserves all content after splitting', () => {
const text = 'Hello world. This is a test. Foo bar baz.'
const chunks = splitMessage(text, 15)
const joined = chunks.join(' ')
// All words should be present
expect(joined).toContain('Hello')
expect(joined).toContain('test')
expect(joined).toContain('baz')
})
})
describe('formatToolUse', () => {
it('includes tool name and input preview', () => {
const result = formatToolUse('Bash', { command: 'npm test' })
expect(result).toContain('🔧 Bash')
expect(result).toContain('npm test')
})
})
describe('formatPermissionRequest', () => {
it('includes tool name, input preview, and request ID', () => {
const result = formatPermissionRequest('Bash', { command: 'rm -rf /' }, 'abcde')
expect(result).toContain('🔐')
expect(result).toContain('Bash')
expect(result).toContain('abcde')
expect(result).toContain('rm -rf')
})
})
describe('truncateInput', () => {
it('returns short input as-is', () => {
expect(truncateInput('hello', 100)).toBe('hello')
})
it('truncates long input with ellipsis', () => {
const long = 'x'.repeat(300)
const result = truncateInput(long, 100)
expect(result.length).toBe(101) // 100 chars + '…'
expect(result.endsWith('…')).toBe(true)
})
it('handles objects by stringifying', () => {
const result = truncateInput({ key: 'value' }, 100)
expect(result).toContain('key')
expect(result).toContain('value')
})
it('handles unserializable input', () => {
const circular: any = {}
circular.self = circular
expect(truncateInput(circular, 100)).toBe('(unserializable)')
})
})
describe('escapeMarkdownV2', () => {
it('escapes special characters', () => {
expect(escapeMarkdownV2('hello_world')).toBe('hello\\_world')
expect(escapeMarkdownV2('a*b*c')).toBe('a\\*b\\*c')
expect(escapeMarkdownV2('test.md')).toBe('test\\.md')
})
it('leaves plain text unchanged', () => {
expect(escapeMarkdownV2('hello world')).toBe('hello world')
})
})