cc-haha/adapters/telegram/__tests__/telegram.test.ts
程序员阿江(Relakkes) f1c5f86b80 feat: keep IM permission approvals consistent
DingTalk can receive interactive card callbacks, but the card UI still depends on a published template, so the adapter now supports a template-backed card path with text commands as the reliable fallback. Telegram and WeChat use the same allow-once, allow-always, and deny semantics so manual authorization behaves the same across platforms.

Constraint: DingTalk button rendering requires an operator-provided interactive card template id
Rejected: Treat the existing AI streaming card template as a permission card | it cannot guarantee visible action buttons
Confidence: high
Scope-risk: moderate
Directive: Do not remove the text approval fallback unless DingTalk card template provisioning is guaranteed
Tested: bun test adapters/common/__tests__/permission.test.ts adapters/dingtalk/__tests__/permission-card.test.ts adapters/telegram/__tests__/telegram.test.ts adapters/common/__tests__/config.test.ts src/server/__tests__/adapters.test.ts
Tested: bunx tsc -p adapters/tsconfig.json --noEmit
Tested: bun run check:adapters
Tested: cd desktop && bun run build
Tested: bun run check:server
Tested: bun run check:docs
Tested: git diff --check
Not-tested: bun run quality:pr is blocked by existing CLI core approval policy; see artifacts/quality-runs/2026-05-03T13-19-56-232Z/report.md
2026-05-04 19:03:36 +08:00

119 lines
3.9 KiB
TypeScript

import { describe, it, expect } from 'bun:test'
import { splitMessage, formatPermissionRequest, truncateInput, escapeMarkdownV2 } from '../../common/format.js'
import { parsePermitCallbackData } from '../../common/permission.js'
/**
* Telegram Adapter 翻译逻辑测试
*
* 由于 grammy Bot 需要实际 Token 才能初始化,
* 这里测试的是不依赖 Bot 实例的核心翻译逻辑。
*/
describe('Telegram message formatting', () => {
describe('long message splitting', () => {
it('splits messages at Telegram 4096 char limit', () => {
const longText = 'a'.repeat(8000)
const chunks = splitMessage(longText, 4000)
expect(chunks.length).toBe(2)
expect(chunks[0]!.length).toBeLessThanOrEqual(4000)
expect(chunks[1]!.length).toBeLessThanOrEqual(4000)
})
it('keeps short messages as single chunk', () => {
const chunks = splitMessage('Hello World', 4000)
expect(chunks).toEqual(['Hello World'])
})
it('splits at paragraph boundary when possible', () => {
const text = 'A'.repeat(2000) + '\n\n' + 'B'.repeat(2000)
const chunks = splitMessage(text, 3000)
expect(chunks.length).toBe(2)
})
})
describe('permission request formatting', () => {
it('formats Bash command request', () => {
const result = formatPermissionRequest('Bash', { command: 'npm test' }, 'abcde')
expect(result).toContain('🔐')
expect(result).toContain('Bash')
expect(result).toContain('npm test')
expect(result).toContain('abcde')
})
it('formats Write file request', () => {
const result = formatPermissionRequest(
'Write',
{ file_path: '/src/index.ts', content: 'console.log("hello")' },
'fghij',
)
expect(result).toContain('Write')
expect(result).toContain('index.ts')
expect(result).toContain('fghij')
})
it('truncates long input in permission request', () => {
const longInput = { command: 'x'.repeat(500) }
const result = formatPermissionRequest('Bash', longInput, 'xxxxx')
expect(result.length).toBeLessThan(600)
})
})
describe('callback_data parsing', () => {
it('parses permit:requestId:yes format', () => {
expect(parsePermitCallbackData('permit:abcde:yes')).toEqual({ requestId: 'abcde', allowed: true })
})
it('parses permit:requestId:always format', () => {
expect(parsePermitCallbackData('permit:abcde:always')).toEqual({
requestId: 'abcde',
allowed: true,
rule: 'always',
})
})
it('parses permit:requestId:no format', () => {
expect(parsePermitCallbackData('permit:abcde:no')).toEqual({ requestId: 'abcde', allowed: false })
})
it('ignores non-permit callbacks', () => {
const data = 'other:action'
expect(data.startsWith('permit:')).toBe(false)
})
})
describe('MarkdownV2 escaping', () => {
it('escapes underscores', () => {
expect(escapeMarkdownV2('hello_world')).toBe('hello\\_world')
})
it('escapes multiple special chars', () => {
const result = escapeMarkdownV2('file.ts (line 42)')
expect(result).toBe('file\\.ts \\(line 42\\)')
})
it('handles code blocks safely', () => {
const result = escapeMarkdownV2('`code`')
expect(result).toBe('\\`code\\`')
})
})
describe('whitelist logic', () => {
it('empty allowedUsers means allow all', () => {
const allowedUsers: number[] = []
const isAllowed = (userId: number) =>
allowedUsers.length === 0 || allowedUsers.includes(userId)
expect(isAllowed(12345)).toBe(true)
expect(isAllowed(99999)).toBe(true)
})
it('non-empty allowedUsers filters correctly', () => {
const allowedUsers = [111, 222]
const isAllowed = (userId: number) =>
allowedUsers.length === 0 || allowedUsers.includes(userId)
expect(isAllowed(111)).toBe(true)
expect(isAllowed(222)).toBe(true)
expect(isAllowed(333)).toBe(false)
})
})
})