agent-messenger 2.10.2 → 2.11.1

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.
Files changed (330) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.env.template +4 -1
  3. package/README.md +77 -27
  4. package/bun.lock +26 -0
  5. package/dist/package.json +21 -1
  6. package/dist/src/platforms/channeltalk/commands/auth.d.ts +2 -1
  7. package/dist/src/platforms/channeltalk/commands/auth.d.ts.map +1 -1
  8. package/dist/src/platforms/channeltalk/commands/auth.js +5 -3
  9. package/dist/src/platforms/channeltalk/commands/auth.js.map +1 -1
  10. package/dist/src/platforms/channeltalk/token-extractor.d.ts +2 -1
  11. package/dist/src/platforms/channeltalk/token-extractor.d.ts.map +1 -1
  12. package/dist/src/platforms/channeltalk/token-extractor.js +22 -6
  13. package/dist/src/platforms/channeltalk/token-extractor.js.map +1 -1
  14. package/dist/src/platforms/channeltalkbot/cli.d.ts.map +1 -1
  15. package/dist/src/platforms/channeltalkbot/cli.js +11 -1
  16. package/dist/src/platforms/channeltalkbot/cli.js.map +1 -1
  17. package/dist/src/platforms/channeltalkbot/commands/auth.d.ts.map +1 -1
  18. package/dist/src/platforms/channeltalkbot/commands/auth.js +1 -5
  19. package/dist/src/platforms/channeltalkbot/commands/auth.js.map +1 -1
  20. package/dist/src/platforms/channeltalkbot/commands/bot.d.ts.map +1 -1
  21. package/dist/src/platforms/channeltalkbot/commands/bot.js +1 -6
  22. package/dist/src/platforms/channeltalkbot/commands/bot.js.map +1 -1
  23. package/dist/src/platforms/channeltalkbot/commands/chat.d.ts.map +1 -1
  24. package/dist/src/platforms/channeltalkbot/commands/chat.js +1 -6
  25. package/dist/src/platforms/channeltalkbot/commands/chat.js.map +1 -1
  26. package/dist/src/platforms/channeltalkbot/commands/group.d.ts.map +1 -1
  27. package/dist/src/platforms/channeltalkbot/commands/group.js +1 -6
  28. package/dist/src/platforms/channeltalkbot/commands/group.js.map +1 -1
  29. package/dist/src/platforms/channeltalkbot/commands/manager.d.ts.map +1 -1
  30. package/dist/src/platforms/channeltalkbot/commands/manager.js +1 -6
  31. package/dist/src/platforms/channeltalkbot/commands/manager.js.map +1 -1
  32. package/dist/src/platforms/channeltalkbot/commands/message.d.ts.map +1 -1
  33. package/dist/src/platforms/channeltalkbot/commands/message.js +1 -6
  34. package/dist/src/platforms/channeltalkbot/commands/message.js.map +1 -1
  35. package/dist/src/platforms/channeltalkbot/commands/whoami.d.ts.map +1 -1
  36. package/dist/src/platforms/channeltalkbot/commands/whoami.js +1 -6
  37. package/dist/src/platforms/channeltalkbot/commands/whoami.js.map +1 -1
  38. package/dist/src/platforms/channeltalkbot/credential-manager.d.ts +5 -0
  39. package/dist/src/platforms/channeltalkbot/credential-manager.d.ts.map +1 -1
  40. package/dist/src/platforms/channeltalkbot/credential-manager.js +34 -4
  41. package/dist/src/platforms/channeltalkbot/credential-manager.js.map +1 -1
  42. package/dist/src/platforms/discord/commands/auth.d.ts +1 -0
  43. package/dist/src/platforms/discord/commands/auth.d.ts.map +1 -1
  44. package/dist/src/platforms/discord/commands/auth.js +3 -1
  45. package/dist/src/platforms/discord/commands/auth.js.map +1 -1
  46. package/dist/src/platforms/discord/listener.d.ts +2 -0
  47. package/dist/src/platforms/discord/listener.d.ts.map +1 -1
  48. package/dist/src/platforms/discord/listener.js +51 -21
  49. package/dist/src/platforms/discord/listener.js.map +1 -1
  50. package/dist/src/platforms/discord/token-extractor.d.ts +2 -1
  51. package/dist/src/platforms/discord/token-extractor.d.ts.map +1 -1
  52. package/dist/src/platforms/discord/token-extractor.js +21 -6
  53. package/dist/src/platforms/discord/token-extractor.js.map +1 -1
  54. package/dist/src/platforms/discordbot/cli.d.ts.map +1 -1
  55. package/dist/src/platforms/discordbot/cli.js +12 -1
  56. package/dist/src/platforms/discordbot/cli.js.map +1 -1
  57. package/dist/src/platforms/discordbot/client.d.ts +3 -0
  58. package/dist/src/platforms/discordbot/client.d.ts.map +1 -1
  59. package/dist/src/platforms/discordbot/client.js +3 -0
  60. package/dist/src/platforms/discordbot/client.js.map +1 -1
  61. package/dist/src/platforms/discordbot/commands/auth.d.ts.map +1 -1
  62. package/dist/src/platforms/discordbot/commands/auth.js +1 -5
  63. package/dist/src/platforms/discordbot/commands/auth.js.map +1 -1
  64. package/dist/src/platforms/discordbot/commands/message.d.ts.map +1 -1
  65. package/dist/src/platforms/discordbot/commands/message.js +1 -6
  66. package/dist/src/platforms/discordbot/commands/message.js.map +1 -1
  67. package/dist/src/platforms/discordbot/commands/server.d.ts.map +1 -1
  68. package/dist/src/platforms/discordbot/commands/server.js +1 -4
  69. package/dist/src/platforms/discordbot/commands/server.js.map +1 -1
  70. package/dist/src/platforms/discordbot/commands/whoami.d.ts.map +1 -1
  71. package/dist/src/platforms/discordbot/commands/whoami.js +1 -6
  72. package/dist/src/platforms/discordbot/commands/whoami.js.map +1 -1
  73. package/dist/src/platforms/discordbot/index.d.ts +3 -1
  74. package/dist/src/platforms/discordbot/index.d.ts.map +1 -1
  75. package/dist/src/platforms/discordbot/index.js +2 -1
  76. package/dist/src/platforms/discordbot/index.js.map +1 -1
  77. package/dist/src/platforms/discordbot/listener.d.ts +43 -0
  78. package/dist/src/platforms/discordbot/listener.d.ts.map +1 -0
  79. package/dist/src/platforms/discordbot/listener.js +292 -0
  80. package/dist/src/platforms/discordbot/listener.js.map +1 -0
  81. package/dist/src/platforms/discordbot/types.d.ts +161 -0
  82. package/dist/src/platforms/discordbot/types.d.ts.map +1 -1
  83. package/dist/src/platforms/discordbot/types.js +34 -0
  84. package/dist/src/platforms/discordbot/types.js.map +1 -1
  85. package/dist/src/platforms/instagram/commands/auth.d.ts.map +1 -1
  86. package/dist/src/platforms/instagram/commands/auth.js +3 -1
  87. package/dist/src/platforms/instagram/commands/auth.js.map +1 -1
  88. package/dist/src/platforms/instagram/token-extractor.d.ts +2 -1
  89. package/dist/src/platforms/instagram/token-extractor.d.ts.map +1 -1
  90. package/dist/src/platforms/instagram/token-extractor.js +11 -2
  91. package/dist/src/platforms/instagram/token-extractor.js.map +1 -1
  92. package/dist/src/platforms/slack/commands/auth.d.ts.map +1 -1
  93. package/dist/src/platforms/slack/commands/auth.js +4 -2
  94. package/dist/src/platforms/slack/commands/auth.js.map +1 -1
  95. package/dist/src/platforms/slack/token-extractor.d.ts +4 -1
  96. package/dist/src/platforms/slack/token-extractor.d.ts.map +1 -1
  97. package/dist/src/platforms/slack/token-extractor.js +64 -15
  98. package/dist/src/platforms/slack/token-extractor.js.map +1 -1
  99. package/dist/src/platforms/slackbot/cli.d.ts.map +1 -1
  100. package/dist/src/platforms/slackbot/cli.js +15 -3
  101. package/dist/src/platforms/slackbot/cli.js.map +1 -1
  102. package/dist/src/platforms/slackbot/client.d.ts +22 -1
  103. package/dist/src/platforms/slackbot/client.d.ts.map +1 -1
  104. package/dist/src/platforms/slackbot/client.js +104 -1
  105. package/dist/src/platforms/slackbot/client.js.map +1 -1
  106. package/dist/src/platforms/slackbot/commands/auth.d.ts.map +1 -1
  107. package/dist/src/platforms/slackbot/commands/auth.js +1 -5
  108. package/dist/src/platforms/slackbot/commands/auth.js.map +1 -1
  109. package/dist/src/platforms/slackbot/commands/file.d.ts +3 -0
  110. package/dist/src/platforms/slackbot/commands/file.d.ts.map +1 -0
  111. package/dist/src/platforms/slackbot/commands/file.js +164 -0
  112. package/dist/src/platforms/slackbot/commands/file.js.map +1 -0
  113. package/dist/src/platforms/slackbot/commands/index.d.ts +1 -0
  114. package/dist/src/platforms/slackbot/commands/index.d.ts.map +1 -1
  115. package/dist/src/platforms/slackbot/commands/index.js +1 -0
  116. package/dist/src/platforms/slackbot/commands/index.js.map +1 -1
  117. package/dist/src/platforms/slackbot/commands/message.d.ts.map +1 -1
  118. package/dist/src/platforms/slackbot/commands/message.js +19 -0
  119. package/dist/src/platforms/slackbot/commands/message.js.map +1 -1
  120. package/dist/src/platforms/slackbot/commands/whoami.d.ts.map +1 -1
  121. package/dist/src/platforms/slackbot/commands/whoami.js +1 -6
  122. package/dist/src/platforms/slackbot/commands/whoami.js.map +1 -1
  123. package/dist/src/platforms/slackbot/credential-manager.d.ts +1 -0
  124. package/dist/src/platforms/slackbot/credential-manager.d.ts.map +1 -1
  125. package/dist/src/platforms/slackbot/credential-manager.js +30 -2
  126. package/dist/src/platforms/slackbot/credential-manager.js.map +1 -1
  127. package/dist/src/platforms/slackbot/index.d.ts +4 -1
  128. package/dist/src/platforms/slackbot/index.d.ts.map +1 -1
  129. package/dist/src/platforms/slackbot/index.js +1 -0
  130. package/dist/src/platforms/slackbot/index.js.map +1 -1
  131. package/dist/src/platforms/slackbot/listener.d.ts +44 -0
  132. package/dist/src/platforms/slackbot/listener.d.ts.map +1 -0
  133. package/dist/src/platforms/slackbot/listener.js +313 -0
  134. package/dist/src/platforms/slackbot/listener.js.map +1 -0
  135. package/dist/src/platforms/slackbot/types.d.ts +196 -1
  136. package/dist/src/platforms/slackbot/types.d.ts.map +1 -1
  137. package/dist/src/platforms/slackbot/types.js +4 -1
  138. package/dist/src/platforms/slackbot/types.js.map +1 -1
  139. package/dist/src/platforms/teams/commands/auth.d.ts +1 -0
  140. package/dist/src/platforms/teams/commands/auth.d.ts.map +1 -1
  141. package/dist/src/platforms/teams/commands/auth.js +37 -6
  142. package/dist/src/platforms/teams/commands/auth.js.map +1 -1
  143. package/dist/src/platforms/teams/ensure-auth.js +31 -9
  144. package/dist/src/platforms/teams/ensure-auth.js.map +1 -1
  145. package/dist/src/platforms/teams/token-extractor.d.ts +4 -1
  146. package/dist/src/platforms/teams/token-extractor.d.ts.map +1 -1
  147. package/dist/src/platforms/teams/token-extractor.js +71 -29
  148. package/dist/src/platforms/teams/token-extractor.js.map +1 -1
  149. package/dist/src/platforms/webex/commands/auth.d.ts +1 -0
  150. package/dist/src/platforms/webex/commands/auth.d.ts.map +1 -1
  151. package/dist/src/platforms/webex/commands/auth.js +3 -1
  152. package/dist/src/platforms/webex/commands/auth.js.map +1 -1
  153. package/dist/src/platforms/webex/token-extractor.d.ts +3 -1
  154. package/dist/src/platforms/webex/token-extractor.d.ts.map +1 -1
  155. package/dist/src/platforms/webex/token-extractor.js +16 -2
  156. package/dist/src/platforms/webex/token-extractor.js.map +1 -1
  157. package/dist/src/platforms/wechatbot/cli.d.ts.map +1 -1
  158. package/dist/src/platforms/wechatbot/cli.js +11 -1
  159. package/dist/src/platforms/wechatbot/cli.js.map +1 -1
  160. package/dist/src/platforms/wechatbot/commands/auth.d.ts.map +1 -1
  161. package/dist/src/platforms/wechatbot/commands/auth.js +1 -5
  162. package/dist/src/platforms/wechatbot/commands/auth.js.map +1 -1
  163. package/dist/src/platforms/wechatbot/commands/message.d.ts.map +1 -1
  164. package/dist/src/platforms/wechatbot/commands/message.js +1 -6
  165. package/dist/src/platforms/wechatbot/commands/message.js.map +1 -1
  166. package/dist/src/platforms/wechatbot/commands/template.d.ts.map +1 -1
  167. package/dist/src/platforms/wechatbot/commands/template.js +1 -6
  168. package/dist/src/platforms/wechatbot/commands/template.js.map +1 -1
  169. package/dist/src/platforms/wechatbot/commands/user.d.ts.map +1 -1
  170. package/dist/src/platforms/wechatbot/commands/user.js +1 -6
  171. package/dist/src/platforms/wechatbot/commands/user.js.map +1 -1
  172. package/dist/src/platforms/wechatbot/commands/whoami.d.ts.map +1 -1
  173. package/dist/src/platforms/wechatbot/commands/whoami.js +1 -6
  174. package/dist/src/platforms/wechatbot/commands/whoami.js.map +1 -1
  175. package/dist/src/platforms/whatsappbot/cli.d.ts.map +1 -1
  176. package/dist/src/platforms/whatsappbot/cli.js +11 -1
  177. package/dist/src/platforms/whatsappbot/cli.js.map +1 -1
  178. package/dist/src/platforms/whatsappbot/commands/auth.d.ts.map +1 -1
  179. package/dist/src/platforms/whatsappbot/commands/auth.js +1 -5
  180. package/dist/src/platforms/whatsappbot/commands/auth.js.map +1 -1
  181. package/dist/src/platforms/whatsappbot/commands/message.d.ts.map +1 -1
  182. package/dist/src/platforms/whatsappbot/commands/message.js +1 -6
  183. package/dist/src/platforms/whatsappbot/commands/message.js.map +1 -1
  184. package/dist/src/platforms/whatsappbot/commands/template.d.ts.map +1 -1
  185. package/dist/src/platforms/whatsappbot/commands/template.js +1 -6
  186. package/dist/src/platforms/whatsappbot/commands/template.js.map +1 -1
  187. package/dist/src/platforms/whatsappbot/commands/whoami.d.ts.map +1 -1
  188. package/dist/src/platforms/whatsappbot/commands/whoami.js +1 -6
  189. package/dist/src/platforms/whatsappbot/commands/whoami.js.map +1 -1
  190. package/dist/src/shared/chromium/browsers.d.ts +8 -0
  191. package/dist/src/shared/chromium/browsers.d.ts.map +1 -1
  192. package/dist/src/shared/chromium/browsers.js +58 -3
  193. package/dist/src/shared/chromium/browsers.js.map +1 -1
  194. package/dist/src/shared/chromium/cli-options.d.ts +5 -0
  195. package/dist/src/shared/chromium/cli-options.d.ts.map +1 -0
  196. package/dist/src/shared/chromium/cli-options.js +8 -0
  197. package/dist/src/shared/chromium/cli-options.js.map +1 -0
  198. package/dist/src/shared/chromium/index.d.ts +3 -1
  199. package/dist/src/shared/chromium/index.d.ts.map +1 -1
  200. package/dist/src/shared/chromium/index.js +2 -1
  201. package/dist/src/shared/chromium/index.js.map +1 -1
  202. package/dist/src/shared/utils/cli-output.d.ts +7 -0
  203. package/dist/src/shared/utils/cli-output.d.ts.map +1 -0
  204. package/dist/src/shared/utils/cli-output.js +7 -0
  205. package/dist/src/shared/utils/cli-output.js.map +1 -0
  206. package/dist/src/tui/app.d.ts.map +1 -1
  207. package/dist/src/tui/app.js +73 -20
  208. package/dist/src/tui/app.js.map +1 -1
  209. package/docs/content/docs/cli/channeltalk.mdx +4 -0
  210. package/docs/content/docs/cli/discord.mdx +5 -0
  211. package/docs/content/docs/cli/instagram.mdx +3 -0
  212. package/docs/content/docs/cli/slack.mdx +5 -0
  213. package/docs/content/docs/cli/slackbot.mdx +60 -22
  214. package/docs/content/docs/cli/teams.mdx +5 -0
  215. package/docs/content/docs/cli/webex.mdx +3 -0
  216. package/docs/content/docs/sdk/channeltalkbot.mdx +38 -1
  217. package/docs/content/docs/sdk/discordbot.mdx +501 -0
  218. package/docs/content/docs/sdk/meta.json +2 -0
  219. package/docs/content/docs/sdk/slackbot.mdx +576 -0
  220. package/e2e/README.md +1 -1
  221. package/e2e/config.ts +9 -4
  222. package/examples/discordbot-listen.ts +65 -0
  223. package/examples/slackbot-listen.ts +65 -0
  224. package/package.json +21 -1
  225. package/skills/agent-channeltalk/SKILL.md +5 -1
  226. package/skills/agent-channeltalk/references/authentication.md +5 -1
  227. package/skills/agent-channeltalkbot/SKILL.md +17 -3
  228. package/skills/agent-channeltalkbot/references/authentication.md +7 -5
  229. package/skills/agent-discord/SKILL.md +5 -1
  230. package/skills/agent-discord/references/authentication.md +7 -1
  231. package/skills/agent-discordbot/SKILL.md +13 -2
  232. package/skills/agent-discordbot/references/common-patterns.md +1 -1
  233. package/skills/agent-instagram/SKILL.md +7 -1
  234. package/skills/agent-instagram/references/authentication.md +6 -0
  235. package/skills/agent-kakaotalk/SKILL.md +1 -1
  236. package/skills/agent-line/SKILL.md +1 -1
  237. package/skills/agent-slack/SKILL.md +5 -1
  238. package/skills/agent-slack/references/authentication.md +7 -1
  239. package/skills/agent-slackbot/SKILL.md +56 -4
  240. package/skills/agent-slackbot/references/authentication.md +4 -0
  241. package/skills/agent-teams/SKILL.md +5 -1
  242. package/skills/agent-teams/references/authentication.md +7 -1
  243. package/skills/agent-telegram/SKILL.md +1 -1
  244. package/skills/agent-webex/SKILL.md +7 -1
  245. package/skills/agent-webex/references/authentication.md +6 -0
  246. package/skills/agent-wechatbot/SKILL.md +16 -1
  247. package/skills/agent-wechatbot/references/authentication.md +219 -0
  248. package/skills/agent-wechatbot/references/common-patterns.md +358 -0
  249. package/skills/agent-wechatbot/templates/account-summary.sh +122 -0
  250. package/skills/agent-wechatbot/templates/post-message.sh +122 -0
  251. package/skills/agent-wechatbot/templates/send-template.sh +152 -0
  252. package/skills/agent-whatsapp/SKILL.md +1 -1
  253. package/skills/agent-whatsappbot/SKILL.md +30 -1
  254. package/src/platforms/channeltalk/commands/auth.test.ts +15 -3
  255. package/src/platforms/channeltalk/commands/auth.ts +15 -5
  256. package/src/platforms/channeltalk/token-extractor.ts +24 -5
  257. package/src/platforms/channeltalkbot/cli.ts +9 -0
  258. package/src/platforms/channeltalkbot/commands/auth.ts +1 -5
  259. package/src/platforms/channeltalkbot/commands/bot.ts +1 -6
  260. package/src/platforms/channeltalkbot/commands/chat.ts +1 -6
  261. package/src/platforms/channeltalkbot/commands/group.ts +1 -6
  262. package/src/platforms/channeltalkbot/commands/manager.ts +1 -6
  263. package/src/platforms/channeltalkbot/commands/message.ts +1 -6
  264. package/src/platforms/channeltalkbot/commands/whoami.test.ts +2 -0
  265. package/src/platforms/channeltalkbot/commands/whoami.ts +1 -6
  266. package/src/platforms/channeltalkbot/credential-manager.test.ts +96 -2
  267. package/src/platforms/channeltalkbot/credential-manager.ts +37 -4
  268. package/src/platforms/discord/commands/auth.ts +13 -2
  269. package/src/platforms/discord/listener.test.ts +59 -1
  270. package/src/platforms/discord/listener.ts +43 -19
  271. package/src/platforms/discord/token-extractor.ts +30 -6
  272. package/src/platforms/discordbot/cli.ts +10 -0
  273. package/src/platforms/discordbot/client.ts +4 -0
  274. package/src/platforms/discordbot/commands/auth.ts +1 -5
  275. package/src/platforms/discordbot/commands/message.ts +1 -6
  276. package/src/platforms/discordbot/commands/server.ts +1 -5
  277. package/src/platforms/discordbot/commands/whoami.ts +1 -6
  278. package/src/platforms/discordbot/index.test.ts +82 -0
  279. package/src/platforms/discordbot/index.ts +27 -9
  280. package/src/platforms/discordbot/listener.test.ts +1002 -0
  281. package/src/platforms/discordbot/listener.ts +321 -0
  282. package/src/platforms/discordbot/types.ts +163 -0
  283. package/src/platforms/instagram/commands/auth.ts +9 -1
  284. package/src/platforms/instagram/token-extractor.ts +13 -1
  285. package/src/platforms/slack/commands/auth.ts +11 -2
  286. package/src/platforms/slack/token-extractor.test.ts +96 -0
  287. package/src/platforms/slack/token-extractor.ts +76 -13
  288. package/src/platforms/slackbot/cli.ts +13 -1
  289. package/src/platforms/slackbot/client.test.ts +274 -0
  290. package/src/platforms/slackbot/client.ts +130 -2
  291. package/src/platforms/slackbot/commands/auth.ts +1 -5
  292. package/src/platforms/slackbot/commands/file.test.ts +201 -0
  293. package/src/platforms/slackbot/commands/file.ts +212 -0
  294. package/src/platforms/slackbot/commands/index.ts +1 -0
  295. package/src/platforms/slackbot/commands/message.ts +22 -0
  296. package/src/platforms/slackbot/commands/whoami.ts +1 -6
  297. package/src/platforms/slackbot/credential-manager.test.ts +62 -2
  298. package/src/platforms/slackbot/credential-manager.ts +32 -2
  299. package/src/platforms/slackbot/index.test.ts +59 -0
  300. package/src/platforms/slackbot/index.ts +31 -7
  301. package/src/platforms/slackbot/listener.test.ts +1012 -0
  302. package/src/platforms/slackbot/listener.ts +362 -0
  303. package/src/platforms/slackbot/types.ts +224 -1
  304. package/src/platforms/teams/commands/auth.test.ts +1 -1
  305. package/src/platforms/teams/commands/auth.ts +66 -7
  306. package/src/platforms/teams/ensure-auth.test.ts +56 -5
  307. package/src/platforms/teams/ensure-auth.ts +39 -11
  308. package/src/platforms/teams/token-extractor.test.ts +146 -24
  309. package/src/platforms/teams/token-extractor.ts +87 -29
  310. package/src/platforms/webex/commands/auth.ts +13 -2
  311. package/src/platforms/webex/token-extractor.ts +25 -3
  312. package/src/platforms/wechatbot/cli.ts +9 -0
  313. package/src/platforms/wechatbot/commands/auth.ts +1 -5
  314. package/src/platforms/wechatbot/commands/message.ts +1 -6
  315. package/src/platforms/wechatbot/commands/template.ts +1 -6
  316. package/src/platforms/wechatbot/commands/user.ts +1 -6
  317. package/src/platforms/wechatbot/commands/whoami.ts +1 -6
  318. package/src/platforms/whatsappbot/cli.ts +9 -0
  319. package/src/platforms/whatsappbot/commands/auth.ts +1 -5
  320. package/src/platforms/whatsappbot/commands/message.ts +1 -6
  321. package/src/platforms/whatsappbot/commands/template.ts +1 -6
  322. package/src/platforms/whatsappbot/commands/whoami.ts +1 -6
  323. package/src/shared/chromium/browsers.test.ts +80 -0
  324. package/src/shared/chromium/browsers.ts +72 -3
  325. package/src/shared/chromium/cli-options.test.ts +22 -0
  326. package/src/shared/chromium/cli-options.ts +12 -0
  327. package/src/shared/chromium/index.ts +3 -0
  328. package/src/shared/utils/cli-output.test.ts +57 -0
  329. package/src/shared/utils/cli-output.ts +8 -0
  330. 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
+ })