import { describe, expect, it, vi } from 'vitest' import { WeComChannelDriver } from './wecom-channel-driver' import type { WeComSdkTransport } from './wecom-driver' type MessageListener = (frame: unknown) => void type ErrorListener = (error: Error) => void type AuthenticatedListener = () => void class FakeTransport implements WeComSdkTransport { readonly connect = vi.fn(() => { this.authenticatedListener?.() }) readonly disconnect = vi.fn() readonly replyStream = vi.fn( async () => ({}) ) private messageListener?: MessageListener private authenticatedListener?: AuthenticatedListener on(event: 'message', listener: MessageListener): unknown on(event: 'error', listener: ErrorListener): unknown on(event: 'authenticated', listener: AuthenticatedListener): unknown on( event: 'message' | 'error' | 'authenticated', listener: MessageListener | ErrorListener | AuthenticatedListener ): unknown { if (event === 'message') { this.messageListener = listener as MessageListener } else if (event === 'authenticated') { this.authenticatedListener = listener as AuthenticatedListener } return this } off(event: 'message', listener: MessageListener): unknown off(event: 'error', listener: ErrorListener): unknown off(event: 'authenticated', listener: AuthenticatedListener): unknown off(event: 'message' | 'error' | 'authenticated'): unknown { if (event === 'message') { this.messageListener = undefined } else if (event === 'authenticated') { this.authenticatedListener = undefined } return this } emit(frame: unknown): void { this.messageListener?.(frame) } } function groupFrame( eventId: string, requestId: string ): Record { return { cmd: 'aibot_msg_callback', headers: { req_id: requestId }, body: { msgid: eventId, aibotid: 'bot-1', chatid: 'group-1', chattype: 'group', from: { userid: 'user-1' }, create_time: 1_700_000_000, msgtype: 'text', text: { content: '@GoodBuddy 请规划下一步' } } } } describe('WeComChannelDriver', () => { it('adapts mentioned group messages and bounds reply contexts', async () => { const transport = new FakeTransport() const driver = new WeComChannelDriver({ botId: 'bot-1', secret: 'secret', transportFactory: () => transport, maximumReplyContexts: 1 }) const messages: unknown[] = [] await driver.start((message) => { messages.push(message) }) transport.emit(groupFrame('event-1', 'request-1')) transport.emit(groupFrame('event-2', 'request-2')) expect(messages[0]).toEqual({ channel: 'wecom', accountId: 'bot-1', eventId: 'event-1', senderId: 'user-1', conversationId: 'group-1', conversationType: 'group', text: '@GoodBuddy 请规划下一步', mentioned: true, workMode: 'ask', receivedAt: 1_700_000_000 }) await expect( driver.send( { channel: 'wecom', eventId: 'event-1', conversationId: 'group-1', recipientId: 'user-1', status: 'completed', output: '旧回复' }, new AbortController().signal ) ).rejects.toThrow('上下文无效') await driver.send( { channel: 'wecom', eventId: 'event-2', conversationId: 'group-1', recipientId: 'user-1', status: 'completed', output: '新回复' }, new AbortController().signal ) expect(transport.replyStream).toHaveBeenCalledWith( { headers: { req_id: 'request-2' } }, expect.stringMatching(/^goodbuddy_/u), '新回复', true ) await driver.stop() expect(transport.disconnect).toHaveBeenCalledOnce() }) it('rejects unsupported attachments without consuming the reply context', async () => { const transport = new FakeTransport() const driver = new WeComChannelDriver({ botId: 'bot-1', secret: 'secret', transportFactory: () => transport }) await driver.start(() => undefined) transport.emit(groupFrame('media-event', 'media-request')) const message = { channel: 'wecom' as const, eventId: 'media-event', conversationId: 'group-1', recipientId: 'user-1', status: 'completed', output: '文件已生成', attachments: [ { name: 'result.txt', mimeType: 'text/plain', size: 2, kind: 'file' as const, dataBase64: 'b2s=' } ] } await expect( driver.send(message, new AbortController().signal) ).rejects.toThrow('回复失败') await driver.send( { ...message, attachments: undefined }, new AbortController().signal ) expect(transport.replyStream).toHaveBeenCalledOnce() await driver.stop() }) })