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,1002 @@
|
|
|
1
|
+
import { afterEach, describe, expect, mock, it } from 'bun:test'
|
|
2
|
+
|
|
3
|
+
import { DiscordBotListener } from '@/platforms/discordbot/listener'
|
|
4
|
+
import type {
|
|
5
|
+
DiscordGatewayGenericEvent,
|
|
6
|
+
DiscordGatewayMessageCreateEvent,
|
|
7
|
+
DiscordGatewayMessageDeleteEvent,
|
|
8
|
+
DiscordGatewayMessageUpdateEvent,
|
|
9
|
+
DiscordGatewayReactionEvent,
|
|
10
|
+
} from '@/platforms/discordbot/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
|
+
|
|
22
|
+
private handlers = new Map<string, WsHandler[]>()
|
|
23
|
+
sent: string[] = []
|
|
24
|
+
url: string
|
|
25
|
+
|
|
26
|
+
constructor(url: string, _options?: any) {
|
|
27
|
+
this.url = url
|
|
28
|
+
MockWs.lastUrl = url
|
|
29
|
+
// oxlint-disable-next-line typescript-eslint/no-this-alias
|
|
30
|
+
mockWsInstance = this
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
on(event: string, handler: WsHandler) {
|
|
34
|
+
const list = this.handlers.get(event) ?? []
|
|
35
|
+
list.push(handler)
|
|
36
|
+
this.handlers.set(event, list)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
send(data: string) {
|
|
40
|
+
this.sent.push(data)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
close(code?: number) {
|
|
44
|
+
this.readyState = MockWs.CLOSED
|
|
45
|
+
setTimeout(() => this.emit('close', code ?? 1000), 0)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
emit(event: string, ...args: any[]) {
|
|
49
|
+
for (const handler of this.handlers.get(event) ?? []) {
|
|
50
|
+
handler(...args)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
simulateOpen() {
|
|
55
|
+
this.emit('open')
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
simulateMessage(data: Record<string, unknown>) {
|
|
59
|
+
this.emit('message', Buffer.from(JSON.stringify(data)))
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
simulateClose(code?: number) {
|
|
63
|
+
this.readyState = MockWs.CLOSED
|
|
64
|
+
this.emit('close', code ?? 1000)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
simulateHello(interval = 41250) {
|
|
68
|
+
this.simulateMessage({ op: 10, d: { heartbeat_interval: interval } })
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
simulateReady(sessionId = 'session_123') {
|
|
72
|
+
this.simulateMessage({
|
|
73
|
+
op: 0,
|
|
74
|
+
t: 'READY',
|
|
75
|
+
s: 1,
|
|
76
|
+
d: {
|
|
77
|
+
session_id: sessionId,
|
|
78
|
+
resume_gateway_url: 'wss://resume.discord.gg',
|
|
79
|
+
user: { id: 'BOT_SELF', username: 'testbot' },
|
|
80
|
+
},
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
simulateDispatch(t: string, d: Record<string, unknown>, s: number) {
|
|
85
|
+
this.simulateMessage({ op: 0, t, s, d })
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
simulateHeartbeatACK() {
|
|
89
|
+
this.simulateMessage({ op: 11 })
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
simulateReconnect() {
|
|
93
|
+
this.simulateMessage({ op: 7 })
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
simulateInvalidSession(resumable: boolean) {
|
|
97
|
+
this.simulateMessage({ op: 9, d: resumable })
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
mock.module('ws', () => ({ default: MockWs, __esModule: true }))
|
|
102
|
+
|
|
103
|
+
function createMockClient(overrides: Record<string, any> = {}) {
|
|
104
|
+
return {
|
|
105
|
+
gatewayConnect: mock(() => Promise.resolve({ token: 'fake-bot-token' })),
|
|
106
|
+
...overrides,
|
|
107
|
+
} as any
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
describe('DiscordBotListener', () => {
|
|
111
|
+
let listener: DiscordBotListener
|
|
112
|
+
|
|
113
|
+
afterEach(() => {
|
|
114
|
+
listener?.stop()
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
describe('start', () => {
|
|
118
|
+
it('calls gatewayConnect and opens WebSocket', async () => {
|
|
119
|
+
const client = createMockClient()
|
|
120
|
+
listener = new DiscordBotListener(client)
|
|
121
|
+
|
|
122
|
+
await listener.start()
|
|
123
|
+
mockWsInstance.simulateOpen()
|
|
124
|
+
|
|
125
|
+
expect(client.gatewayConnect).toHaveBeenCalledTimes(1)
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it('is idempotent', async () => {
|
|
129
|
+
const client = createMockClient()
|
|
130
|
+
listener = new DiscordBotListener(client)
|
|
131
|
+
|
|
132
|
+
await listener.start()
|
|
133
|
+
await listener.start()
|
|
134
|
+
|
|
135
|
+
expect(client.gatewayConnect).toHaveBeenCalledTimes(1)
|
|
136
|
+
})
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
describe('connected event', () => {
|
|
140
|
+
it('emits connected with bot user/sessionId on READY', async () => {
|
|
141
|
+
const client = createMockClient()
|
|
142
|
+
listener = new DiscordBotListener(client)
|
|
143
|
+
|
|
144
|
+
const connected: any[] = []
|
|
145
|
+
listener.on('connected', (info) => connected.push(info))
|
|
146
|
+
|
|
147
|
+
await listener.start()
|
|
148
|
+
mockWsInstance.simulateOpen()
|
|
149
|
+
mockWsInstance.simulateHello()
|
|
150
|
+
mockWsInstance.simulateReady()
|
|
151
|
+
|
|
152
|
+
expect(connected.length).toBe(1)
|
|
153
|
+
expect(connected[0].user.id).toBe('BOT_SELF')
|
|
154
|
+
expect(connected[0].sessionId).toBe('session_123')
|
|
155
|
+
})
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
describe('identify', () => {
|
|
159
|
+
it('sends Identify with bot token after Hello', async () => {
|
|
160
|
+
const client = createMockClient()
|
|
161
|
+
listener = new DiscordBotListener(client)
|
|
162
|
+
|
|
163
|
+
await listener.start()
|
|
164
|
+
mockWsInstance.simulateOpen()
|
|
165
|
+
mockWsInstance.simulateHello()
|
|
166
|
+
|
|
167
|
+
const sentMessages = mockWsInstance.sent.map((s) => JSON.parse(s))
|
|
168
|
+
const identifyMsg = sentMessages.find((m) => m.op === 2)
|
|
169
|
+
|
|
170
|
+
expect(identifyMsg).toBeDefined()
|
|
171
|
+
expect(identifyMsg.d.token).toBe('fake-bot-token')
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
it('sends Identify with custom intents', async () => {
|
|
175
|
+
const client = createMockClient()
|
|
176
|
+
const customIntents = (1 << 9) | (1 << 15) // GuildMessages | MessageContent
|
|
177
|
+
listener = new DiscordBotListener(client, { intents: customIntents })
|
|
178
|
+
|
|
179
|
+
await listener.start()
|
|
180
|
+
mockWsInstance.simulateOpen()
|
|
181
|
+
mockWsInstance.simulateHello()
|
|
182
|
+
|
|
183
|
+
const sentMessages = mockWsInstance.sent.map((s) => JSON.parse(s))
|
|
184
|
+
const identifyMsg = sentMessages.find((m) => m.op === 2)
|
|
185
|
+
|
|
186
|
+
expect(identifyMsg).toBeDefined()
|
|
187
|
+
expect(identifyMsg.d.intents).toBe(customIntents)
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
it('uses sensible default intents when none specified', async () => {
|
|
191
|
+
const client = createMockClient()
|
|
192
|
+
listener = new DiscordBotListener(client)
|
|
193
|
+
|
|
194
|
+
await listener.start()
|
|
195
|
+
mockWsInstance.simulateOpen()
|
|
196
|
+
mockWsInstance.simulateHello()
|
|
197
|
+
|
|
198
|
+
const sentMessages = mockWsInstance.sent.map((s) => JSON.parse(s))
|
|
199
|
+
const identifyMsg = sentMessages.find((m) => m.op === 2)
|
|
200
|
+
|
|
201
|
+
// given: default intents enable Guilds + GuildMessages + reactions/typing + DMs
|
|
202
|
+
// then: the bitfield must include GuildMessages (1 << 9) at minimum
|
|
203
|
+
expect(identifyMsg.d.intents & (1 << 9)).toBeGreaterThan(0)
|
|
204
|
+
// and: must NOT include privileged MessageContent (1 << 15) by default
|
|
205
|
+
expect(identifyMsg.d.intents & (1 << 15)).toBe(0)
|
|
206
|
+
})
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
describe('message events', () => {
|
|
210
|
+
it('emits message_create events', async () => {
|
|
211
|
+
const client = createMockClient()
|
|
212
|
+
listener = new DiscordBotListener(client)
|
|
213
|
+
|
|
214
|
+
const messages: DiscordGatewayMessageCreateEvent[] = []
|
|
215
|
+
listener.on('message_create', (event) => messages.push(event))
|
|
216
|
+
|
|
217
|
+
await listener.start()
|
|
218
|
+
mockWsInstance.simulateOpen()
|
|
219
|
+
mockWsInstance.simulateHello()
|
|
220
|
+
mockWsInstance.simulateReady()
|
|
221
|
+
mockWsInstance.simulateDispatch(
|
|
222
|
+
'MESSAGE_CREATE',
|
|
223
|
+
{
|
|
224
|
+
id: 'msg_1',
|
|
225
|
+
channel_id: 'C123',
|
|
226
|
+
author: { id: 'U456', username: 'user' },
|
|
227
|
+
content: 'hello world',
|
|
228
|
+
timestamp: '2024-01-01T00:00:00Z',
|
|
229
|
+
},
|
|
230
|
+
2,
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
expect(messages.length).toBe(1)
|
|
234
|
+
expect(messages[0].content).toBe('hello world')
|
|
235
|
+
expect(messages[0].channel_id).toBe('C123')
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
it('emits message_update events', async () => {
|
|
239
|
+
const client = createMockClient()
|
|
240
|
+
listener = new DiscordBotListener(client)
|
|
241
|
+
|
|
242
|
+
const updates: DiscordGatewayMessageUpdateEvent[] = []
|
|
243
|
+
listener.on('message_update', (event) => updates.push(event))
|
|
244
|
+
|
|
245
|
+
await listener.start()
|
|
246
|
+
mockWsInstance.simulateOpen()
|
|
247
|
+
mockWsInstance.simulateHello()
|
|
248
|
+
mockWsInstance.simulateReady()
|
|
249
|
+
mockWsInstance.simulateDispatch(
|
|
250
|
+
'MESSAGE_UPDATE',
|
|
251
|
+
{ id: 'msg_1', channel_id: 'C123', content: 'edited', edited_timestamp: '2024-01-01T00:01:00Z' },
|
|
252
|
+
2,
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
expect(updates.length).toBe(1)
|
|
256
|
+
expect(updates[0].id).toBe('msg_1')
|
|
257
|
+
expect(updates[0].content).toBe('edited')
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
it('emits message_delete events', async () => {
|
|
261
|
+
const client = createMockClient()
|
|
262
|
+
listener = new DiscordBotListener(client)
|
|
263
|
+
|
|
264
|
+
const deletes: DiscordGatewayMessageDeleteEvent[] = []
|
|
265
|
+
listener.on('message_delete', (event) => deletes.push(event))
|
|
266
|
+
|
|
267
|
+
await listener.start()
|
|
268
|
+
mockWsInstance.simulateOpen()
|
|
269
|
+
mockWsInstance.simulateHello()
|
|
270
|
+
mockWsInstance.simulateReady()
|
|
271
|
+
mockWsInstance.simulateDispatch('MESSAGE_DELETE', { id: 'msg_1', channel_id: 'C123' }, 2)
|
|
272
|
+
|
|
273
|
+
expect(deletes.length).toBe(1)
|
|
274
|
+
expect(deletes[0].id).toBe('msg_1')
|
|
275
|
+
})
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
describe('reaction events', () => {
|
|
279
|
+
it('emits message_reaction_add events', async () => {
|
|
280
|
+
const client = createMockClient()
|
|
281
|
+
listener = new DiscordBotListener(client)
|
|
282
|
+
|
|
283
|
+
const reactions: DiscordGatewayReactionEvent[] = []
|
|
284
|
+
listener.on('message_reaction_add', (event) => reactions.push(event))
|
|
285
|
+
|
|
286
|
+
await listener.start()
|
|
287
|
+
mockWsInstance.simulateOpen()
|
|
288
|
+
mockWsInstance.simulateHello()
|
|
289
|
+
mockWsInstance.simulateReady()
|
|
290
|
+
mockWsInstance.simulateDispatch(
|
|
291
|
+
'MESSAGE_REACTION_ADD',
|
|
292
|
+
{
|
|
293
|
+
user_id: 'U789',
|
|
294
|
+
channel_id: 'C123',
|
|
295
|
+
message_id: 'msg_1',
|
|
296
|
+
emoji: { name: '👍' },
|
|
297
|
+
},
|
|
298
|
+
2,
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
expect(reactions.length).toBe(1)
|
|
302
|
+
expect(reactions[0].user_id).toBe('U789')
|
|
303
|
+
expect(reactions[0].emoji.name).toBe('👍')
|
|
304
|
+
})
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
describe('interaction events', () => {
|
|
308
|
+
it('emits interaction_create events for slash commands', async () => {
|
|
309
|
+
const client = createMockClient()
|
|
310
|
+
listener = new DiscordBotListener(client)
|
|
311
|
+
|
|
312
|
+
const interactions: any[] = []
|
|
313
|
+
listener.on('interaction_create', (event) => interactions.push(event))
|
|
314
|
+
|
|
315
|
+
await listener.start()
|
|
316
|
+
mockWsInstance.simulateOpen()
|
|
317
|
+
mockWsInstance.simulateHello()
|
|
318
|
+
mockWsInstance.simulateReady()
|
|
319
|
+
mockWsInstance.simulateDispatch(
|
|
320
|
+
'INTERACTION_CREATE',
|
|
321
|
+
{
|
|
322
|
+
id: 'int_1',
|
|
323
|
+
application_id: 'app_1',
|
|
324
|
+
token: 'interaction_token',
|
|
325
|
+
channel_id: 'C123',
|
|
326
|
+
data: { name: 'ping' },
|
|
327
|
+
},
|
|
328
|
+
2,
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
expect(interactions.length).toBe(1)
|
|
332
|
+
expect(interactions[0].id).toBe('int_1')
|
|
333
|
+
expect(interactions[0].data.name).toBe('ping')
|
|
334
|
+
})
|
|
335
|
+
})
|
|
336
|
+
|
|
337
|
+
describe('discord_event catch-all', () => {
|
|
338
|
+
it('emits discord_event for every dispatch (not READY)', async () => {
|
|
339
|
+
const client = createMockClient()
|
|
340
|
+
listener = new DiscordBotListener(client)
|
|
341
|
+
|
|
342
|
+
const events: DiscordGatewayGenericEvent[] = []
|
|
343
|
+
listener.on('discord_event', (event) => events.push(event))
|
|
344
|
+
|
|
345
|
+
await listener.start()
|
|
346
|
+
mockWsInstance.simulateOpen()
|
|
347
|
+
mockWsInstance.simulateHello()
|
|
348
|
+
mockWsInstance.simulateReady()
|
|
349
|
+
mockWsInstance.simulateDispatch(
|
|
350
|
+
'MESSAGE_CREATE',
|
|
351
|
+
{ id: 'm1', channel_id: 'C1', content: 'hi', timestamp: 't', author: { id: 'U1', username: 'u' } },
|
|
352
|
+
2,
|
|
353
|
+
)
|
|
354
|
+
mockWsInstance.simulateDispatch('TYPING_START', { user_id: 'U1', channel_id: 'C1', timestamp: 1 }, 3)
|
|
355
|
+
|
|
356
|
+
expect(events.length).toBe(2)
|
|
357
|
+
expect(events[0].type).toBe('MESSAGE_CREATE')
|
|
358
|
+
expect(events[1].type).toBe('TYPING_START')
|
|
359
|
+
})
|
|
360
|
+
})
|
|
361
|
+
|
|
362
|
+
describe('heartbeat', () => {
|
|
363
|
+
it('sends heartbeat after Hello (with jitter)', async () => {
|
|
364
|
+
const originalRandom = Math.random
|
|
365
|
+
Math.random = () => 0
|
|
366
|
+
|
|
367
|
+
try {
|
|
368
|
+
const client = createMockClient()
|
|
369
|
+
listener = new DiscordBotListener(client)
|
|
370
|
+
|
|
371
|
+
await listener.start()
|
|
372
|
+
mockWsInstance.simulateOpen()
|
|
373
|
+
mockWsInstance.simulateHello(50)
|
|
374
|
+
|
|
375
|
+
await new Promise((r) => setTimeout(r, 150))
|
|
376
|
+
|
|
377
|
+
const sentMessages = mockWsInstance.sent.map((s) => JSON.parse(s))
|
|
378
|
+
const heartbeats = sentMessages.filter((m) => m.op === 1)
|
|
379
|
+
|
|
380
|
+
expect(heartbeats.length).toBeGreaterThanOrEqual(1)
|
|
381
|
+
} finally {
|
|
382
|
+
Math.random = originalRandom
|
|
383
|
+
}
|
|
384
|
+
})
|
|
385
|
+
|
|
386
|
+
it('heartbeat ACK not emitted as user event', async () => {
|
|
387
|
+
const client = createMockClient()
|
|
388
|
+
listener = new DiscordBotListener(client)
|
|
389
|
+
|
|
390
|
+
const events: DiscordGatewayGenericEvent[] = []
|
|
391
|
+
listener.on('discord_event', (event) => events.push(event))
|
|
392
|
+
|
|
393
|
+
await listener.start()
|
|
394
|
+
mockWsInstance.simulateOpen()
|
|
395
|
+
mockWsInstance.simulateHello()
|
|
396
|
+
mockWsInstance.simulateReady()
|
|
397
|
+
mockWsInstance.simulateHeartbeatACK()
|
|
398
|
+
|
|
399
|
+
expect(events.length).toBe(0)
|
|
400
|
+
})
|
|
401
|
+
|
|
402
|
+
it('zombie connection triggers reconnect (no ACK received)', async () => {
|
|
403
|
+
const originalRandom = Math.random
|
|
404
|
+
Math.random = () => 0
|
|
405
|
+
|
|
406
|
+
try {
|
|
407
|
+
const client = createMockClient()
|
|
408
|
+
listener = new DiscordBotListener(client)
|
|
409
|
+
|
|
410
|
+
const disconnected: boolean[] = []
|
|
411
|
+
listener.on('disconnected', () => disconnected.push(true))
|
|
412
|
+
|
|
413
|
+
await listener.start()
|
|
414
|
+
mockWsInstance.simulateOpen()
|
|
415
|
+
mockWsInstance.simulateHello(50)
|
|
416
|
+
|
|
417
|
+
await new Promise((r) => setTimeout(r, 300))
|
|
418
|
+
|
|
419
|
+
expect(disconnected.length).toBeGreaterThanOrEqual(1)
|
|
420
|
+
} finally {
|
|
421
|
+
Math.random = originalRandom
|
|
422
|
+
}
|
|
423
|
+
})
|
|
424
|
+
})
|
|
425
|
+
|
|
426
|
+
describe('stop', () => {
|
|
427
|
+
it('closes WebSocket and prevents reconnection', async () => {
|
|
428
|
+
const client = createMockClient()
|
|
429
|
+
listener = new DiscordBotListener(client)
|
|
430
|
+
|
|
431
|
+
await listener.start()
|
|
432
|
+
mockWsInstance.simulateOpen()
|
|
433
|
+
|
|
434
|
+
listener.stop()
|
|
435
|
+
|
|
436
|
+
await new Promise((r) => setTimeout(r, 50))
|
|
437
|
+
expect(client.gatewayConnect).toHaveBeenCalledTimes(1)
|
|
438
|
+
})
|
|
439
|
+
})
|
|
440
|
+
|
|
441
|
+
describe('reconnection', () => {
|
|
442
|
+
it('reconnects on WebSocket close when running', async () => {
|
|
443
|
+
const client = createMockClient()
|
|
444
|
+
listener = new DiscordBotListener(client)
|
|
445
|
+
|
|
446
|
+
const disconnected: boolean[] = []
|
|
447
|
+
listener.on('disconnected', () => disconnected.push(true))
|
|
448
|
+
|
|
449
|
+
await listener.start()
|
|
450
|
+
mockWsInstance.simulateOpen()
|
|
451
|
+
mockWsInstance.simulateClose()
|
|
452
|
+
|
|
453
|
+
expect(disconnected.length).toBe(1)
|
|
454
|
+
|
|
455
|
+
await new Promise((r) => setTimeout(r, 1500))
|
|
456
|
+
expect(client.gatewayConnect.mock.calls.length).toBeGreaterThanOrEqual(2)
|
|
457
|
+
})
|
|
458
|
+
|
|
459
|
+
it('emits error and reconnects on gatewayConnect failure', async () => {
|
|
460
|
+
let callCount = 0
|
|
461
|
+
const client = createMockClient({
|
|
462
|
+
gatewayConnect: mock(() => {
|
|
463
|
+
callCount++
|
|
464
|
+
if (callCount === 1) return Promise.reject(new Error('network_error'))
|
|
465
|
+
return Promise.resolve({ token: 'fake-bot-token' })
|
|
466
|
+
}),
|
|
467
|
+
})
|
|
468
|
+
|
|
469
|
+
listener = new DiscordBotListener(client)
|
|
470
|
+
|
|
471
|
+
const errors: Error[] = []
|
|
472
|
+
listener.on('error', (err) => errors.push(err))
|
|
473
|
+
|
|
474
|
+
await listener.start()
|
|
475
|
+
|
|
476
|
+
await new Promise((r) => setTimeout(r, 1500))
|
|
477
|
+
|
|
478
|
+
expect(errors.length).toBe(1)
|
|
479
|
+
expect(errors[0].message).toBe('network_error')
|
|
480
|
+
expect(client.gatewayConnect.mock.calls.length).toBeGreaterThanOrEqual(2)
|
|
481
|
+
})
|
|
482
|
+
})
|
|
483
|
+
|
|
484
|
+
describe('on/off/once', () => {
|
|
485
|
+
it('off removes listener', async () => {
|
|
486
|
+
const client = createMockClient()
|
|
487
|
+
listener = new DiscordBotListener(client)
|
|
488
|
+
|
|
489
|
+
const messages: DiscordGatewayMessageCreateEvent[] = []
|
|
490
|
+
const handler = (event: DiscordGatewayMessageCreateEvent) => messages.push(event)
|
|
491
|
+
listener.on('message_create', handler)
|
|
492
|
+
|
|
493
|
+
await listener.start()
|
|
494
|
+
mockWsInstance.simulateOpen()
|
|
495
|
+
mockWsInstance.simulateHello()
|
|
496
|
+
mockWsInstance.simulateReady()
|
|
497
|
+
mockWsInstance.simulateDispatch(
|
|
498
|
+
'MESSAGE_CREATE',
|
|
499
|
+
{ id: 'm1', channel_id: 'C1', content: 'a', timestamp: 't', author: { id: 'U1', username: 'u' } },
|
|
500
|
+
2,
|
|
501
|
+
)
|
|
502
|
+
|
|
503
|
+
listener.off('message_create', handler)
|
|
504
|
+
mockWsInstance.simulateDispatch(
|
|
505
|
+
'MESSAGE_CREATE',
|
|
506
|
+
{ id: 'm2', channel_id: 'C1', content: 'b', timestamp: 't', author: { id: 'U1', username: 'u' } },
|
|
507
|
+
3,
|
|
508
|
+
)
|
|
509
|
+
|
|
510
|
+
expect(messages.length).toBe(1)
|
|
511
|
+
expect(messages[0].content).toBe('a')
|
|
512
|
+
})
|
|
513
|
+
|
|
514
|
+
it('once fires only once', async () => {
|
|
515
|
+
const client = createMockClient()
|
|
516
|
+
listener = new DiscordBotListener(client)
|
|
517
|
+
|
|
518
|
+
const messages: DiscordGatewayMessageCreateEvent[] = []
|
|
519
|
+
listener.once('message_create', (event) => messages.push(event))
|
|
520
|
+
|
|
521
|
+
await listener.start()
|
|
522
|
+
mockWsInstance.simulateOpen()
|
|
523
|
+
mockWsInstance.simulateHello()
|
|
524
|
+
mockWsInstance.simulateReady()
|
|
525
|
+
mockWsInstance.simulateDispatch(
|
|
526
|
+
'MESSAGE_CREATE',
|
|
527
|
+
{ id: 'm1', channel_id: 'C1', content: 'first', timestamp: 't', author: { id: 'U1', username: 'u' } },
|
|
528
|
+
2,
|
|
529
|
+
)
|
|
530
|
+
mockWsInstance.simulateDispatch(
|
|
531
|
+
'MESSAGE_CREATE',
|
|
532
|
+
{ id: 'm2', channel_id: 'C1', content: 'second', timestamp: 't', author: { id: 'U1', username: 'u' } },
|
|
533
|
+
3,
|
|
534
|
+
)
|
|
535
|
+
|
|
536
|
+
expect(messages.length).toBe(1)
|
|
537
|
+
expect(messages[0].content).toBe('first')
|
|
538
|
+
})
|
|
539
|
+
})
|
|
540
|
+
|
|
541
|
+
describe('opcode 7 Reconnect', () => {
|
|
542
|
+
it('triggers immediate reconnect without backoff', async () => {
|
|
543
|
+
const client = createMockClient()
|
|
544
|
+
listener = new DiscordBotListener(client)
|
|
545
|
+
|
|
546
|
+
await listener.start()
|
|
547
|
+
mockWsInstance.simulateOpen()
|
|
548
|
+
mockWsInstance.simulateHello()
|
|
549
|
+
mockWsInstance.simulateReady()
|
|
550
|
+
|
|
551
|
+
;(listener as any).reconnectAttempts = 5
|
|
552
|
+
mockWsInstance.simulateReconnect()
|
|
553
|
+
|
|
554
|
+
expect((listener as any).reconnectAttempts).toBe(0)
|
|
555
|
+
})
|
|
556
|
+
})
|
|
557
|
+
|
|
558
|
+
describe('opcode 9 InvalidSession', () => {
|
|
559
|
+
it('d=false clears session state, reconnects with fresh identify', async () => {
|
|
560
|
+
const client = createMockClient()
|
|
561
|
+
listener = new DiscordBotListener(client)
|
|
562
|
+
|
|
563
|
+
await listener.start()
|
|
564
|
+
mockWsInstance.simulateOpen()
|
|
565
|
+
mockWsInstance.simulateHello()
|
|
566
|
+
mockWsInstance.simulateReady()
|
|
567
|
+
|
|
568
|
+
expect((listener as any).sessionId).toBe('session_123')
|
|
569
|
+
|
|
570
|
+
mockWsInstance.simulateInvalidSession(false)
|
|
571
|
+
|
|
572
|
+
expect((listener as any).sessionId).toBeNull()
|
|
573
|
+
expect((listener as any).sequence).toBeNull()
|
|
574
|
+
expect((listener as any).resumeGatewayUrl).toBeNull()
|
|
575
|
+
})
|
|
576
|
+
|
|
577
|
+
it('d=true allows resume on reconnect', async () => {
|
|
578
|
+
const client = createMockClient()
|
|
579
|
+
listener = new DiscordBotListener(client)
|
|
580
|
+
|
|
581
|
+
await listener.start()
|
|
582
|
+
mockWsInstance.simulateOpen()
|
|
583
|
+
mockWsInstance.simulateHello()
|
|
584
|
+
mockWsInstance.simulateReady()
|
|
585
|
+
|
|
586
|
+
const sessionIdBefore = (listener as any).sessionId
|
|
587
|
+
|
|
588
|
+
mockWsInstance.simulateInvalidSession(true)
|
|
589
|
+
|
|
590
|
+
expect((listener as any).sessionId).toBe(sessionIdBefore)
|
|
591
|
+
})
|
|
592
|
+
})
|
|
593
|
+
|
|
594
|
+
describe('resume', () => {
|
|
595
|
+
it('sends Resume instead of Identify when session exists', async () => {
|
|
596
|
+
const client = createMockClient()
|
|
597
|
+
listener = new DiscordBotListener(client)
|
|
598
|
+
|
|
599
|
+
await listener.start()
|
|
600
|
+
mockWsInstance.simulateOpen()
|
|
601
|
+
mockWsInstance.simulateHello()
|
|
602
|
+
mockWsInstance.simulateReady()
|
|
603
|
+
|
|
604
|
+
mockWsInstance.simulateClose()
|
|
605
|
+
|
|
606
|
+
await new Promise((r) => setTimeout(r, 1500))
|
|
607
|
+
|
|
608
|
+
mockWsInstance.simulateOpen()
|
|
609
|
+
mockWsInstance.simulateHello()
|
|
610
|
+
|
|
611
|
+
const sentMessages = mockWsInstance.sent.map((s) => JSON.parse(s))
|
|
612
|
+
const resumeMsg = sentMessages.find((m) => m.op === 6)
|
|
613
|
+
|
|
614
|
+
expect(resumeMsg).toBeDefined()
|
|
615
|
+
expect(resumeMsg.d.session_id).toBe('session_123')
|
|
616
|
+
})
|
|
617
|
+
})
|
|
618
|
+
|
|
619
|
+
describe('non-recoverable close codes', () => {
|
|
620
|
+
it('emits error and stops on code 4014 (privileged intent not approved)', async () => {
|
|
621
|
+
const client = createMockClient()
|
|
622
|
+
listener = new DiscordBotListener(client)
|
|
623
|
+
|
|
624
|
+
const errors: Error[] = []
|
|
625
|
+
listener.on('error', (err) => errors.push(err))
|
|
626
|
+
|
|
627
|
+
await listener.start()
|
|
628
|
+
mockWsInstance.simulateOpen()
|
|
629
|
+
mockWsInstance.simulateClose(4014)
|
|
630
|
+
|
|
631
|
+
await new Promise((r) => setTimeout(r, 50))
|
|
632
|
+
|
|
633
|
+
expect(errors.length).toBe(1)
|
|
634
|
+
expect(errors[0].message).toContain('4014')
|
|
635
|
+
expect(client.gatewayConnect).toHaveBeenCalledTimes(1)
|
|
636
|
+
})
|
|
637
|
+
|
|
638
|
+
it('emits error and stops on code 4004 (invalid token)', async () => {
|
|
639
|
+
const client = createMockClient()
|
|
640
|
+
listener = new DiscordBotListener(client)
|
|
641
|
+
|
|
642
|
+
const errors: Error[] = []
|
|
643
|
+
listener.on('error', (err) => errors.push(err))
|
|
644
|
+
|
|
645
|
+
await listener.start()
|
|
646
|
+
mockWsInstance.simulateOpen()
|
|
647
|
+
mockWsInstance.simulateClose(4004)
|
|
648
|
+
|
|
649
|
+
await new Promise((r) => setTimeout(r, 50))
|
|
650
|
+
|
|
651
|
+
expect(errors.length).toBe(1)
|
|
652
|
+
expect(errors[0].message).toContain('4004')
|
|
653
|
+
})
|
|
654
|
+
})
|
|
655
|
+
|
|
656
|
+
describe('session reset close codes', () => {
|
|
657
|
+
it('4007 clears session and reconnects with fresh identify', async () => {
|
|
658
|
+
const client = createMockClient()
|
|
659
|
+
listener = new DiscordBotListener(client)
|
|
660
|
+
|
|
661
|
+
await listener.start()
|
|
662
|
+
mockWsInstance.simulateOpen()
|
|
663
|
+
mockWsInstance.simulateHello()
|
|
664
|
+
mockWsInstance.simulateReady()
|
|
665
|
+
|
|
666
|
+
expect((listener as any).sessionId).toBe('session_123')
|
|
667
|
+
|
|
668
|
+
mockWsInstance.simulateClose(4007)
|
|
669
|
+
|
|
670
|
+
expect((listener as any).sessionId).toBeNull()
|
|
671
|
+
expect((listener as any).sequence).toBeNull()
|
|
672
|
+
expect((listener as any).resumeGatewayUrl).toBeNull()
|
|
673
|
+
|
|
674
|
+
await new Promise((r) => setTimeout(r, 1500))
|
|
675
|
+
|
|
676
|
+
mockWsInstance.simulateOpen()
|
|
677
|
+
mockWsInstance.simulateHello()
|
|
678
|
+
|
|
679
|
+
const sentMessages = mockWsInstance.sent.map((s) => JSON.parse(s))
|
|
680
|
+
const identifyMsg = sentMessages.find((m) => m.op === 2)
|
|
681
|
+
const resumeMsg = sentMessages.find((m) => m.op === 6)
|
|
682
|
+
|
|
683
|
+
expect(identifyMsg).toBeDefined()
|
|
684
|
+
expect(resumeMsg).toBeUndefined()
|
|
685
|
+
})
|
|
686
|
+
|
|
687
|
+
it('4009 clears session', async () => {
|
|
688
|
+
const client = createMockClient()
|
|
689
|
+
listener = new DiscordBotListener(client)
|
|
690
|
+
|
|
691
|
+
await listener.start()
|
|
692
|
+
mockWsInstance.simulateOpen()
|
|
693
|
+
mockWsInstance.simulateHello()
|
|
694
|
+
mockWsInstance.simulateReady()
|
|
695
|
+
|
|
696
|
+
mockWsInstance.simulateClose(4009)
|
|
697
|
+
|
|
698
|
+
expect((listener as any).sessionId).toBeNull()
|
|
699
|
+
})
|
|
700
|
+
})
|
|
701
|
+
|
|
702
|
+
describe('duplicate Hello', () => {
|
|
703
|
+
it('does not stack heartbeat timers on second Hello', async () => {
|
|
704
|
+
const originalRandom = Math.random
|
|
705
|
+
Math.random = () => 0
|
|
706
|
+
|
|
707
|
+
try {
|
|
708
|
+
const client = createMockClient()
|
|
709
|
+
listener = new DiscordBotListener(client)
|
|
710
|
+
|
|
711
|
+
await listener.start()
|
|
712
|
+
mockWsInstance.simulateOpen()
|
|
713
|
+
mockWsInstance.simulateHello(50)
|
|
714
|
+
mockWsInstance.simulateReady()
|
|
715
|
+
|
|
716
|
+
mockWsInstance.simulateHello(50)
|
|
717
|
+
|
|
718
|
+
await new Promise((r) => setTimeout(r, 200))
|
|
719
|
+
|
|
720
|
+
const sentMessages = mockWsInstance.sent.map((s) => JSON.parse(s))
|
|
721
|
+
const heartbeats = sentMessages.filter((m) => m.op === 1)
|
|
722
|
+
|
|
723
|
+
expect(heartbeats.length).toBeLessThanOrEqual(8)
|
|
724
|
+
} finally {
|
|
725
|
+
Math.random = originalRandom
|
|
726
|
+
}
|
|
727
|
+
})
|
|
728
|
+
})
|
|
729
|
+
|
|
730
|
+
describe('InvalidSession timer safety', () => {
|
|
731
|
+
it('stop cancels pending InvalidSession d=true timeout', async () => {
|
|
732
|
+
const originalRandom = Math.random
|
|
733
|
+
Math.random = () => 0
|
|
734
|
+
|
|
735
|
+
try {
|
|
736
|
+
const client = createMockClient()
|
|
737
|
+
listener = new DiscordBotListener(client)
|
|
738
|
+
|
|
739
|
+
await listener.start()
|
|
740
|
+
mockWsInstance.simulateOpen()
|
|
741
|
+
mockWsInstance.simulateHello()
|
|
742
|
+
mockWsInstance.simulateReady()
|
|
743
|
+
|
|
744
|
+
mockWsInstance.simulateInvalidSession(true)
|
|
745
|
+
|
|
746
|
+
listener.stop()
|
|
747
|
+
|
|
748
|
+
expect((listener as any).invalidSessionTimer).toBeNull()
|
|
749
|
+
} finally {
|
|
750
|
+
Math.random = originalRandom
|
|
751
|
+
}
|
|
752
|
+
})
|
|
753
|
+
})
|
|
754
|
+
|
|
755
|
+
describe('server-requested heartbeat', () => {
|
|
756
|
+
it('responds to opcode 1 with heartbeat', async () => {
|
|
757
|
+
const client = createMockClient()
|
|
758
|
+
listener = new DiscordBotListener(client)
|
|
759
|
+
|
|
760
|
+
await listener.start()
|
|
761
|
+
mockWsInstance.simulateOpen()
|
|
762
|
+
mockWsInstance.simulateHello()
|
|
763
|
+
mockWsInstance.simulateReady()
|
|
764
|
+
|
|
765
|
+
const sentBefore = mockWsInstance.sent.length
|
|
766
|
+
mockWsInstance.simulateMessage({ op: 1 })
|
|
767
|
+
|
|
768
|
+
const sentAfter = mockWsInstance.sent.slice(sentBefore)
|
|
769
|
+
const heartbeats = sentAfter.map((s) => JSON.parse(s)).filter((m) => m.op === 1)
|
|
770
|
+
|
|
771
|
+
expect(heartbeats.length).toBe(1)
|
|
772
|
+
})
|
|
773
|
+
})
|
|
774
|
+
|
|
775
|
+
describe('sequence tracking', () => {
|
|
776
|
+
it('tracks sequence from dispatch events', async () => {
|
|
777
|
+
const client = createMockClient()
|
|
778
|
+
listener = new DiscordBotListener(client)
|
|
779
|
+
|
|
780
|
+
await listener.start()
|
|
781
|
+
mockWsInstance.simulateOpen()
|
|
782
|
+
mockWsInstance.simulateHello()
|
|
783
|
+
mockWsInstance.simulateReady()
|
|
784
|
+
|
|
785
|
+
mockWsInstance.simulateDispatch(
|
|
786
|
+
'MESSAGE_CREATE',
|
|
787
|
+
{ id: 'm1', channel_id: 'C1', content: 'hi', timestamp: 't', author: { id: 'U1', username: 'u' } },
|
|
788
|
+
5,
|
|
789
|
+
)
|
|
790
|
+
|
|
791
|
+
expect((listener as any).sequence).toBe(5)
|
|
792
|
+
})
|
|
793
|
+
|
|
794
|
+
it('ignores null sequence values', async () => {
|
|
795
|
+
const client = createMockClient()
|
|
796
|
+
listener = new DiscordBotListener(client)
|
|
797
|
+
|
|
798
|
+
await listener.start()
|
|
799
|
+
mockWsInstance.simulateOpen()
|
|
800
|
+
mockWsInstance.simulateHello()
|
|
801
|
+
mockWsInstance.simulateReady()
|
|
802
|
+
|
|
803
|
+
mockWsInstance.simulateDispatch(
|
|
804
|
+
'MESSAGE_CREATE',
|
|
805
|
+
{ id: 'm1', channel_id: 'C1', content: 'hi', timestamp: 't', author: { id: 'U1', username: 'u' } },
|
|
806
|
+
5,
|
|
807
|
+
)
|
|
808
|
+
|
|
809
|
+
mockWsInstance.simulateMessage({
|
|
810
|
+
op: 0,
|
|
811
|
+
t: 'TYPING_START',
|
|
812
|
+
s: null,
|
|
813
|
+
d: { user_id: 'U1', channel_id: 'C1', timestamp: 1 },
|
|
814
|
+
})
|
|
815
|
+
|
|
816
|
+
expect((listener as any).sequence).toBe(5)
|
|
817
|
+
})
|
|
818
|
+
})
|
|
819
|
+
|
|
820
|
+
describe('start after stop', () => {
|
|
821
|
+
it('resets reconnect attempts on fresh start', async () => {
|
|
822
|
+
const client = createMockClient()
|
|
823
|
+
listener = new DiscordBotListener(client)
|
|
824
|
+
|
|
825
|
+
await listener.start()
|
|
826
|
+
;(listener as any).reconnectAttempts = 5
|
|
827
|
+
listener.stop()
|
|
828
|
+
|
|
829
|
+
await listener.start()
|
|
830
|
+
expect((listener as any).reconnectAttempts).toBe(0)
|
|
831
|
+
})
|
|
832
|
+
})
|
|
833
|
+
|
|
834
|
+
describe('reconnect URL', () => {
|
|
835
|
+
it('appends ?v=10&encoding=json to resume_gateway_url on reconnect', async () => {
|
|
836
|
+
const client = createMockClient()
|
|
837
|
+
listener = new DiscordBotListener(client)
|
|
838
|
+
|
|
839
|
+
await listener.start()
|
|
840
|
+
mockWsInstance.simulateOpen()
|
|
841
|
+
mockWsInstance.simulateHello()
|
|
842
|
+
mockWsInstance.simulateMessage({
|
|
843
|
+
op: 0,
|
|
844
|
+
t: 'READY',
|
|
845
|
+
s: 1,
|
|
846
|
+
d: {
|
|
847
|
+
session_id: 'session_xyz',
|
|
848
|
+
resume_gateway_url: 'wss://gateway-us-east1-b.discord.gg',
|
|
849
|
+
user: { id: 'BOT', username: 'bot' },
|
|
850
|
+
},
|
|
851
|
+
})
|
|
852
|
+
|
|
853
|
+
mockWsInstance.simulateClose()
|
|
854
|
+
await new Promise((r) => setTimeout(r, 1500))
|
|
855
|
+
|
|
856
|
+
expect(MockWs.lastUrl).toBe('wss://gateway-us-east1-b.discord.gg?v=10&encoding=json')
|
|
857
|
+
})
|
|
858
|
+
|
|
859
|
+
it('uses default gateway URL on initial connect when no resume URL is set', async () => {
|
|
860
|
+
const client = createMockClient()
|
|
861
|
+
listener = new DiscordBotListener(client)
|
|
862
|
+
|
|
863
|
+
await listener.start()
|
|
864
|
+
|
|
865
|
+
expect(MockWs.lastUrl).toBe('wss://gateway.discord.gg/?v=10&encoding=json')
|
|
866
|
+
})
|
|
867
|
+
})
|
|
868
|
+
|
|
869
|
+
describe('reconnectAttempts deferred to READY/RESUMED', () => {
|
|
870
|
+
it('does not reset reconnectAttempts on socket open alone', async () => {
|
|
871
|
+
const client = createMockClient()
|
|
872
|
+
listener = new DiscordBotListener(client)
|
|
873
|
+
|
|
874
|
+
await listener.start()
|
|
875
|
+
;(listener as any).reconnectAttempts = 5
|
|
876
|
+
|
|
877
|
+
mockWsInstance.simulateOpen()
|
|
878
|
+
|
|
879
|
+
// given: a socket opens but no READY received yet
|
|
880
|
+
// then: reconnectAttempts must NOT be reset (open alone is not a successful session)
|
|
881
|
+
expect((listener as any).reconnectAttempts).toBe(5)
|
|
882
|
+
})
|
|
883
|
+
|
|
884
|
+
it('resets reconnectAttempts on READY dispatch', async () => {
|
|
885
|
+
const client = createMockClient()
|
|
886
|
+
listener = new DiscordBotListener(client)
|
|
887
|
+
|
|
888
|
+
await listener.start()
|
|
889
|
+
;(listener as any).reconnectAttempts = 5
|
|
890
|
+
|
|
891
|
+
mockWsInstance.simulateOpen()
|
|
892
|
+
mockWsInstance.simulateHello()
|
|
893
|
+
mockWsInstance.simulateReady()
|
|
894
|
+
|
|
895
|
+
expect((listener as any).reconnectAttempts).toBe(0)
|
|
896
|
+
})
|
|
897
|
+
|
|
898
|
+
it('resets reconnectAttempts on RESUMED dispatch', async () => {
|
|
899
|
+
const client = createMockClient()
|
|
900
|
+
listener = new DiscordBotListener(client)
|
|
901
|
+
|
|
902
|
+
await listener.start()
|
|
903
|
+
mockWsInstance.simulateOpen()
|
|
904
|
+
mockWsInstance.simulateHello()
|
|
905
|
+
mockWsInstance.simulateReady()
|
|
906
|
+
|
|
907
|
+
mockWsInstance.simulateClose()
|
|
908
|
+
await new Promise((r) => setTimeout(r, 1500))
|
|
909
|
+
|
|
910
|
+
mockWsInstance.simulateOpen()
|
|
911
|
+
mockWsInstance.simulateHello()
|
|
912
|
+
;(listener as any).reconnectAttempts = 5
|
|
913
|
+
|
|
914
|
+
mockWsInstance.simulateMessage({ op: 0, t: 'RESUMED', s: 2, d: {} })
|
|
915
|
+
|
|
916
|
+
expect((listener as any).reconnectAttempts).toBe(0)
|
|
917
|
+
})
|
|
918
|
+
})
|
|
919
|
+
|
|
920
|
+
describe('RESUMED dispatch', () => {
|
|
921
|
+
it('emits connected with cached user/session on RESUMED', async () => {
|
|
922
|
+
const client = createMockClient()
|
|
923
|
+
listener = new DiscordBotListener(client)
|
|
924
|
+
|
|
925
|
+
const connectedEvents: any[] = []
|
|
926
|
+
listener.on('connected', (info) => connectedEvents.push(info))
|
|
927
|
+
|
|
928
|
+
await listener.start()
|
|
929
|
+
mockWsInstance.simulateOpen()
|
|
930
|
+
mockWsInstance.simulateHello()
|
|
931
|
+
mockWsInstance.simulateReady()
|
|
932
|
+
|
|
933
|
+
expect(connectedEvents.length).toBe(1)
|
|
934
|
+
|
|
935
|
+
mockWsInstance.simulateClose()
|
|
936
|
+
await new Promise((r) => setTimeout(r, 1500))
|
|
937
|
+
|
|
938
|
+
mockWsInstance.simulateOpen()
|
|
939
|
+
mockWsInstance.simulateHello()
|
|
940
|
+
mockWsInstance.simulateMessage({ op: 0, t: 'RESUMED', s: 2, d: {} })
|
|
941
|
+
|
|
942
|
+
expect(connectedEvents.length).toBe(2)
|
|
943
|
+
expect(connectedEvents[1].user.id).toBe('BOT_SELF')
|
|
944
|
+
expect(connectedEvents[1].sessionId).toBe('session_123')
|
|
945
|
+
})
|
|
946
|
+
})
|
|
947
|
+
|
|
948
|
+
describe('generation guard prevents stale-socket interference', () => {
|
|
949
|
+
it('stale socket close after stop+start does not clear new socket timers', async () => {
|
|
950
|
+
const client = createMockClient()
|
|
951
|
+
listener = new DiscordBotListener(client)
|
|
952
|
+
|
|
953
|
+
await listener.start()
|
|
954
|
+
mockWsInstance.simulateOpen()
|
|
955
|
+
mockWsInstance.simulateHello()
|
|
956
|
+
mockWsInstance.simulateReady()
|
|
957
|
+
|
|
958
|
+
const oldWs = mockWsInstance
|
|
959
|
+
|
|
960
|
+
listener.stop()
|
|
961
|
+
await listener.start()
|
|
962
|
+
|
|
963
|
+
// given: a fresh socket from the second start()
|
|
964
|
+
// when: the OLD socket fires a stale close event
|
|
965
|
+
oldWs.emit('close', 1000)
|
|
966
|
+
|
|
967
|
+
// then: the new socket's state should be intact (running, generation incremented)
|
|
968
|
+
expect((listener as any).running).toBe(true)
|
|
969
|
+
expect((listener as any).generation).toBeGreaterThanOrEqual(2)
|
|
970
|
+
})
|
|
971
|
+
|
|
972
|
+
it('InvalidSession d=true timer no-ops if generation changed before firing', async () => {
|
|
973
|
+
const originalRandom = Math.random
|
|
974
|
+
Math.random = () => 0
|
|
975
|
+
|
|
976
|
+
try {
|
|
977
|
+
const client = createMockClient()
|
|
978
|
+
listener = new DiscordBotListener(client)
|
|
979
|
+
|
|
980
|
+
await listener.start()
|
|
981
|
+
mockWsInstance.simulateOpen()
|
|
982
|
+
mockWsInstance.simulateHello()
|
|
983
|
+
mockWsInstance.simulateReady()
|
|
984
|
+
|
|
985
|
+
const initialGeneration = (listener as any).generation
|
|
986
|
+
|
|
987
|
+
mockWsInstance.simulateInvalidSession(true)
|
|
988
|
+
|
|
989
|
+
// when: stop()+start() before the d=true delay fires
|
|
990
|
+
listener.stop()
|
|
991
|
+
await listener.start()
|
|
992
|
+
|
|
993
|
+
await new Promise((r) => setTimeout(r, 1500))
|
|
994
|
+
|
|
995
|
+
// then: generation moved forward so the stale timer's close call is suppressed
|
|
996
|
+
expect((listener as any).generation).toBeGreaterThan(initialGeneration)
|
|
997
|
+
} finally {
|
|
998
|
+
Math.random = originalRandom
|
|
999
|
+
}
|
|
1000
|
+
})
|
|
1001
|
+
})
|
|
1002
|
+
})
|