agent-messenger 2.10.2 → 2.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/.env.template +4 -1
- package/README.md +77 -27
- package/bun.lock +26 -0
- package/dist/package.json +14 -1
- package/dist/src/platforms/channeltalk/commands/auth.d.ts +2 -1
- package/dist/src/platforms/channeltalk/commands/auth.d.ts.map +1 -1
- package/dist/src/platforms/channeltalk/commands/auth.js +5 -3
- package/dist/src/platforms/channeltalk/commands/auth.js.map +1 -1
- package/dist/src/platforms/channeltalk/token-extractor.d.ts +2 -1
- package/dist/src/platforms/channeltalk/token-extractor.d.ts.map +1 -1
- package/dist/src/platforms/channeltalk/token-extractor.js +22 -6
- package/dist/src/platforms/channeltalk/token-extractor.js.map +1 -1
- package/dist/src/platforms/channeltalkbot/cli.d.ts.map +1 -1
- package/dist/src/platforms/channeltalkbot/cli.js +11 -1
- package/dist/src/platforms/channeltalkbot/cli.js.map +1 -1
- package/dist/src/platforms/channeltalkbot/commands/auth.d.ts.map +1 -1
- package/dist/src/platforms/channeltalkbot/commands/auth.js +1 -5
- package/dist/src/platforms/channeltalkbot/commands/auth.js.map +1 -1
- package/dist/src/platforms/channeltalkbot/commands/bot.d.ts.map +1 -1
- package/dist/src/platforms/channeltalkbot/commands/bot.js +1 -6
- package/dist/src/platforms/channeltalkbot/commands/bot.js.map +1 -1
- package/dist/src/platforms/channeltalkbot/commands/chat.d.ts.map +1 -1
- package/dist/src/platforms/channeltalkbot/commands/chat.js +1 -6
- package/dist/src/platforms/channeltalkbot/commands/chat.js.map +1 -1
- package/dist/src/platforms/channeltalkbot/commands/group.d.ts.map +1 -1
- package/dist/src/platforms/channeltalkbot/commands/group.js +1 -6
- package/dist/src/platforms/channeltalkbot/commands/group.js.map +1 -1
- package/dist/src/platforms/channeltalkbot/commands/manager.d.ts.map +1 -1
- package/dist/src/platforms/channeltalkbot/commands/manager.js +1 -6
- package/dist/src/platforms/channeltalkbot/commands/manager.js.map +1 -1
- package/dist/src/platforms/channeltalkbot/commands/message.d.ts.map +1 -1
- package/dist/src/platforms/channeltalkbot/commands/message.js +1 -6
- package/dist/src/platforms/channeltalkbot/commands/message.js.map +1 -1
- package/dist/src/platforms/channeltalkbot/commands/whoami.d.ts.map +1 -1
- package/dist/src/platforms/channeltalkbot/commands/whoami.js +1 -6
- package/dist/src/platforms/channeltalkbot/commands/whoami.js.map +1 -1
- package/dist/src/platforms/channeltalkbot/credential-manager.d.ts +5 -0
- package/dist/src/platforms/channeltalkbot/credential-manager.d.ts.map +1 -1
- package/dist/src/platforms/channeltalkbot/credential-manager.js +34 -4
- package/dist/src/platforms/channeltalkbot/credential-manager.js.map +1 -1
- package/dist/src/platforms/discord/commands/auth.d.ts +1 -0
- package/dist/src/platforms/discord/commands/auth.d.ts.map +1 -1
- package/dist/src/platforms/discord/commands/auth.js +3 -1
- package/dist/src/platforms/discord/commands/auth.js.map +1 -1
- package/dist/src/platforms/discord/listener.d.ts +2 -0
- package/dist/src/platforms/discord/listener.d.ts.map +1 -1
- package/dist/src/platforms/discord/listener.js +51 -21
- package/dist/src/platforms/discord/listener.js.map +1 -1
- package/dist/src/platforms/discord/token-extractor.d.ts +2 -1
- package/dist/src/platforms/discord/token-extractor.d.ts.map +1 -1
- package/dist/src/platforms/discord/token-extractor.js +21 -6
- package/dist/src/platforms/discord/token-extractor.js.map +1 -1
- package/dist/src/platforms/discordbot/cli.d.ts.map +1 -1
- package/dist/src/platforms/discordbot/cli.js +12 -1
- package/dist/src/platforms/discordbot/cli.js.map +1 -1
- package/dist/src/platforms/discordbot/client.d.ts +3 -0
- package/dist/src/platforms/discordbot/client.d.ts.map +1 -1
- package/dist/src/platforms/discordbot/client.js +3 -0
- package/dist/src/platforms/discordbot/client.js.map +1 -1
- package/dist/src/platforms/discordbot/commands/auth.d.ts.map +1 -1
- package/dist/src/platforms/discordbot/commands/auth.js +1 -5
- package/dist/src/platforms/discordbot/commands/auth.js.map +1 -1
- package/dist/src/platforms/discordbot/commands/message.d.ts.map +1 -1
- package/dist/src/platforms/discordbot/commands/message.js +1 -6
- package/dist/src/platforms/discordbot/commands/message.js.map +1 -1
- package/dist/src/platforms/discordbot/commands/server.d.ts.map +1 -1
- package/dist/src/platforms/discordbot/commands/server.js +1 -4
- package/dist/src/platforms/discordbot/commands/server.js.map +1 -1
- package/dist/src/platforms/discordbot/commands/whoami.d.ts.map +1 -1
- package/dist/src/platforms/discordbot/commands/whoami.js +1 -6
- package/dist/src/platforms/discordbot/commands/whoami.js.map +1 -1
- package/dist/src/platforms/discordbot/index.d.ts +3 -1
- package/dist/src/platforms/discordbot/index.d.ts.map +1 -1
- package/dist/src/platforms/discordbot/index.js +2 -1
- package/dist/src/platforms/discordbot/index.js.map +1 -1
- package/dist/src/platforms/discordbot/listener.d.ts +43 -0
- package/dist/src/platforms/discordbot/listener.d.ts.map +1 -0
- package/dist/src/platforms/discordbot/listener.js +292 -0
- package/dist/src/platforms/discordbot/listener.js.map +1 -0
- package/dist/src/platforms/discordbot/types.d.ts +161 -0
- package/dist/src/platforms/discordbot/types.d.ts.map +1 -1
- package/dist/src/platforms/discordbot/types.js +34 -0
- package/dist/src/platforms/discordbot/types.js.map +1 -1
- package/dist/src/platforms/instagram/commands/auth.d.ts.map +1 -1
- package/dist/src/platforms/instagram/commands/auth.js +3 -1
- package/dist/src/platforms/instagram/commands/auth.js.map +1 -1
- package/dist/src/platforms/instagram/token-extractor.d.ts +2 -1
- package/dist/src/platforms/instagram/token-extractor.d.ts.map +1 -1
- package/dist/src/platforms/instagram/token-extractor.js +11 -2
- package/dist/src/platforms/instagram/token-extractor.js.map +1 -1
- package/dist/src/platforms/slack/commands/auth.d.ts.map +1 -1
- package/dist/src/platforms/slack/commands/auth.js +4 -2
- package/dist/src/platforms/slack/commands/auth.js.map +1 -1
- package/dist/src/platforms/slack/token-extractor.d.ts +4 -1
- package/dist/src/platforms/slack/token-extractor.d.ts.map +1 -1
- package/dist/src/platforms/slack/token-extractor.js +64 -15
- package/dist/src/platforms/slack/token-extractor.js.map +1 -1
- package/dist/src/platforms/slackbot/cli.d.ts.map +1 -1
- package/dist/src/platforms/slackbot/cli.js +15 -3
- package/dist/src/platforms/slackbot/cli.js.map +1 -1
- package/dist/src/platforms/slackbot/client.d.ts +22 -1
- package/dist/src/platforms/slackbot/client.d.ts.map +1 -1
- package/dist/src/platforms/slackbot/client.js +104 -1
- package/dist/src/platforms/slackbot/client.js.map +1 -1
- package/dist/src/platforms/slackbot/commands/auth.d.ts.map +1 -1
- package/dist/src/platforms/slackbot/commands/auth.js +1 -5
- package/dist/src/platforms/slackbot/commands/auth.js.map +1 -1
- package/dist/src/platforms/slackbot/commands/file.d.ts +3 -0
- package/dist/src/platforms/slackbot/commands/file.d.ts.map +1 -0
- package/dist/src/platforms/slackbot/commands/file.js +164 -0
- package/dist/src/platforms/slackbot/commands/file.js.map +1 -0
- package/dist/src/platforms/slackbot/commands/index.d.ts +1 -0
- package/dist/src/platforms/slackbot/commands/index.d.ts.map +1 -1
- package/dist/src/platforms/slackbot/commands/index.js +1 -0
- package/dist/src/platforms/slackbot/commands/index.js.map +1 -1
- package/dist/src/platforms/slackbot/commands/message.d.ts.map +1 -1
- package/dist/src/platforms/slackbot/commands/message.js +19 -0
- package/dist/src/platforms/slackbot/commands/message.js.map +1 -1
- package/dist/src/platforms/slackbot/commands/whoami.d.ts.map +1 -1
- package/dist/src/platforms/slackbot/commands/whoami.js +1 -6
- package/dist/src/platforms/slackbot/commands/whoami.js.map +1 -1
- package/dist/src/platforms/slackbot/credential-manager.d.ts +1 -0
- package/dist/src/platforms/slackbot/credential-manager.d.ts.map +1 -1
- package/dist/src/platforms/slackbot/credential-manager.js +30 -2
- package/dist/src/platforms/slackbot/credential-manager.js.map +1 -1
- package/dist/src/platforms/slackbot/index.d.ts +4 -1
- package/dist/src/platforms/slackbot/index.d.ts.map +1 -1
- package/dist/src/platforms/slackbot/index.js +1 -0
- package/dist/src/platforms/slackbot/index.js.map +1 -1
- package/dist/src/platforms/slackbot/listener.d.ts +44 -0
- package/dist/src/platforms/slackbot/listener.d.ts.map +1 -0
- package/dist/src/platforms/slackbot/listener.js +313 -0
- package/dist/src/platforms/slackbot/listener.js.map +1 -0
- package/dist/src/platforms/slackbot/types.d.ts +196 -1
- package/dist/src/platforms/slackbot/types.d.ts.map +1 -1
- package/dist/src/platforms/slackbot/types.js +4 -1
- package/dist/src/platforms/slackbot/types.js.map +1 -1
- package/dist/src/platforms/teams/commands/auth.d.ts +1 -0
- package/dist/src/platforms/teams/commands/auth.d.ts.map +1 -1
- package/dist/src/platforms/teams/commands/auth.js +37 -6
- package/dist/src/platforms/teams/commands/auth.js.map +1 -1
- package/dist/src/platforms/teams/ensure-auth.js +31 -9
- package/dist/src/platforms/teams/ensure-auth.js.map +1 -1
- package/dist/src/platforms/teams/token-extractor.d.ts +4 -1
- package/dist/src/platforms/teams/token-extractor.d.ts.map +1 -1
- package/dist/src/platforms/teams/token-extractor.js +71 -29
- package/dist/src/platforms/teams/token-extractor.js.map +1 -1
- package/dist/src/platforms/webex/commands/auth.d.ts +1 -0
- package/dist/src/platforms/webex/commands/auth.d.ts.map +1 -1
- package/dist/src/platforms/webex/commands/auth.js +3 -1
- package/dist/src/platforms/webex/commands/auth.js.map +1 -1
- package/dist/src/platforms/webex/token-extractor.d.ts +3 -1
- package/dist/src/platforms/webex/token-extractor.d.ts.map +1 -1
- package/dist/src/platforms/webex/token-extractor.js +16 -2
- package/dist/src/platforms/webex/token-extractor.js.map +1 -1
- package/dist/src/platforms/wechatbot/cli.d.ts.map +1 -1
- package/dist/src/platforms/wechatbot/cli.js +11 -1
- package/dist/src/platforms/wechatbot/cli.js.map +1 -1
- package/dist/src/platforms/wechatbot/commands/auth.d.ts.map +1 -1
- package/dist/src/platforms/wechatbot/commands/auth.js +1 -5
- package/dist/src/platforms/wechatbot/commands/auth.js.map +1 -1
- package/dist/src/platforms/wechatbot/commands/message.d.ts.map +1 -1
- package/dist/src/platforms/wechatbot/commands/message.js +1 -6
- package/dist/src/platforms/wechatbot/commands/message.js.map +1 -1
- package/dist/src/platforms/wechatbot/commands/template.d.ts.map +1 -1
- package/dist/src/platforms/wechatbot/commands/template.js +1 -6
- package/dist/src/platforms/wechatbot/commands/template.js.map +1 -1
- package/dist/src/platforms/wechatbot/commands/user.d.ts.map +1 -1
- package/dist/src/platforms/wechatbot/commands/user.js +1 -6
- package/dist/src/platforms/wechatbot/commands/user.js.map +1 -1
- package/dist/src/platforms/wechatbot/commands/whoami.d.ts.map +1 -1
- package/dist/src/platforms/wechatbot/commands/whoami.js +1 -6
- package/dist/src/platforms/wechatbot/commands/whoami.js.map +1 -1
- package/dist/src/platforms/whatsappbot/cli.d.ts.map +1 -1
- package/dist/src/platforms/whatsappbot/cli.js +11 -1
- package/dist/src/platforms/whatsappbot/cli.js.map +1 -1
- package/dist/src/platforms/whatsappbot/commands/auth.d.ts.map +1 -1
- package/dist/src/platforms/whatsappbot/commands/auth.js +1 -5
- package/dist/src/platforms/whatsappbot/commands/auth.js.map +1 -1
- package/dist/src/platforms/whatsappbot/commands/message.d.ts.map +1 -1
- package/dist/src/platforms/whatsappbot/commands/message.js +1 -6
- package/dist/src/platforms/whatsappbot/commands/message.js.map +1 -1
- package/dist/src/platforms/whatsappbot/commands/template.d.ts.map +1 -1
- package/dist/src/platforms/whatsappbot/commands/template.js +1 -6
- package/dist/src/platforms/whatsappbot/commands/template.js.map +1 -1
- package/dist/src/platforms/whatsappbot/commands/whoami.d.ts.map +1 -1
- package/dist/src/platforms/whatsappbot/commands/whoami.js +1 -6
- package/dist/src/platforms/whatsappbot/commands/whoami.js.map +1 -1
- package/dist/src/shared/chromium/browsers.d.ts +8 -0
- package/dist/src/shared/chromium/browsers.d.ts.map +1 -1
- package/dist/src/shared/chromium/browsers.js +58 -3
- package/dist/src/shared/chromium/browsers.js.map +1 -1
- package/dist/src/shared/chromium/cli-options.d.ts +5 -0
- package/dist/src/shared/chromium/cli-options.d.ts.map +1 -0
- package/dist/src/shared/chromium/cli-options.js +8 -0
- package/dist/src/shared/chromium/cli-options.js.map +1 -0
- package/dist/src/shared/chromium/index.d.ts +3 -1
- package/dist/src/shared/chromium/index.d.ts.map +1 -1
- package/dist/src/shared/chromium/index.js +2 -1
- package/dist/src/shared/chromium/index.js.map +1 -1
- package/dist/src/shared/utils/cli-output.d.ts +7 -0
- package/dist/src/shared/utils/cli-output.d.ts.map +1 -0
- package/dist/src/shared/utils/cli-output.js +7 -0
- package/dist/src/shared/utils/cli-output.js.map +1 -0
- package/dist/src/tui/app.d.ts.map +1 -1
- package/dist/src/tui/app.js +73 -20
- package/dist/src/tui/app.js.map +1 -1
- package/docs/content/docs/cli/channeltalk.mdx +4 -0
- package/docs/content/docs/cli/discord.mdx +5 -0
- package/docs/content/docs/cli/instagram.mdx +3 -0
- package/docs/content/docs/cli/slack.mdx +5 -0
- package/docs/content/docs/cli/slackbot.mdx +60 -22
- package/docs/content/docs/cli/teams.mdx +5 -0
- package/docs/content/docs/cli/webex.mdx +3 -0
- package/docs/content/docs/sdk/channeltalkbot.mdx +38 -1
- package/docs/content/docs/sdk/discordbot.mdx +501 -0
- package/docs/content/docs/sdk/meta.json +2 -0
- package/docs/content/docs/sdk/slackbot.mdx +576 -0
- package/e2e/README.md +1 -1
- package/e2e/config.ts +9 -4
- package/examples/discordbot-listen.ts +65 -0
- package/examples/slackbot-listen.ts +65 -0
- package/package.json +14 -1
- package/skills/agent-channeltalk/SKILL.md +5 -1
- package/skills/agent-channeltalk/references/authentication.md +5 -1
- package/skills/agent-channeltalkbot/SKILL.md +17 -3
- package/skills/agent-channeltalkbot/references/authentication.md +7 -5
- package/skills/agent-discord/SKILL.md +5 -1
- package/skills/agent-discord/references/authentication.md +7 -1
- package/skills/agent-discordbot/SKILL.md +13 -2
- package/skills/agent-discordbot/references/common-patterns.md +1 -1
- package/skills/agent-instagram/SKILL.md +7 -1
- package/skills/agent-instagram/references/authentication.md +6 -0
- package/skills/agent-kakaotalk/SKILL.md +1 -1
- package/skills/agent-line/SKILL.md +1 -1
- package/skills/agent-slack/SKILL.md +5 -1
- package/skills/agent-slack/references/authentication.md +7 -1
- package/skills/agent-slackbot/SKILL.md +56 -4
- package/skills/agent-slackbot/references/authentication.md +4 -0
- package/skills/agent-teams/SKILL.md +5 -1
- package/skills/agent-teams/references/authentication.md +7 -1
- package/skills/agent-telegram/SKILL.md +1 -1
- package/skills/agent-webex/SKILL.md +7 -1
- package/skills/agent-webex/references/authentication.md +6 -0
- package/skills/agent-wechatbot/SKILL.md +16 -1
- package/skills/agent-wechatbot/references/authentication.md +219 -0
- package/skills/agent-wechatbot/references/common-patterns.md +358 -0
- package/skills/agent-wechatbot/templates/account-summary.sh +122 -0
- package/skills/agent-wechatbot/templates/post-message.sh +122 -0
- package/skills/agent-wechatbot/templates/send-template.sh +152 -0
- package/skills/agent-whatsapp/SKILL.md +1 -1
- package/skills/agent-whatsappbot/SKILL.md +30 -1
- package/src/platforms/channeltalk/commands/auth.test.ts +15 -3
- package/src/platforms/channeltalk/commands/auth.ts +15 -5
- package/src/platforms/channeltalk/token-extractor.ts +24 -5
- package/src/platforms/channeltalkbot/cli.ts +9 -0
- package/src/platforms/channeltalkbot/commands/auth.ts +1 -5
- package/src/platforms/channeltalkbot/commands/bot.ts +1 -6
- package/src/platforms/channeltalkbot/commands/chat.ts +1 -6
- package/src/platforms/channeltalkbot/commands/group.ts +1 -6
- package/src/platforms/channeltalkbot/commands/manager.ts +1 -6
- package/src/platforms/channeltalkbot/commands/message.ts +1 -6
- package/src/platforms/channeltalkbot/commands/whoami.test.ts +2 -0
- package/src/platforms/channeltalkbot/commands/whoami.ts +1 -6
- package/src/platforms/channeltalkbot/credential-manager.test.ts +96 -2
- package/src/platforms/channeltalkbot/credential-manager.ts +37 -4
- package/src/platforms/discord/commands/auth.ts +13 -2
- package/src/platforms/discord/listener.test.ts +59 -1
- package/src/platforms/discord/listener.ts +43 -19
- package/src/platforms/discord/token-extractor.ts +30 -6
- package/src/platforms/discordbot/cli.ts +10 -0
- package/src/platforms/discordbot/client.ts +4 -0
- package/src/platforms/discordbot/commands/auth.ts +1 -5
- package/src/platforms/discordbot/commands/message.ts +1 -6
- package/src/platforms/discordbot/commands/server.ts +1 -5
- package/src/platforms/discordbot/commands/whoami.ts +1 -6
- package/src/platforms/discordbot/index.test.ts +82 -0
- package/src/platforms/discordbot/index.ts +27 -9
- package/src/platforms/discordbot/listener.test.ts +1002 -0
- package/src/platforms/discordbot/listener.ts +321 -0
- package/src/platforms/discordbot/types.ts +163 -0
- package/src/platforms/instagram/commands/auth.ts +9 -1
- package/src/platforms/instagram/token-extractor.ts +13 -1
- package/src/platforms/slack/commands/auth.ts +11 -2
- package/src/platforms/slack/token-extractor.test.ts +96 -0
- package/src/platforms/slack/token-extractor.ts +76 -13
- package/src/platforms/slackbot/cli.ts +13 -1
- package/src/platforms/slackbot/client.test.ts +274 -0
- package/src/platforms/slackbot/client.ts +130 -2
- package/src/platforms/slackbot/commands/auth.ts +1 -5
- package/src/platforms/slackbot/commands/file.test.ts +201 -0
- package/src/platforms/slackbot/commands/file.ts +212 -0
- package/src/platforms/slackbot/commands/index.ts +1 -0
- package/src/platforms/slackbot/commands/message.ts +22 -0
- package/src/platforms/slackbot/commands/whoami.ts +1 -6
- package/src/platforms/slackbot/credential-manager.test.ts +62 -2
- package/src/platforms/slackbot/credential-manager.ts +32 -2
- package/src/platforms/slackbot/index.test.ts +59 -0
- package/src/platforms/slackbot/index.ts +31 -7
- package/src/platforms/slackbot/listener.test.ts +1012 -0
- package/src/platforms/slackbot/listener.ts +362 -0
- package/src/platforms/slackbot/types.ts +224 -1
- package/src/platforms/teams/commands/auth.test.ts +1 -1
- package/src/platforms/teams/commands/auth.ts +66 -7
- package/src/platforms/teams/ensure-auth.test.ts +56 -5
- package/src/platforms/teams/ensure-auth.ts +39 -11
- package/src/platforms/teams/token-extractor.test.ts +146 -24
- package/src/platforms/teams/token-extractor.ts +87 -29
- package/src/platforms/webex/commands/auth.ts +13 -2
- package/src/platforms/webex/token-extractor.ts +25 -3
- package/src/platforms/wechatbot/cli.ts +9 -0
- package/src/platforms/wechatbot/commands/auth.ts +1 -5
- package/src/platforms/wechatbot/commands/message.ts +1 -6
- package/src/platforms/wechatbot/commands/template.ts +1 -6
- package/src/platforms/wechatbot/commands/user.ts +1 -6
- package/src/platforms/wechatbot/commands/whoami.ts +1 -6
- package/src/platforms/whatsappbot/cli.ts +9 -0
- package/src/platforms/whatsappbot/commands/auth.ts +1 -5
- package/src/platforms/whatsappbot/commands/message.ts +1 -6
- package/src/platforms/whatsappbot/commands/template.ts +1 -6
- package/src/platforms/whatsappbot/commands/whoami.ts +1 -6
- package/src/shared/chromium/browsers.test.ts +80 -0
- package/src/shared/chromium/browsers.ts +72 -3
- package/src/shared/chromium/cli-options.test.ts +22 -0
- package/src/shared/chromium/cli-options.ts +12 -0
- package/src/shared/chromium/index.ts +3 -0
- package/src/shared/utils/cli-output.test.ts +57 -0
- package/src/shared/utils/cli-output.ts +8 -0
- package/src/tui/app.ts +129 -20
|
@@ -0,0 +1,1012 @@
|
|
|
1
|
+
import { afterEach, describe, expect, mock, it } from 'bun:test'
|
|
2
|
+
|
|
3
|
+
import { SlackBotListener } from '@/platforms/slackbot/listener'
|
|
4
|
+
import type {
|
|
5
|
+
SlackSocketModeEventsApiArgs,
|
|
6
|
+
SlackSocketModeInteractiveArgs,
|
|
7
|
+
SlackSocketModeMessageEvent,
|
|
8
|
+
SlackSocketModeReactionEvent,
|
|
9
|
+
SlackSocketModeSlashCommandArgs,
|
|
10
|
+
} from '@/platforms/slackbot/types'
|
|
11
|
+
|
|
12
|
+
type WsHandler = (...args: any[]) => void
|
|
13
|
+
|
|
14
|
+
let mockWsInstance: MockWs
|
|
15
|
+
|
|
16
|
+
class MockWs {
|
|
17
|
+
static OPEN = 1
|
|
18
|
+
static CLOSED = 3
|
|
19
|
+
static lastUrl: string | null = null
|
|
20
|
+
readyState = MockWs.OPEN
|
|
21
|
+
url: string
|
|
22
|
+
|
|
23
|
+
private handlers = new Map<string, WsHandler[]>()
|
|
24
|
+
sent: string[] = []
|
|
25
|
+
pings: number = 0
|
|
26
|
+
|
|
27
|
+
constructor(url: string, _options?: any) {
|
|
28
|
+
this.url = url
|
|
29
|
+
MockWs.lastUrl = url
|
|
30
|
+
// oxlint-disable-next-line typescript-eslint/no-this-alias
|
|
31
|
+
mockWsInstance = this
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
on(event: string, handler: WsHandler) {
|
|
35
|
+
const list = this.handlers.get(event) ?? []
|
|
36
|
+
list.push(handler)
|
|
37
|
+
this.handlers.set(event, list)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
send(data: string) {
|
|
41
|
+
this.sent.push(data)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
ping() {
|
|
45
|
+
this.pings++
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
close() {
|
|
49
|
+
this.readyState = MockWs.CLOSED
|
|
50
|
+
setTimeout(() => this.emit('close'), 0)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
emit(event: string, ...args: any[]) {
|
|
54
|
+
for (const handler of this.handlers.get(event) ?? []) {
|
|
55
|
+
handler(...args)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
simulateOpen() {
|
|
60
|
+
this.emit('open')
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
simulateMessage(data: Record<string, unknown>) {
|
|
64
|
+
this.emit('message', Buffer.from(JSON.stringify(data)))
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
simulateRawMessage(raw: string) {
|
|
68
|
+
this.emit('message', Buffer.from(raw))
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
simulateClose() {
|
|
72
|
+
this.readyState = MockWs.CLOSED
|
|
73
|
+
this.emit('close')
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
simulatePong() {
|
|
77
|
+
this.emit('pong')
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
simulateHello(extra: Record<string, unknown> = {}) {
|
|
81
|
+
this.simulateMessage({
|
|
82
|
+
type: 'hello',
|
|
83
|
+
connection_info: { app_id: 'A_APP' },
|
|
84
|
+
num_connections: 1,
|
|
85
|
+
...extra,
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
simulateEventsApi(envelopeId: string, event: Record<string, unknown>, extra: Record<string, unknown> = {}) {
|
|
90
|
+
this.simulateMessage({
|
|
91
|
+
type: 'events_api',
|
|
92
|
+
envelope_id: envelopeId,
|
|
93
|
+
payload: {
|
|
94
|
+
team_id: 'T_TEAM',
|
|
95
|
+
api_app_id: 'A_APP',
|
|
96
|
+
event,
|
|
97
|
+
type: 'event_callback',
|
|
98
|
+
event_id: 'Ev123',
|
|
99
|
+
event_time: 1700000000,
|
|
100
|
+
},
|
|
101
|
+
accepts_response_payload: false,
|
|
102
|
+
...extra,
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
simulateSlashCommand(envelopeId: string, payload: Record<string, unknown>) {
|
|
107
|
+
this.simulateMessage({
|
|
108
|
+
type: 'slash_commands',
|
|
109
|
+
envelope_id: envelopeId,
|
|
110
|
+
payload,
|
|
111
|
+
accepts_response_payload: true,
|
|
112
|
+
})
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
simulateInteractive(envelopeId: string, payload: Record<string, unknown>) {
|
|
116
|
+
this.simulateMessage({
|
|
117
|
+
type: 'interactive',
|
|
118
|
+
envelope_id: envelopeId,
|
|
119
|
+
payload,
|
|
120
|
+
accepts_response_payload: true,
|
|
121
|
+
})
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
simulateDisconnect(reason = 'warning') {
|
|
125
|
+
this.simulateMessage({ type: 'disconnect', reason, debug_info: { host: 'h' } })
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
mock.module('ws', () => ({ default: MockWs, __esModule: true }))
|
|
130
|
+
|
|
131
|
+
function createMockClient(overrides: Record<string, any> = {}) {
|
|
132
|
+
return {
|
|
133
|
+
appsConnectionsOpen: mock(() => Promise.resolve({ url: 'wss://wss.slack.com/?ticket=abc' })),
|
|
134
|
+
...overrides,
|
|
135
|
+
} as any
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const APP_TOKEN = 'xapp-1-A123-456-deadbeef'
|
|
139
|
+
|
|
140
|
+
describe('SlackBotListener', () => {
|
|
141
|
+
let listener: SlackBotListener
|
|
142
|
+
|
|
143
|
+
afterEach(() => {
|
|
144
|
+
listener?.stop()
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
describe('constructor', () => {
|
|
148
|
+
it('throws without app token', () => {
|
|
149
|
+
const client = createMockClient()
|
|
150
|
+
expect(() => new SlackBotListener(client, { appToken: '' })).toThrow(/app-level token/i)
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
it('accepts a valid xapp- token', () => {
|
|
154
|
+
const client = createMockClient()
|
|
155
|
+
listener = new SlackBotListener(client, { appToken: APP_TOKEN })
|
|
156
|
+
expect(listener).toBeInstanceOf(SlackBotListener)
|
|
157
|
+
})
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
describe('start', () => {
|
|
161
|
+
it('calls appsConnectionsOpen with the app token and opens WebSocket', async () => {
|
|
162
|
+
const client = createMockClient()
|
|
163
|
+
listener = new SlackBotListener(client, { appToken: APP_TOKEN })
|
|
164
|
+
|
|
165
|
+
await listener.start()
|
|
166
|
+
mockWsInstance.simulateOpen()
|
|
167
|
+
|
|
168
|
+
expect(client.appsConnectionsOpen).toHaveBeenCalledTimes(1)
|
|
169
|
+
expect(client.appsConnectionsOpen).toHaveBeenCalledWith(APP_TOKEN)
|
|
170
|
+
expect(MockWs.lastUrl).toBe('wss://wss.slack.com/?ticket=abc')
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
it('is idempotent', async () => {
|
|
174
|
+
const client = createMockClient()
|
|
175
|
+
listener = new SlackBotListener(client, { appToken: APP_TOKEN })
|
|
176
|
+
|
|
177
|
+
await listener.start()
|
|
178
|
+
await listener.start()
|
|
179
|
+
|
|
180
|
+
expect(client.appsConnectionsOpen).toHaveBeenCalledTimes(1)
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
it('appends debug_reconnects=true when option is set', async () => {
|
|
184
|
+
const client = createMockClient()
|
|
185
|
+
listener = new SlackBotListener(client, { appToken: APP_TOKEN, debugReconnects: true })
|
|
186
|
+
|
|
187
|
+
await listener.start()
|
|
188
|
+
|
|
189
|
+
expect(MockWs.lastUrl).toContain('debug_reconnects=true')
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
it('preserves existing query params when appending debug_reconnects', async () => {
|
|
193
|
+
const client = createMockClient({
|
|
194
|
+
appsConnectionsOpen: mock(() => Promise.resolve({ url: 'wss://wss.slack.com/?ticket=abc' })),
|
|
195
|
+
})
|
|
196
|
+
listener = new SlackBotListener(client, { appToken: APP_TOKEN, debugReconnects: true })
|
|
197
|
+
|
|
198
|
+
await listener.start()
|
|
199
|
+
|
|
200
|
+
expect(MockWs.lastUrl).toBe('wss://wss.slack.com/?ticket=abc&debug_reconnects=true')
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
it('uses ? when URL has no existing query string', async () => {
|
|
204
|
+
const client = createMockClient({
|
|
205
|
+
appsConnectionsOpen: mock(() => Promise.resolve({ url: 'wss://wss.slack.com/' })),
|
|
206
|
+
})
|
|
207
|
+
listener = new SlackBotListener(client, { appToken: APP_TOKEN, debugReconnects: true })
|
|
208
|
+
|
|
209
|
+
await listener.start()
|
|
210
|
+
|
|
211
|
+
expect(MockWs.lastUrl).toBe('wss://wss.slack.com/?debug_reconnects=true')
|
|
212
|
+
})
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
describe('hello envelope', () => {
|
|
216
|
+
it('emits connected with app_id and num_connections', async () => {
|
|
217
|
+
const client = createMockClient()
|
|
218
|
+
listener = new SlackBotListener(client, { appToken: APP_TOKEN })
|
|
219
|
+
|
|
220
|
+
const connected: any[] = []
|
|
221
|
+
listener.on('connected', (info) => connected.push(info))
|
|
222
|
+
|
|
223
|
+
await listener.start()
|
|
224
|
+
mockWsInstance.simulateOpen()
|
|
225
|
+
mockWsInstance.simulateHello()
|
|
226
|
+
|
|
227
|
+
expect(connected.length).toBe(1)
|
|
228
|
+
expect(connected[0].app_id).toBe('A_APP')
|
|
229
|
+
expect(connected[0].num_connections).toBe(1)
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
it('resets reconnect attempts on hello', async () => {
|
|
233
|
+
const client = createMockClient()
|
|
234
|
+
listener = new SlackBotListener(client, { appToken: APP_TOKEN })
|
|
235
|
+
|
|
236
|
+
await listener.start()
|
|
237
|
+
;(listener as any).reconnectAttempts = 5
|
|
238
|
+
|
|
239
|
+
mockWsInstance.simulateOpen()
|
|
240
|
+
mockWsInstance.simulateHello()
|
|
241
|
+
|
|
242
|
+
expect((listener as any).reconnectAttempts).toBe(0)
|
|
243
|
+
})
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
describe('events_api envelope', () => {
|
|
247
|
+
it('emits inner event type with ack and event payload', async () => {
|
|
248
|
+
const client = createMockClient()
|
|
249
|
+
listener = new SlackBotListener(client, { appToken: APP_TOKEN })
|
|
250
|
+
|
|
251
|
+
const args: SlackSocketModeEventsApiArgs<SlackSocketModeMessageEvent>[] = []
|
|
252
|
+
listener.on('message', (a) => args.push(a))
|
|
253
|
+
|
|
254
|
+
await listener.start()
|
|
255
|
+
mockWsInstance.simulateOpen()
|
|
256
|
+
mockWsInstance.simulateHello()
|
|
257
|
+
mockWsInstance.simulateEventsApi('env_001', {
|
|
258
|
+
type: 'message',
|
|
259
|
+
channel: 'C123',
|
|
260
|
+
user: 'U456',
|
|
261
|
+
text: 'hello',
|
|
262
|
+
ts: '111.222',
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
expect(args.length).toBe(1)
|
|
266
|
+
expect(args[0].envelope_id).toBe('env_001')
|
|
267
|
+
expect(args[0].event.type).toBe('message')
|
|
268
|
+
expect(args[0].event.channel).toBe('C123')
|
|
269
|
+
expect(args[0].event.text).toBe('hello')
|
|
270
|
+
expect(args[0].body.team_id).toBe('T_TEAM')
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
it('ack() sends envelope_id back over the WebSocket', async () => {
|
|
274
|
+
const client = createMockClient()
|
|
275
|
+
listener = new SlackBotListener(client, { appToken: APP_TOKEN })
|
|
276
|
+
|
|
277
|
+
let captured: SlackSocketModeEventsApiArgs<SlackSocketModeMessageEvent> | null = null
|
|
278
|
+
listener.on('message', (a) => {
|
|
279
|
+
captured = a
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
await listener.start()
|
|
283
|
+
mockWsInstance.simulateOpen()
|
|
284
|
+
mockWsInstance.simulateHello()
|
|
285
|
+
mockWsInstance.simulateEventsApi('env_002', { type: 'message', channel: 'C', ts: '1' })
|
|
286
|
+
|
|
287
|
+
expect(captured).not.toBeNull()
|
|
288
|
+
captured!.ack()
|
|
289
|
+
|
|
290
|
+
expect(mockWsInstance.sent.length).toBe(1)
|
|
291
|
+
expect(JSON.parse(mockWsInstance.sent[0])).toEqual({ envelope_id: 'env_002' })
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
it('ack(payload) sends envelope_id with response payload', async () => {
|
|
295
|
+
const client = createMockClient()
|
|
296
|
+
listener = new SlackBotListener(client, { appToken: APP_TOKEN })
|
|
297
|
+
|
|
298
|
+
let captured: SlackSocketModeEventsApiArgs<SlackSocketModeMessageEvent> | null = null
|
|
299
|
+
listener.on('message', (a) => {
|
|
300
|
+
captured = a
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
await listener.start()
|
|
304
|
+
mockWsInstance.simulateOpen()
|
|
305
|
+
mockWsInstance.simulateHello()
|
|
306
|
+
mockWsInstance.simulateEventsApi('env_003', { type: 'message', channel: 'C', ts: '1' })
|
|
307
|
+
|
|
308
|
+
captured!.ack({ text: 'ok' })
|
|
309
|
+
|
|
310
|
+
expect(mockWsInstance.sent.length).toBe(1)
|
|
311
|
+
expect(JSON.parse(mockWsInstance.sent[0])).toEqual({
|
|
312
|
+
envelope_id: 'env_003',
|
|
313
|
+
payload: { text: 'ok' },
|
|
314
|
+
})
|
|
315
|
+
})
|
|
316
|
+
|
|
317
|
+
it('ack is idempotent — only the first call hits the wire', async () => {
|
|
318
|
+
const client = createMockClient()
|
|
319
|
+
listener = new SlackBotListener(client, { appToken: APP_TOKEN })
|
|
320
|
+
|
|
321
|
+
let captured: SlackSocketModeEventsApiArgs<SlackSocketModeMessageEvent> | null = null
|
|
322
|
+
listener.on('message', (a) => {
|
|
323
|
+
captured = a
|
|
324
|
+
})
|
|
325
|
+
|
|
326
|
+
await listener.start()
|
|
327
|
+
mockWsInstance.simulateOpen()
|
|
328
|
+
mockWsInstance.simulateHello()
|
|
329
|
+
mockWsInstance.simulateEventsApi('env_004', { type: 'message', channel: 'C', ts: '1' })
|
|
330
|
+
|
|
331
|
+
captured!.ack()
|
|
332
|
+
captured!.ack({ retry: true })
|
|
333
|
+
captured!.ack()
|
|
334
|
+
|
|
335
|
+
expect(mockWsInstance.sent.length).toBe(1)
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
it('exposes retry_attempt and retry_reason', async () => {
|
|
339
|
+
const client = createMockClient()
|
|
340
|
+
listener = new SlackBotListener(client, { appToken: APP_TOKEN })
|
|
341
|
+
|
|
342
|
+
const args: SlackSocketModeEventsApiArgs<SlackSocketModeMessageEvent>[] = []
|
|
343
|
+
listener.on('message', (a) => args.push(a))
|
|
344
|
+
|
|
345
|
+
await listener.start()
|
|
346
|
+
mockWsInstance.simulateOpen()
|
|
347
|
+
mockWsInstance.simulateHello()
|
|
348
|
+
mockWsInstance.simulateEventsApi(
|
|
349
|
+
'env_005',
|
|
350
|
+
{ type: 'message', channel: 'C', ts: '1' },
|
|
351
|
+
{ retry_attempt: 2, retry_reason: 'timeout' },
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
expect(args[0].retry_num).toBe(2)
|
|
355
|
+
expect(args[0].retry_reason).toBe('timeout')
|
|
356
|
+
})
|
|
357
|
+
|
|
358
|
+
it('routes reaction_added to its own listener', async () => {
|
|
359
|
+
const client = createMockClient()
|
|
360
|
+
listener = new SlackBotListener(client, { appToken: APP_TOKEN })
|
|
361
|
+
|
|
362
|
+
const reactions: SlackSocketModeEventsApiArgs<SlackSocketModeReactionEvent>[] = []
|
|
363
|
+
listener.on('reaction_added', (a) => reactions.push(a))
|
|
364
|
+
|
|
365
|
+
await listener.start()
|
|
366
|
+
mockWsInstance.simulateOpen()
|
|
367
|
+
mockWsInstance.simulateHello()
|
|
368
|
+
mockWsInstance.simulateEventsApi('env_r1', {
|
|
369
|
+
type: 'reaction_added',
|
|
370
|
+
user: 'U1',
|
|
371
|
+
reaction: 'thumbsup',
|
|
372
|
+
item: { type: 'message', channel: 'C', ts: '1' },
|
|
373
|
+
event_ts: '2',
|
|
374
|
+
})
|
|
375
|
+
|
|
376
|
+
expect(reactions.length).toBe(1)
|
|
377
|
+
expect(reactions[0].event.reaction).toBe('thumbsup')
|
|
378
|
+
})
|
|
379
|
+
|
|
380
|
+
it('also emits slack_event for every events_api dispatch', async () => {
|
|
381
|
+
const client = createMockClient()
|
|
382
|
+
listener = new SlackBotListener(client, { appToken: APP_TOKEN })
|
|
383
|
+
|
|
384
|
+
const generic: any[] = []
|
|
385
|
+
listener.on('slack_event', (a) => generic.push(a))
|
|
386
|
+
|
|
387
|
+
await listener.start()
|
|
388
|
+
mockWsInstance.simulateOpen()
|
|
389
|
+
mockWsInstance.simulateHello()
|
|
390
|
+
mockWsInstance.simulateEventsApi('env_g1', { type: 'message', channel: 'C', ts: '1' })
|
|
391
|
+
mockWsInstance.simulateEventsApi('env_g2', { type: 'app_mention', channel: 'C', user: 'U', text: 'hi', ts: '2' })
|
|
392
|
+
|
|
393
|
+
expect(generic.length).toBe(2)
|
|
394
|
+
})
|
|
395
|
+
|
|
396
|
+
it('ignores events_api envelope with no inner event.type', async () => {
|
|
397
|
+
const client = createMockClient()
|
|
398
|
+
listener = new SlackBotListener(client, { appToken: APP_TOKEN })
|
|
399
|
+
|
|
400
|
+
const events: any[] = []
|
|
401
|
+
listener.on('slack_event', (a) => events.push(a))
|
|
402
|
+
|
|
403
|
+
await listener.start()
|
|
404
|
+
mockWsInstance.simulateOpen()
|
|
405
|
+
mockWsInstance.simulateHello()
|
|
406
|
+
mockWsInstance.simulateMessage({
|
|
407
|
+
type: 'events_api',
|
|
408
|
+
envelope_id: 'env_bad',
|
|
409
|
+
payload: { event: {} },
|
|
410
|
+
})
|
|
411
|
+
|
|
412
|
+
expect(events.length).toBe(0)
|
|
413
|
+
})
|
|
414
|
+
})
|
|
415
|
+
|
|
416
|
+
describe('slash_commands envelope', () => {
|
|
417
|
+
it('emits slash_commands with ack and command payload', async () => {
|
|
418
|
+
const client = createMockClient()
|
|
419
|
+
listener = new SlackBotListener(client, { appToken: APP_TOKEN })
|
|
420
|
+
|
|
421
|
+
const commands: SlackSocketModeSlashCommandArgs[] = []
|
|
422
|
+
listener.on('slash_commands', (a) => commands.push(a))
|
|
423
|
+
|
|
424
|
+
await listener.start()
|
|
425
|
+
mockWsInstance.simulateOpen()
|
|
426
|
+
mockWsInstance.simulateHello()
|
|
427
|
+
mockWsInstance.simulateSlashCommand('env_sc1', {
|
|
428
|
+
command: '/deploy',
|
|
429
|
+
text: 'production',
|
|
430
|
+
user_id: 'U1',
|
|
431
|
+
channel_id: 'C1',
|
|
432
|
+
team_id: 'T1',
|
|
433
|
+
})
|
|
434
|
+
|
|
435
|
+
expect(commands.length).toBe(1)
|
|
436
|
+
expect(commands[0].body.command).toBe('/deploy')
|
|
437
|
+
expect(commands[0].body.text).toBe('production')
|
|
438
|
+
expect(commands[0].accepts_response_payload).toBe(true)
|
|
439
|
+
|
|
440
|
+
commands[0].ack({ text: 'Deploying...' })
|
|
441
|
+
|
|
442
|
+
expect(mockWsInstance.sent.length).toBe(1)
|
|
443
|
+
expect(JSON.parse(mockWsInstance.sent[0])).toEqual({
|
|
444
|
+
envelope_id: 'env_sc1',
|
|
445
|
+
payload: { text: 'Deploying...' },
|
|
446
|
+
})
|
|
447
|
+
})
|
|
448
|
+
})
|
|
449
|
+
|
|
450
|
+
describe('interactive envelope', () => {
|
|
451
|
+
it('emits interactive with ack and action payload', async () => {
|
|
452
|
+
const client = createMockClient()
|
|
453
|
+
listener = new SlackBotListener(client, { appToken: APP_TOKEN })
|
|
454
|
+
|
|
455
|
+
const interactions: SlackSocketModeInteractiveArgs[] = []
|
|
456
|
+
listener.on('interactive', (a) => interactions.push(a))
|
|
457
|
+
|
|
458
|
+
await listener.start()
|
|
459
|
+
mockWsInstance.simulateOpen()
|
|
460
|
+
mockWsInstance.simulateHello()
|
|
461
|
+
mockWsInstance.simulateInteractive('env_i1', {
|
|
462
|
+
type: 'block_actions',
|
|
463
|
+
user: { id: 'U1' },
|
|
464
|
+
actions: [{ action_id: 'approve', value: 'PR-123' }],
|
|
465
|
+
})
|
|
466
|
+
|
|
467
|
+
expect(interactions.length).toBe(1)
|
|
468
|
+
expect(interactions[0].body.type).toBe('block_actions')
|
|
469
|
+
|
|
470
|
+
interactions[0].ack()
|
|
471
|
+
|
|
472
|
+
expect(JSON.parse(mockWsInstance.sent[0])).toEqual({ envelope_id: 'env_i1' })
|
|
473
|
+
})
|
|
474
|
+
})
|
|
475
|
+
|
|
476
|
+
describe('disconnect envelope', () => {
|
|
477
|
+
it('closes the WebSocket on disconnect message (triggering reconnect)', async () => {
|
|
478
|
+
const client = createMockClient()
|
|
479
|
+
listener = new SlackBotListener(client, { appToken: APP_TOKEN })
|
|
480
|
+
|
|
481
|
+
await listener.start()
|
|
482
|
+
mockWsInstance.simulateOpen()
|
|
483
|
+
mockWsInstance.simulateHello()
|
|
484
|
+
|
|
485
|
+
const ws = mockWsInstance
|
|
486
|
+
mockWsInstance.simulateDisconnect('refresh_requested')
|
|
487
|
+
|
|
488
|
+
expect(ws.readyState).toBe(MockWs.CLOSED)
|
|
489
|
+
})
|
|
490
|
+
|
|
491
|
+
it('does not emit slack_event for disconnect envelopes', async () => {
|
|
492
|
+
const client = createMockClient()
|
|
493
|
+
listener = new SlackBotListener(client, { appToken: APP_TOKEN })
|
|
494
|
+
|
|
495
|
+
const events: any[] = []
|
|
496
|
+
listener.on('slack_event', (a) => events.push(a))
|
|
497
|
+
|
|
498
|
+
await listener.start()
|
|
499
|
+
mockWsInstance.simulateOpen()
|
|
500
|
+
mockWsInstance.simulateHello()
|
|
501
|
+
mockWsInstance.simulateDisconnect('warning')
|
|
502
|
+
|
|
503
|
+
expect(events.length).toBe(0)
|
|
504
|
+
})
|
|
505
|
+
})
|
|
506
|
+
|
|
507
|
+
describe('unknown envelope', () => {
|
|
508
|
+
it('emits slack_event for envelopes the listener does not specifically handle', async () => {
|
|
509
|
+
const client = createMockClient()
|
|
510
|
+
listener = new SlackBotListener(client, { appToken: APP_TOKEN })
|
|
511
|
+
|
|
512
|
+
const events: any[] = []
|
|
513
|
+
listener.on('slack_event', (a) => events.push(a))
|
|
514
|
+
|
|
515
|
+
await listener.start()
|
|
516
|
+
mockWsInstance.simulateOpen()
|
|
517
|
+
mockWsInstance.simulateHello()
|
|
518
|
+
mockWsInstance.simulateMessage({ type: 'something_new', envelope_id: 'env_x', payload: { foo: 'bar' } })
|
|
519
|
+
|
|
520
|
+
expect(events.length).toBe(1)
|
|
521
|
+
expect(events[0].type).toBe('something_new')
|
|
522
|
+
})
|
|
523
|
+
})
|
|
524
|
+
|
|
525
|
+
describe('malformed frames', () => {
|
|
526
|
+
it('does not emit error or crash on invalid JSON', async () => {
|
|
527
|
+
const client = createMockClient()
|
|
528
|
+
listener = new SlackBotListener(client, { appToken: APP_TOKEN })
|
|
529
|
+
|
|
530
|
+
const errors: Error[] = []
|
|
531
|
+
listener.on('error', (e) => errors.push(e))
|
|
532
|
+
|
|
533
|
+
await listener.start()
|
|
534
|
+
mockWsInstance.simulateOpen()
|
|
535
|
+
mockWsInstance.simulateRawMessage('not json')
|
|
536
|
+
|
|
537
|
+
expect(errors.length).toBe(0)
|
|
538
|
+
})
|
|
539
|
+
})
|
|
540
|
+
|
|
541
|
+
describe('ping/pong', () => {
|
|
542
|
+
it('clears the pong timeout when a pong is received', async () => {
|
|
543
|
+
const client = createMockClient()
|
|
544
|
+
listener = new SlackBotListener(client, { appToken: APP_TOKEN })
|
|
545
|
+
|
|
546
|
+
await listener.start()
|
|
547
|
+
mockWsInstance.simulateOpen()
|
|
548
|
+
;(listener as any).pongTimer = setTimeout(() => {}, 60_000)
|
|
549
|
+
|
|
550
|
+
mockWsInstance.simulatePong()
|
|
551
|
+
|
|
552
|
+
expect((listener as any).pongTimer).toBeNull()
|
|
553
|
+
})
|
|
554
|
+
})
|
|
555
|
+
|
|
556
|
+
describe('stop', () => {
|
|
557
|
+
it('closes the WebSocket and prevents reconnection', async () => {
|
|
558
|
+
const client = createMockClient()
|
|
559
|
+
listener = new SlackBotListener(client, { appToken: APP_TOKEN })
|
|
560
|
+
|
|
561
|
+
await listener.start()
|
|
562
|
+
mockWsInstance.simulateOpen()
|
|
563
|
+
mockWsInstance.simulateHello()
|
|
564
|
+
|
|
565
|
+
listener.stop()
|
|
566
|
+
await new Promise((r) => setTimeout(r, 50))
|
|
567
|
+
|
|
568
|
+
expect(client.appsConnectionsOpen).toHaveBeenCalledTimes(1)
|
|
569
|
+
})
|
|
570
|
+
|
|
571
|
+
it('ack after stop is a no-op', async () => {
|
|
572
|
+
const client = createMockClient()
|
|
573
|
+
listener = new SlackBotListener(client, { appToken: APP_TOKEN })
|
|
574
|
+
|
|
575
|
+
let captured: SlackSocketModeEventsApiArgs<SlackSocketModeMessageEvent> | null = null
|
|
576
|
+
listener.on('message', (a) => {
|
|
577
|
+
captured = a
|
|
578
|
+
})
|
|
579
|
+
|
|
580
|
+
await listener.start()
|
|
581
|
+
mockWsInstance.simulateOpen()
|
|
582
|
+
mockWsInstance.simulateHello()
|
|
583
|
+
mockWsInstance.simulateEventsApi('env_stop', { type: 'message', channel: 'C', ts: '1' })
|
|
584
|
+
|
|
585
|
+
const ws = mockWsInstance
|
|
586
|
+
listener.stop()
|
|
587
|
+
ws.sent = []
|
|
588
|
+
|
|
589
|
+
captured!.ack()
|
|
590
|
+
|
|
591
|
+
expect(ws.sent.length).toBe(0)
|
|
592
|
+
})
|
|
593
|
+
})
|
|
594
|
+
|
|
595
|
+
describe('reconnection', () => {
|
|
596
|
+
it('reconnects after WebSocket close while running', async () => {
|
|
597
|
+
const client = createMockClient()
|
|
598
|
+
listener = new SlackBotListener(client, { appToken: APP_TOKEN })
|
|
599
|
+
|
|
600
|
+
const disconnected: boolean[] = []
|
|
601
|
+
listener.on('disconnected', () => disconnected.push(true))
|
|
602
|
+
|
|
603
|
+
await listener.start()
|
|
604
|
+
mockWsInstance.simulateOpen()
|
|
605
|
+
mockWsInstance.simulateHello()
|
|
606
|
+
mockWsInstance.simulateClose()
|
|
607
|
+
|
|
608
|
+
expect(disconnected.length).toBe(1)
|
|
609
|
+
|
|
610
|
+
await new Promise((r) => setTimeout(r, 1500))
|
|
611
|
+
expect(client.appsConnectionsOpen.mock.calls.length).toBeGreaterThanOrEqual(2)
|
|
612
|
+
})
|
|
613
|
+
|
|
614
|
+
it('emits error and reconnects on appsConnectionsOpen network failure', async () => {
|
|
615
|
+
let callCount = 0
|
|
616
|
+
const client = createMockClient({
|
|
617
|
+
appsConnectionsOpen: mock(() => {
|
|
618
|
+
callCount++
|
|
619
|
+
if (callCount === 1) return Promise.reject(new Error('network_error'))
|
|
620
|
+
return Promise.resolve({ url: 'wss://wss.slack.com/?ticket=2' })
|
|
621
|
+
}),
|
|
622
|
+
})
|
|
623
|
+
|
|
624
|
+
listener = new SlackBotListener(client, { appToken: APP_TOKEN })
|
|
625
|
+
|
|
626
|
+
const errors: Error[] = []
|
|
627
|
+
listener.on('error', (e) => errors.push(e))
|
|
628
|
+
|
|
629
|
+
await listener.start()
|
|
630
|
+
await new Promise((r) => setTimeout(r, 1500))
|
|
631
|
+
|
|
632
|
+
expect(errors.length).toBe(1)
|
|
633
|
+
expect(errors[0].message).toBe('network_error')
|
|
634
|
+
expect(client.appsConnectionsOpen.mock.calls.length).toBeGreaterThanOrEqual(2)
|
|
635
|
+
})
|
|
636
|
+
|
|
637
|
+
it('does not reconnect on fatal Slack errors', async () => {
|
|
638
|
+
const fatal: any = new Error('invalid auth')
|
|
639
|
+
fatal.code = 'invalid_auth'
|
|
640
|
+
|
|
641
|
+
const client = createMockClient({
|
|
642
|
+
appsConnectionsOpen: mock(() => Promise.reject(fatal)),
|
|
643
|
+
})
|
|
644
|
+
|
|
645
|
+
listener = new SlackBotListener(client, { appToken: APP_TOKEN })
|
|
646
|
+
|
|
647
|
+
const errors: Error[] = []
|
|
648
|
+
listener.on('error', (e) => errors.push(e))
|
|
649
|
+
|
|
650
|
+
await listener.start()
|
|
651
|
+
await new Promise((r) => setTimeout(r, 1500))
|
|
652
|
+
|
|
653
|
+
expect(errors.length).toBe(1)
|
|
654
|
+
expect(client.appsConnectionsOpen).toHaveBeenCalledTimes(1)
|
|
655
|
+
})
|
|
656
|
+
|
|
657
|
+
it('does not reconnect on missing_app_token', async () => {
|
|
658
|
+
const fatal: any = new Error('missing app token')
|
|
659
|
+
fatal.code = 'missing_app_token'
|
|
660
|
+
|
|
661
|
+
const client = createMockClient({
|
|
662
|
+
appsConnectionsOpen: mock(() => Promise.reject(fatal)),
|
|
663
|
+
})
|
|
664
|
+
|
|
665
|
+
listener = new SlackBotListener(client, { appToken: APP_TOKEN })
|
|
666
|
+
|
|
667
|
+
const errors: Error[] = []
|
|
668
|
+
listener.on('error', (e) => errors.push(e))
|
|
669
|
+
|
|
670
|
+
await listener.start()
|
|
671
|
+
await new Promise((r) => setTimeout(r, 1500))
|
|
672
|
+
|
|
673
|
+
expect(client.appsConnectionsOpen).toHaveBeenCalledTimes(1)
|
|
674
|
+
})
|
|
675
|
+
|
|
676
|
+
it('resets reconnectAttempts to 0 on hello after reconnect', async () => {
|
|
677
|
+
const client = createMockClient()
|
|
678
|
+
listener = new SlackBotListener(client, { appToken: APP_TOKEN })
|
|
679
|
+
|
|
680
|
+
await listener.start()
|
|
681
|
+
;(listener as any).reconnectAttempts = 3
|
|
682
|
+
mockWsInstance.simulateOpen()
|
|
683
|
+
mockWsInstance.simulateHello()
|
|
684
|
+
|
|
685
|
+
expect((listener as any).reconnectAttempts).toBe(0)
|
|
686
|
+
})
|
|
687
|
+
})
|
|
688
|
+
|
|
689
|
+
describe('on/off/once', () => {
|
|
690
|
+
it('off removes a listener', async () => {
|
|
691
|
+
const client = createMockClient()
|
|
692
|
+
listener = new SlackBotListener(client, { appToken: APP_TOKEN })
|
|
693
|
+
|
|
694
|
+
const messages: any[] = []
|
|
695
|
+
const handler = (a: any) => messages.push(a.event)
|
|
696
|
+
listener.on('message', handler)
|
|
697
|
+
|
|
698
|
+
await listener.start()
|
|
699
|
+
mockWsInstance.simulateOpen()
|
|
700
|
+
mockWsInstance.simulateHello()
|
|
701
|
+
mockWsInstance.simulateEventsApi('e1', { type: 'message', channel: 'C', text: 'a', ts: '1' })
|
|
702
|
+
|
|
703
|
+
listener.off('message', handler)
|
|
704
|
+
mockWsInstance.simulateEventsApi('e2', { type: 'message', channel: 'C', text: 'b', ts: '2' })
|
|
705
|
+
|
|
706
|
+
expect(messages.length).toBe(1)
|
|
707
|
+
expect(messages[0].text).toBe('a')
|
|
708
|
+
})
|
|
709
|
+
|
|
710
|
+
it('once fires only once', async () => {
|
|
711
|
+
const client = createMockClient()
|
|
712
|
+
listener = new SlackBotListener(client, { appToken: APP_TOKEN })
|
|
713
|
+
|
|
714
|
+
const messages: any[] = []
|
|
715
|
+
listener.once('message', (a) => messages.push(a.event))
|
|
716
|
+
|
|
717
|
+
await listener.start()
|
|
718
|
+
mockWsInstance.simulateOpen()
|
|
719
|
+
mockWsInstance.simulateHello()
|
|
720
|
+
mockWsInstance.simulateEventsApi('e1', { type: 'message', channel: 'C', text: 'first', ts: '1' })
|
|
721
|
+
mockWsInstance.simulateEventsApi('e2', { type: 'message', channel: 'C', text: 'second', ts: '2' })
|
|
722
|
+
|
|
723
|
+
expect(messages.length).toBe(1)
|
|
724
|
+
expect(messages[0].text).toBe('first')
|
|
725
|
+
})
|
|
726
|
+
})
|
|
727
|
+
|
|
728
|
+
describe('generation guard', () => {
|
|
729
|
+
it('ignores frames from a stale socket after stop/start', async () => {
|
|
730
|
+
const client = createMockClient()
|
|
731
|
+
listener = new SlackBotListener(client, { appToken: APP_TOKEN })
|
|
732
|
+
|
|
733
|
+
const messages: any[] = []
|
|
734
|
+
listener.on('message', (a) => messages.push(a.event))
|
|
735
|
+
|
|
736
|
+
await listener.start()
|
|
737
|
+
const staleWs = mockWsInstance
|
|
738
|
+
staleWs.simulateOpen()
|
|
739
|
+
|
|
740
|
+
listener.stop()
|
|
741
|
+
await listener.start()
|
|
742
|
+
|
|
743
|
+
staleWs.simulateMessage({
|
|
744
|
+
type: 'events_api',
|
|
745
|
+
envelope_id: 'stale',
|
|
746
|
+
payload: { event: { type: 'message', channel: 'C', text: 'old', ts: '1' } },
|
|
747
|
+
})
|
|
748
|
+
|
|
749
|
+
expect(messages.length).toBe(0)
|
|
750
|
+
})
|
|
751
|
+
|
|
752
|
+
it('ignores stale close events after stop+start (does not double-schedule reconnect)', async () => {
|
|
753
|
+
const client = createMockClient()
|
|
754
|
+
listener = new SlackBotListener(client, { appToken: APP_TOKEN })
|
|
755
|
+
|
|
756
|
+
await listener.start()
|
|
757
|
+
const staleWs = mockWsInstance
|
|
758
|
+
staleWs.simulateOpen()
|
|
759
|
+
|
|
760
|
+
listener.stop()
|
|
761
|
+
await listener.start()
|
|
762
|
+
const callsAfterRestart = client.appsConnectionsOpen.mock.calls.length
|
|
763
|
+
|
|
764
|
+
staleWs.simulateClose()
|
|
765
|
+
await new Promise((r) => setTimeout(r, 1500))
|
|
766
|
+
|
|
767
|
+
expect(client.appsConnectionsOpen.mock.calls.length).toBe(callsAfterRestart)
|
|
768
|
+
})
|
|
769
|
+
|
|
770
|
+
it('stale ack after reconnect targets the new socket guard, not the old socket', async () => {
|
|
771
|
+
const client = createMockClient()
|
|
772
|
+
listener = new SlackBotListener(client, { appToken: APP_TOKEN })
|
|
773
|
+
|
|
774
|
+
let captured: SlackSocketModeEventsApiArgs<SlackSocketModeMessageEvent> | null = null
|
|
775
|
+
listener.on('message', (a) => {
|
|
776
|
+
captured = a
|
|
777
|
+
})
|
|
778
|
+
|
|
779
|
+
await listener.start()
|
|
780
|
+
const staleWs = mockWsInstance
|
|
781
|
+
staleWs.simulateOpen()
|
|
782
|
+
staleWs.simulateHello()
|
|
783
|
+
staleWs.simulateEventsApi('env_stale', { type: 'message', channel: 'C', ts: '1' })
|
|
784
|
+
|
|
785
|
+
listener.stop()
|
|
786
|
+
await listener.start()
|
|
787
|
+
|
|
788
|
+
const newWs = mockWsInstance
|
|
789
|
+
expect(newWs).not.toBe(staleWs)
|
|
790
|
+
|
|
791
|
+
captured!.ack()
|
|
792
|
+
|
|
793
|
+
expect(newWs.sent.length).toBe(0)
|
|
794
|
+
expect(staleWs.sent.length).toBe(0)
|
|
795
|
+
})
|
|
796
|
+
})
|
|
797
|
+
|
|
798
|
+
describe('start after stop', () => {
|
|
799
|
+
it('resets reconnect attempts on fresh start', async () => {
|
|
800
|
+
const client = createMockClient()
|
|
801
|
+
listener = new SlackBotListener(client, { appToken: APP_TOKEN })
|
|
802
|
+
|
|
803
|
+
await listener.start()
|
|
804
|
+
;(listener as any).reconnectAttempts = 5
|
|
805
|
+
listener.stop()
|
|
806
|
+
|
|
807
|
+
await listener.start()
|
|
808
|
+
expect((listener as any).reconnectAttempts).toBe(0)
|
|
809
|
+
})
|
|
810
|
+
|
|
811
|
+
it('clears retryAfter floor on fresh start', async () => {
|
|
812
|
+
const client = createMockClient()
|
|
813
|
+
listener = new SlackBotListener(client, { appToken: APP_TOKEN })
|
|
814
|
+
|
|
815
|
+
await listener.start()
|
|
816
|
+
;(listener as any).nextReconnectFloorMs = 5000
|
|
817
|
+
listener.stop()
|
|
818
|
+
|
|
819
|
+
await listener.start()
|
|
820
|
+
expect((listener as any).nextReconnectFloorMs).toBe(0)
|
|
821
|
+
})
|
|
822
|
+
})
|
|
823
|
+
|
|
824
|
+
describe('zombie connection', () => {
|
|
825
|
+
it('closes the WebSocket when no pong arrives within the timeout', async () => {
|
|
826
|
+
const client = createMockClient()
|
|
827
|
+
listener = new SlackBotListener(client, { appToken: APP_TOKEN })
|
|
828
|
+
|
|
829
|
+
await listener.start()
|
|
830
|
+
mockWsInstance.simulateOpen()
|
|
831
|
+
mockWsInstance.simulateHello()
|
|
832
|
+
|
|
833
|
+
const ws = mockWsInstance
|
|
834
|
+
|
|
835
|
+
const pingTimer = (listener as any).pingTimer
|
|
836
|
+
if (pingTimer) clearInterval(pingTimer)
|
|
837
|
+
ws.ping()
|
|
838
|
+
;(listener as any).pongTimer = setTimeout(() => {
|
|
839
|
+
if (!(listener as any).isCurrent((listener as any).generation, ws)) return
|
|
840
|
+
ws.close()
|
|
841
|
+
}, 5)
|
|
842
|
+
|
|
843
|
+
await new Promise((r) => setTimeout(r, 30))
|
|
844
|
+
|
|
845
|
+
expect(ws.readyState).toBe(MockWs.CLOSED)
|
|
846
|
+
})
|
|
847
|
+
})
|
|
848
|
+
|
|
849
|
+
describe('disconnect envelope reason handling', () => {
|
|
850
|
+
it('resets reconnectAttempts on non-terminal disconnect (refresh_requested)', async () => {
|
|
851
|
+
const client = createMockClient()
|
|
852
|
+
listener = new SlackBotListener(client, { appToken: APP_TOKEN })
|
|
853
|
+
|
|
854
|
+
await listener.start()
|
|
855
|
+
mockWsInstance.simulateOpen()
|
|
856
|
+
mockWsInstance.simulateHello()
|
|
857
|
+
|
|
858
|
+
;(listener as any).reconnectAttempts = 5
|
|
859
|
+
mockWsInstance.simulateDisconnect('refresh_requested')
|
|
860
|
+
|
|
861
|
+
expect((listener as any).reconnectAttempts).toBe(0)
|
|
862
|
+
})
|
|
863
|
+
|
|
864
|
+
it('resets reconnectAttempts on warning disconnect', async () => {
|
|
865
|
+
const client = createMockClient()
|
|
866
|
+
listener = new SlackBotListener(client, { appToken: APP_TOKEN })
|
|
867
|
+
|
|
868
|
+
await listener.start()
|
|
869
|
+
mockWsInstance.simulateOpen()
|
|
870
|
+
mockWsInstance.simulateHello()
|
|
871
|
+
|
|
872
|
+
;(listener as any).reconnectAttempts = 3
|
|
873
|
+
mockWsInstance.simulateDisconnect('warning')
|
|
874
|
+
|
|
875
|
+
expect((listener as any).reconnectAttempts).toBe(0)
|
|
876
|
+
})
|
|
877
|
+
|
|
878
|
+
it('treats link_disabled as terminal: emits error, stops, no reconnect', async () => {
|
|
879
|
+
const client = createMockClient()
|
|
880
|
+
listener = new SlackBotListener(client, { appToken: APP_TOKEN })
|
|
881
|
+
|
|
882
|
+
const errors: Error[] = []
|
|
883
|
+
listener.on('error', (e) => errors.push(e))
|
|
884
|
+
|
|
885
|
+
await listener.start()
|
|
886
|
+
mockWsInstance.simulateOpen()
|
|
887
|
+
mockWsInstance.simulateHello()
|
|
888
|
+
|
|
889
|
+
const callsBeforeDisconnect = client.appsConnectionsOpen.mock.calls.length
|
|
890
|
+
mockWsInstance.simulateDisconnect('link_disabled')
|
|
891
|
+
await new Promise((r) => setTimeout(r, 1500))
|
|
892
|
+
|
|
893
|
+
expect(errors.length).toBe(1)
|
|
894
|
+
expect(errors[0].message).toMatch(/link_disabled/)
|
|
895
|
+
expect(client.appsConnectionsOpen.mock.calls.length).toBe(callsBeforeDisconnect)
|
|
896
|
+
})
|
|
897
|
+
})
|
|
898
|
+
|
|
899
|
+
describe('hello timeout', () => {
|
|
900
|
+
it('closes the socket if hello does not arrive within HELLO_TIMEOUT', async () => {
|
|
901
|
+
const client = createMockClient()
|
|
902
|
+
listener = new SlackBotListener(client, { appToken: APP_TOKEN })
|
|
903
|
+
|
|
904
|
+
await listener.start()
|
|
905
|
+
mockWsInstance.simulateOpen()
|
|
906
|
+
const ws = mockWsInstance
|
|
907
|
+
|
|
908
|
+
const helloTimer = (listener as any).helloTimer
|
|
909
|
+
expect(helloTimer).not.toBeNull()
|
|
910
|
+
|
|
911
|
+
;(listener as any).clearHelloTimer()
|
|
912
|
+
;(listener as any).helloTimer = setTimeout(() => {
|
|
913
|
+
if (!(listener as any).isCurrent((listener as any).generation, ws)) return
|
|
914
|
+
ws.close()
|
|
915
|
+
}, 5)
|
|
916
|
+
|
|
917
|
+
await new Promise((r) => setTimeout(r, 30))
|
|
918
|
+
|
|
919
|
+
expect(ws.readyState).toBe(MockWs.CLOSED)
|
|
920
|
+
})
|
|
921
|
+
|
|
922
|
+
it('clears the hello timer once hello arrives', async () => {
|
|
923
|
+
const client = createMockClient()
|
|
924
|
+
listener = new SlackBotListener(client, { appToken: APP_TOKEN })
|
|
925
|
+
|
|
926
|
+
await listener.start()
|
|
927
|
+
mockWsInstance.simulateOpen()
|
|
928
|
+
expect((listener as any).helloTimer).not.toBeNull()
|
|
929
|
+
|
|
930
|
+
mockWsInstance.simulateHello()
|
|
931
|
+
expect((listener as any).helloTimer).toBeNull()
|
|
932
|
+
})
|
|
933
|
+
})
|
|
934
|
+
|
|
935
|
+
describe('retryAfter floor', () => {
|
|
936
|
+
it('uses retryAfter as a floor on the next reconnect delay', async () => {
|
|
937
|
+
const rateLimited: any = new Error('rate limited')
|
|
938
|
+
rateLimited.code = 'slack_webapi_rate_limited_error'
|
|
939
|
+
rateLimited.retryAfter = 5
|
|
940
|
+
|
|
941
|
+
const client = createMockClient({
|
|
942
|
+
appsConnectionsOpen: mock(() => Promise.reject(rateLimited)),
|
|
943
|
+
})
|
|
944
|
+
|
|
945
|
+
listener = new SlackBotListener(client, { appToken: APP_TOKEN })
|
|
946
|
+
listener.on('error', () => {})
|
|
947
|
+
|
|
948
|
+
const setTimeoutSpy = mock<typeof setTimeout>()
|
|
949
|
+
const realSetTimeout = globalThis.setTimeout
|
|
950
|
+
globalThis.setTimeout = ((fn: () => void, ms?: number) => {
|
|
951
|
+
setTimeoutSpy(fn, ms)
|
|
952
|
+
return realSetTimeout(() => {}, 1_000_000) as any
|
|
953
|
+
}) as any
|
|
954
|
+
|
|
955
|
+
try {
|
|
956
|
+
await listener.start()
|
|
957
|
+
} finally {
|
|
958
|
+
globalThis.setTimeout = realSetTimeout
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
const reconnectCalls = setTimeoutSpy.mock.calls.filter((call) => {
|
|
962
|
+
const ms = call[1] as number | undefined
|
|
963
|
+
return typeof ms === 'number' && ms >= 1000
|
|
964
|
+
})
|
|
965
|
+
expect(reconnectCalls.length).toBeGreaterThanOrEqual(1)
|
|
966
|
+
expect(reconnectCalls[0][1]).toBe(5000)
|
|
967
|
+
})
|
|
968
|
+
|
|
969
|
+
it('does not set floor when retryAfter is absent (uses base exponential delay)', async () => {
|
|
970
|
+
const networkErr: any = new Error('boom')
|
|
971
|
+
const client = createMockClient({
|
|
972
|
+
appsConnectionsOpen: mock(() => Promise.reject(networkErr)),
|
|
973
|
+
})
|
|
974
|
+
|
|
975
|
+
listener = new SlackBotListener(client, { appToken: APP_TOKEN })
|
|
976
|
+
listener.on('error', () => {})
|
|
977
|
+
|
|
978
|
+
const setTimeoutSpy = mock<typeof setTimeout>()
|
|
979
|
+
const realSetTimeout = globalThis.setTimeout
|
|
980
|
+
globalThis.setTimeout = ((fn: () => void, ms?: number) => {
|
|
981
|
+
setTimeoutSpy(fn, ms)
|
|
982
|
+
return realSetTimeout(() => {}, 1_000_000) as any
|
|
983
|
+
}) as any
|
|
984
|
+
|
|
985
|
+
try {
|
|
986
|
+
await listener.start()
|
|
987
|
+
} finally {
|
|
988
|
+
globalThis.setTimeout = realSetTimeout
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
const reconnectCalls = setTimeoutSpy.mock.calls.filter((call) => {
|
|
992
|
+
const ms = call[1] as number | undefined
|
|
993
|
+
return typeof ms === 'number' && ms >= 1000
|
|
994
|
+
})
|
|
995
|
+
expect(reconnectCalls.length).toBeGreaterThanOrEqual(1)
|
|
996
|
+
expect(reconnectCalls[0][1]).toBe(1000)
|
|
997
|
+
})
|
|
998
|
+
|
|
999
|
+
it('clears floor after applying it once', async () => {
|
|
1000
|
+
const client = createMockClient()
|
|
1001
|
+
listener = new SlackBotListener(client, { appToken: APP_TOKEN })
|
|
1002
|
+
|
|
1003
|
+
;(listener as any).running = true
|
|
1004
|
+
;(listener as any).generation = 1
|
|
1005
|
+
;(listener as any).nextReconnectFloorMs = 7000
|
|
1006
|
+
;(listener as any).scheduleReconnect()
|
|
1007
|
+
|
|
1008
|
+
expect((listener as any).nextReconnectFloorMs).toBe(0)
|
|
1009
|
+
clearTimeout((listener as any).reconnectTimer)
|
|
1010
|
+
})
|
|
1011
|
+
})
|
|
1012
|
+
})
|