diff --git a/packages/channels/dingtalk/package.json b/packages/channels/dingtalk/package.json index e6f6f2c6037..64de9d5c547 100644 --- a/packages/channels/dingtalk/package.json +++ b/packages/channels/dingtalk/package.json @@ -15,7 +15,9 @@ "dist" ], "scripts": { - "build": "tsc --build" + "build": "tsc --build", + "test": "vitest run", + "test:ci": "vitest run" }, "dependencies": { "@qwen-code/channel-base": "file:../base", diff --git a/packages/channels/dingtalk/src/DingtalkAdapter.test.ts b/packages/channels/dingtalk/src/DingtalkAdapter.test.ts new file mode 100644 index 00000000000..98a46f42887 --- /dev/null +++ b/packages/channels/dingtalk/src/DingtalkAdapter.test.ts @@ -0,0 +1,111 @@ +import { describe, expect, it, vi } from 'vitest'; + +vi.mock('dingtalk-stream-sdk-nodejs', () => ({ + DWClient: class { + disconnect = vi.fn(); + getConfig = vi.fn(() => ({ access_token: 'token' })); + registerCallbackListener = vi.fn(); + send = vi.fn(); + connect = vi.fn(); + }, + TOPIC_ROBOT: 'robot', + EventAck: { SUCCESS: 'success' }, +})); + +vi.mock('@qwen-code/channel-base', () => ({ + ChannelBase: class { + protected config: Record; + protected name: string; + + constructor( + name: string, + config: Record, + _bridge: unknown, + ) { + this.name = name; + this.config = config; + } + }, +})); + +const { DingtalkChannel } = await import('./DingtalkAdapter.js'); + +function createChannel(): DingtalkChannel { + return new DingtalkChannel( + 'test-dingtalk', + { + type: 'dingtalk', + clientId: 'client-id', + clientSecret: 'client-secret', + senderPolicy: 'open', + allowedUsers: [], + sessionScope: 'user', + cwd: '/tmp', + groupPolicy: 'open', + groups: {}, + }, + {} as never, + ); +} + +function getPromptHook( + channel: DingtalkChannel, + hook: 'onPromptStart' | 'onPromptEnd', +): (chatId: string, sessionId: string, messageId?: string) => void { + const fn = (channel as unknown as Record)[hook] as ( + chatId: string, + sessionId: string, + messageId?: string, + ) => void; + return fn.bind(channel); +} + +describe('DingtalkChannel prompt reactions', () => { + it('skips uppercase webhook URLs when starting a prompt', () => { + const channel = createChannel(); + const attachReaction = vi.fn().mockResolvedValue(undefined); + ( + channel as unknown as { attachReaction: typeof attachReaction } + ).attachReaction = attachReaction; + + getPromptHook(channel, 'onPromptStart')( + 'HTTPS://oapi.dingtalk.com/robot/send?access_token=token', + 'session-1', + 'message-1', + ); + + expect(attachReaction).not.toHaveBeenCalled(); + }); + + it('still attaches reactions for conversation IDs', () => { + const channel = createChannel(); + const attachReaction = vi.fn().mockResolvedValue(undefined); + ( + channel as unknown as { attachReaction: typeof attachReaction } + ).attachReaction = attachReaction; + + getPromptHook(channel, 'onPromptStart')( + 'cid-123', + 'session-1', + 'message-1', + ); + + expect(attachReaction).toHaveBeenCalledWith('message-1', 'cid-123'); + }); + + it('skips uppercase webhook URLs when ending a prompt', () => { + const channel = createChannel(); + const recallReaction = vi.fn().mockResolvedValue(undefined); + ( + channel as unknown as { recallReaction: typeof recallReaction } + ).recallReaction = recallReaction; + + getPromptHook(channel, 'onPromptEnd')( + 'HTTPS://oapi.dingtalk.com/robot/send?access_token=token', + 'session-1', + 'message-1', + ); + + expect(recallReaction).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/channels/dingtalk/src/DingtalkAdapter.ts b/packages/channels/dingtalk/src/DingtalkAdapter.ts index 1729504c84a..7309e320e3c 100644 --- a/packages/channels/dingtalk/src/DingtalkAdapter.ts +++ b/packages/channels/dingtalk/src/DingtalkAdapter.ts @@ -244,7 +244,7 @@ export class DingtalkChannel extends ChannelBase { * conversation ID — skip the webhook-URL fallback case. */ private isConversationId(chatId: string): boolean { - return !!chatId && !chatId.startsWith('http'); + return !!chatId && !/^https?:\/\//i.test(chatId); } protected override onPromptStart(