onbuzz 4.9.13 → 4.10.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.
Files changed (451) hide show
  1. package/node_modules/glob/README.md +31 -5
  2. package/node_modules/glob/dist/commonjs/glob.d.ts +8 -0
  3. package/node_modules/glob/dist/commonjs/glob.d.ts.map +1 -1
  4. package/node_modules/glob/dist/commonjs/glob.js +2 -1
  5. package/node_modules/glob/dist/commonjs/glob.js.map +1 -1
  6. package/node_modules/glob/dist/commonjs/index.min.js +3 -3
  7. package/node_modules/glob/dist/commonjs/index.min.js.map +4 -4
  8. package/node_modules/glob/dist/commonjs/pattern.d.ts +3 -0
  9. package/node_modules/glob/dist/commonjs/pattern.d.ts.map +1 -1
  10. package/node_modules/glob/dist/commonjs/pattern.js +4 -0
  11. package/node_modules/glob/dist/commonjs/pattern.js.map +1 -1
  12. package/node_modules/glob/dist/esm/glob.d.ts +8 -0
  13. package/node_modules/glob/dist/esm/glob.d.ts.map +1 -1
  14. package/node_modules/glob/dist/esm/glob.js +2 -1
  15. package/node_modules/glob/dist/esm/glob.js.map +1 -1
  16. package/node_modules/glob/dist/esm/index.min.js +3 -3
  17. package/node_modules/glob/dist/esm/index.min.js.map +4 -4
  18. package/node_modules/glob/dist/esm/pattern.d.ts +3 -0
  19. package/node_modules/glob/dist/esm/pattern.d.ts.map +1 -1
  20. package/node_modules/glob/dist/esm/pattern.js +4 -0
  21. package/node_modules/glob/dist/esm/pattern.js.map +1 -1
  22. package/node_modules/{@isaacs → glob/node_modules}/balanced-match/README.md +7 -10
  23. package/node_modules/{@isaacs → glob/node_modules}/balanced-match/package.json +7 -18
  24. package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/README.md +3 -6
  25. package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/dist/commonjs/index.js +6 -4
  26. package/node_modules/glob/node_modules/brace-expansion/dist/commonjs/index.js.map +1 -0
  27. package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/dist/esm/index.js +6 -4
  28. package/node_modules/glob/node_modules/brace-expansion/dist/esm/index.js.map +1 -0
  29. package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/package.json +11 -7
  30. package/node_modules/glob/node_modules/minimatch/README.md +76 -1
  31. package/node_modules/glob/node_modules/minimatch/dist/commonjs/assert-valid-pattern.d.ts +1 -1
  32. package/node_modules/glob/node_modules/minimatch/dist/commonjs/assert-valid-pattern.d.ts.map +1 -1
  33. package/node_modules/glob/node_modules/minimatch/dist/commonjs/assert-valid-pattern.js.map +1 -1
  34. package/node_modules/glob/node_modules/minimatch/dist/commonjs/ast.d.ts +4 -2
  35. package/node_modules/glob/node_modules/minimatch/dist/commonjs/ast.d.ts.map +1 -1
  36. package/node_modules/glob/node_modules/minimatch/dist/commonjs/ast.js +309 -55
  37. package/node_modules/glob/node_modules/minimatch/dist/commonjs/ast.js.map +1 -1
  38. package/node_modules/glob/node_modules/minimatch/dist/commonjs/brace-expressions.d.ts.map +1 -1
  39. package/node_modules/glob/node_modules/minimatch/dist/commonjs/brace-expressions.js +2 -4
  40. package/node_modules/glob/node_modules/minimatch/dist/commonjs/brace-expressions.js.map +1 -1
  41. package/node_modules/glob/node_modules/minimatch/dist/commonjs/escape.d.ts +1 -1
  42. package/node_modules/glob/node_modules/minimatch/dist/commonjs/escape.d.ts.map +1 -1
  43. package/node_modules/glob/node_modules/minimatch/dist/commonjs/escape.js +4 -4
  44. package/node_modules/glob/node_modules/minimatch/dist/commonjs/escape.js.map +1 -1
  45. package/node_modules/glob/node_modules/minimatch/dist/commonjs/index.d.ts +81 -1
  46. package/node_modules/glob/node_modules/minimatch/dist/commonjs/index.d.ts.map +1 -1
  47. package/node_modules/glob/node_modules/minimatch/dist/commonjs/index.js +232 -134
  48. package/node_modules/glob/node_modules/minimatch/dist/commonjs/index.js.map +1 -1
  49. package/node_modules/glob/node_modules/minimatch/dist/commonjs/unescape.d.ts +1 -1
  50. package/node_modules/glob/node_modules/minimatch/dist/commonjs/unescape.d.ts.map +1 -1
  51. package/node_modules/glob/node_modules/minimatch/dist/commonjs/unescape.js +8 -8
  52. package/node_modules/glob/node_modules/minimatch/dist/commonjs/unescape.js.map +1 -1
  53. package/node_modules/glob/node_modules/minimatch/dist/esm/assert-valid-pattern.d.ts +1 -1
  54. package/node_modules/glob/node_modules/minimatch/dist/esm/assert-valid-pattern.d.ts.map +1 -1
  55. package/node_modules/glob/node_modules/minimatch/dist/esm/assert-valid-pattern.js.map +1 -1
  56. package/node_modules/glob/node_modules/minimatch/dist/esm/ast.d.ts +4 -2
  57. package/node_modules/glob/node_modules/minimatch/dist/esm/ast.d.ts.map +1 -1
  58. package/node_modules/glob/node_modules/minimatch/dist/esm/ast.js +309 -55
  59. package/node_modules/glob/node_modules/minimatch/dist/esm/ast.js.map +1 -1
  60. package/node_modules/glob/node_modules/minimatch/dist/esm/brace-expressions.d.ts.map +1 -1
  61. package/node_modules/glob/node_modules/minimatch/dist/esm/brace-expressions.js +2 -4
  62. package/node_modules/glob/node_modules/minimatch/dist/esm/brace-expressions.js.map +1 -1
  63. package/node_modules/glob/node_modules/minimatch/dist/esm/escape.d.ts +1 -1
  64. package/node_modules/glob/node_modules/minimatch/dist/esm/escape.d.ts.map +1 -1
  65. package/node_modules/glob/node_modules/minimatch/dist/esm/escape.js +4 -4
  66. package/node_modules/glob/node_modules/minimatch/dist/esm/escape.js.map +1 -1
  67. package/node_modules/glob/node_modules/minimatch/dist/esm/index.d.ts +81 -1
  68. package/node_modules/glob/node_modules/minimatch/dist/esm/index.d.ts.map +1 -1
  69. package/node_modules/glob/node_modules/minimatch/dist/esm/index.js +232 -134
  70. package/node_modules/glob/node_modules/minimatch/dist/esm/index.js.map +1 -1
  71. package/node_modules/glob/node_modules/minimatch/dist/esm/unescape.d.ts +1 -1
  72. package/node_modules/glob/node_modules/minimatch/dist/esm/unescape.d.ts.map +1 -1
  73. package/node_modules/glob/node_modules/minimatch/dist/esm/unescape.js +8 -8
  74. package/node_modules/glob/node_modules/minimatch/dist/esm/unescape.js.map +1 -1
  75. package/node_modules/glob/node_modules/minimatch/package.json +17 -11
  76. package/node_modules/glob/package.json +10 -13
  77. package/node_modules/minipass/LICENSE.md +55 -0
  78. package/node_modules/minipass/dist/commonjs/index.d.ts +12 -16
  79. package/node_modules/minipass/dist/commonjs/index.d.ts.map +1 -1
  80. package/node_modules/minipass/dist/commonjs/index.js +13 -3
  81. package/node_modules/minipass/dist/commonjs/index.js.map +1 -1
  82. package/node_modules/minipass/dist/esm/index.d.ts +12 -16
  83. package/node_modules/minipass/dist/esm/index.d.ts.map +1 -1
  84. package/node_modules/minipass/dist/esm/index.js +3 -1
  85. package/node_modules/minipass/dist/esm/index.js.map +1 -1
  86. package/node_modules/minipass/package.json +9 -14
  87. package/node_modules/path-scurry/node_modules/lru-cache/README.md +96 -10
  88. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/diagnostics-channel-browser.d.ts.map +1 -0
  89. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/diagnostics-channel-browser.js.map +1 -0
  90. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/diagnostics-channel.d.ts +5 -0
  91. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/diagnostics-channel.js +7 -0
  92. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/index.d.ts +1400 -0
  93. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/index.d.ts.map +1 -0
  94. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/index.js +1726 -0
  95. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/index.js.map +1 -0
  96. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/index.min.js +2 -0
  97. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/index.min.js.map +7 -0
  98. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/perf.d.ts +12 -0
  99. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/perf.d.ts.map +1 -0
  100. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/perf.js +10 -0
  101. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/perf.js.map +1 -0
  102. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/diagnostics-channel-cjs.cjs.map +1 -0
  103. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/diagnostics-channel-cjs.d.cts.map +1 -0
  104. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/diagnostics-channel.d.ts +5 -0
  105. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/diagnostics-channel.js +7 -0
  106. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/index.d.ts +109 -32
  107. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/index.d.ts.map +1 -1
  108. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/index.js +334 -197
  109. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/index.js.map +1 -1
  110. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/index.min.js +1 -1
  111. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/index.min.js.map +4 -4
  112. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/diagnostics-channel-node.d.ts.map +1 -0
  113. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/diagnostics-channel-node.js.map +1 -0
  114. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/diagnostics-channel.d.ts +5 -0
  115. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/diagnostics-channel.js +9 -0
  116. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/index.d.ts +1400 -0
  117. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/index.d.ts.map +1 -0
  118. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/index.js +1726 -0
  119. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/index.js.map +1 -0
  120. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/index.min.js +2 -0
  121. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/index.min.js.map +7 -0
  122. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/perf.d.ts +12 -0
  123. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/perf.d.ts.map +1 -0
  124. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/perf.js +10 -0
  125. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/perf.js.map +1 -0
  126. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/perf.d.ts +12 -0
  127. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/perf.d.ts.map +1 -0
  128. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/perf.js +10 -0
  129. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/perf.js.map +1 -0
  130. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/diagnostics-channel-browser.d.ts.map +1 -0
  131. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/diagnostics-channel-browser.js.map +1 -0
  132. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/diagnostics-channel.d.ts +5 -0
  133. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/diagnostics-channel.js +4 -0
  134. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/index.d.ts +1400 -0
  135. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/index.d.ts.map +1 -0
  136. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/index.js +1722 -0
  137. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/index.js.map +1 -0
  138. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/index.min.js +2 -0
  139. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/index.min.js.map +7 -0
  140. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/perf.d.ts +12 -0
  141. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/perf.d.ts.map +1 -0
  142. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/perf.js +7 -0
  143. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/perf.js.map +1 -0
  144. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/diagnostics-channel-esm.d.mts.map +1 -0
  145. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/diagnostics-channel-esm.mjs.map +1 -0
  146. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/diagnostics-channel.d.ts +5 -0
  147. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/diagnostics-channel.js +19 -0
  148. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/index.d.ts +109 -32
  149. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/index.d.ts.map +1 -1
  150. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/index.js +333 -196
  151. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/index.js.map +1 -1
  152. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/index.min.js +1 -1
  153. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/index.min.js.map +4 -4
  154. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/diagnostics-channel-node.d.ts.map +1 -0
  155. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/diagnostics-channel-node.js.map +1 -0
  156. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/diagnostics-channel.d.ts +5 -0
  157. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/diagnostics-channel.js +6 -0
  158. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/index.d.ts +1400 -0
  159. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/index.d.ts.map +1 -0
  160. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/index.js +1722 -0
  161. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/index.js.map +1 -0
  162. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/index.min.js +2 -0
  163. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/index.min.js.map +7 -0
  164. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/perf.d.ts +12 -0
  165. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/perf.d.ts.map +1 -0
  166. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/perf.js +7 -0
  167. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/perf.js.map +1 -0
  168. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/perf.d.ts +12 -0
  169. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/perf.d.ts.map +1 -0
  170. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/perf.js +7 -0
  171. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/perf.js.map +1 -0
  172. package/node_modules/path-scurry/node_modules/lru-cache/package.json +71 -18
  173. package/node_modules/path-scurry/package.json +8 -24
  174. package/package.json +1 -1
  175. package/scripts/debug-balance-probe.mjs +35 -35
  176. package/scripts/push-image.sh +43 -43
  177. package/scripts/setup-acr.sh +65 -65
  178. package/scripts/verify-optional-deps.js +96 -1
  179. package/src/__tests__/composioCliFlags.test.js +239 -239
  180. package/src/analyzers/CSSAnalyzer.js +298 -297
  181. package/src/analyzers/ConfigValidator.js +691 -690
  182. package/src/analyzers/ESLintAnalyzer.js +320 -320
  183. package/src/analyzers/JavaScriptAnalyzer.js +260 -261
  184. package/src/analyzers/PrettierFormatter.js +246 -247
  185. package/src/analyzers/PythonAnalyzer.js +283 -283
  186. package/src/analyzers/SecurityAnalyzer.js +729 -729
  187. package/src/analyzers/SparrowAnalyzer.js +341 -341
  188. package/src/analyzers/TypeScriptAnalyzer.js +247 -247
  189. package/src/analyzers/__tests__/CSSAnalyzer.test.js +41 -41
  190. package/src/analyzers/__tests__/ConfigValidator.test.js +362 -362
  191. package/src/analyzers/__tests__/JavaScriptAnalyzer.test.js +40 -40
  192. package/src/analyzers/__tests__/PythonAnalyzer.test.js +205 -208
  193. package/src/analyzers/__tests__/SecurityAnalyzer.test.js +303 -303
  194. package/src/analyzers/__tests__/TypeScriptAnalyzer.test.js +187 -187
  195. package/src/analyzers/codeCloneDetector/analyzer.js +344 -344
  196. package/src/analyzers/codeCloneDetector/detector.js +250 -250
  197. package/src/analyzers/codeCloneDetector/index.js +194 -192
  198. package/src/analyzers/codeCloneDetector/parser.js +199 -199
  199. package/src/core/__tests__/agentPool.test.js +866 -866
  200. package/src/core/__tests__/agentPoolAutoResume.test.js +209 -209
  201. package/src/core/__tests__/agentPoolWakeOnMessage.test.js +315 -315
  202. package/src/core/__tests__/agentScheduler.emptyResponseChatStall.test.js +213 -213
  203. package/src/core/__tests__/agentScheduler.errorCategorisation.test.js +246 -246
  204. package/src/core/__tests__/agentScheduler.firstChunkTimeout.test.js +138 -138
  205. package/src/core/__tests__/agentScheduler.modeTransitions.test.js +233 -233
  206. package/src/core/__tests__/agentScheduler.nativePromptPick.test.js +319 -319
  207. package/src/core/__tests__/agentScheduler.taskLifecycleInstruction.test.js +78 -78
  208. package/src/core/__tests__/agentScheduler.visualizer.test.js +258 -258
  209. package/src/core/__tests__/flowCheckpointStore.test.js +140 -140
  210. package/src/core/__tests__/flowEndToEnd.test.js +565 -565
  211. package/src/core/__tests__/flowFieldMapping.test.js +188 -189
  212. package/src/core/__tests__/flowLintClientMirror.test.js +96 -98
  213. package/src/core/__tests__/flowSavePayload.test.js +170 -169
  214. package/src/core/__tests__/flowTemplates.test.js +311 -311
  215. package/src/core/__tests__/flowVersionStore.test.js +123 -123
  216. package/src/core/__tests__/messageProcessor.test.js +669 -669
  217. package/src/core/__tests__/stateManager.test.js +0 -1
  218. package/src/core/agentPool.js +2474 -2475
  219. package/src/core/agentScheduler.js +1 -4
  220. package/src/core/contextManager.js +708 -708
  221. package/src/core/flowExecutor.js +1510 -1510
  222. package/src/core/flowFieldMapping.js +136 -138
  223. package/src/core/messageProcessor.js +953 -954
  224. package/src/core/orchestrator.js +593 -595
  225. package/src/core/stateManager.js +1765 -1752
  226. package/src/index.js +1221 -1221
  227. package/src/interfaces/__tests__/archivedAgentDelete.test.js +207 -207
  228. package/src/interfaces/__tests__/bulkAgentRoute.test.js +361 -361
  229. package/src/interfaces/__tests__/imageServing.test.js +228 -228
  230. package/src/interfaces/__tests__/remoteSessionAuth.test.js +308 -308
  231. package/src/interfaces/__tests__/videoJobsRoutes.test.js +178 -179
  232. package/src/interfaces/__tests__/webServer.marketplace.test.js +629 -629
  233. package/src/interfaces/schedulerRoutes.js +50 -50
  234. package/src/interfaces/terminal/__tests__/smoke/connection.test.js +341 -350
  235. package/src/interfaces/terminal/__tests__/smoke/enhancements.test.js +156 -156
  236. package/src/interfaces/terminal/__tests__/smoke/imports.test.js +325 -330
  237. package/src/interfaces/terminal/__tests__/smoke/tools.test.js +385 -388
  238. package/src/interfaces/terminal/api/session.js +265 -266
  239. package/src/interfaces/terminal/api/websocket.js +496 -497
  240. package/src/interfaces/terminal/components/AgentCreator.js +691 -705
  241. package/src/interfaces/terminal/components/AgentEditor.js +676 -678
  242. package/src/interfaces/terminal/components/AgentSwitcher.js +331 -330
  243. package/src/interfaces/terminal/components/ErrorPanel.js +263 -264
  244. package/src/interfaces/terminal/components/Header.js +28 -28
  245. package/src/interfaces/terminal/components/Layout.js +598 -603
  246. package/src/interfaces/terminal/components/MessageList.js +280 -281
  247. package/src/interfaces/terminal/components/SettingsPanel.js +410 -415
  248. package/src/interfaces/terminal/components/StatusBar.js +2 -0
  249. package/src/interfaces/terminal/index.js +168 -168
  250. package/src/interfaces/terminal/state/useAgentControl.js +496 -496
  251. package/src/interfaces/terminal/state/useAgents.js +537 -537
  252. package/src/interfaces/terminal/state/useMessages.js +629 -630
  253. package/src/interfaces/terminal/state/useTools.js +554 -554
  254. package/src/interfaces/terminal/utils/debugLogger.js +44 -44
  255. package/src/interfaces/terminal/utils/settingsStorage.js +232 -232
  256. package/src/interfaces/webServer.js +7578 -7579
  257. package/src/interfaces/webServer.js.bak +7046 -7046
  258. package/src/modules/fileExplorer/__tests__/zipDownload.test.js +237 -237
  259. package/src/modules/fileExplorer/controller.js +470 -469
  260. package/src/modules/fileExplorer/routes.js +285 -286
  261. package/src/modules/widget/__tests__/isDisabled.test.js +41 -41
  262. package/src/modules/widget/__tests__/routes.test.js +677 -678
  263. package/src/modules/widget/__tests__/runtime.test.js +401 -401
  264. package/src/modules/widget/__tests__/versioning.test.js +309 -309
  265. package/src/modules/widget/__tests__/webComponentRuntime.test.js +565 -565
  266. package/src/modules/widget/__tests__/widgetTool.test.js +316 -316
  267. package/src/modules/widget/routes.js +435 -435
  268. package/src/modules/widget/runtime/bundle.js +640 -640
  269. package/src/modules/widget/runtime/webComponentBundle.js +470 -470
  270. package/src/modules/widget/schema.js +182 -181
  271. package/src/modules/widget/widgetTool.js +1389 -1389
  272. package/src/services/__tests__/agentActivityService.test.js +401 -402
  273. package/src/services/__tests__/benchmarkService.test.js +184 -184
  274. package/src/services/__tests__/contextInjectionService.test.js +246 -246
  275. package/src/services/__tests__/conversationQuery.test.js +721 -723
  276. package/src/services/__tests__/credentialVault.test.js +469 -469
  277. package/src/services/__tests__/discordService.integration.test.js +638 -639
  278. package/src/services/__tests__/flowContextService.test.js +590 -590
  279. package/src/services/__tests__/memoryService.test.js +1 -1
  280. package/src/services/__tests__/messageSource.test.js +380 -380
  281. package/src/services/__tests__/modelRouterNaming.test.js +111 -111
  282. package/src/services/__tests__/projectDetector.test.js +34 -34
  283. package/src/services/__tests__/promptService.test.js +242 -242
  284. package/src/services/__tests__/telegramService.test.js +941 -941
  285. package/src/services/__tests__/tokenCountingService.test.js +48 -48
  286. package/src/services/agentActivityService.js +419 -420
  287. package/src/services/aiService.js +2997 -3001
  288. package/src/services/apiKeyManager.js +359 -359
  289. package/src/services/benchmarkService.js +196 -196
  290. package/src/services/codebaseKnowledgeService.js +2 -2
  291. package/src/services/composioService.js +738 -738
  292. package/src/services/conversationCompactionService.js +1258 -1257
  293. package/src/services/credentialVault.js +685 -685
  294. package/src/services/discordService.js +792 -793
  295. package/src/services/embeddings/__tests__/azureCustomProvider.test.js +232 -232
  296. package/src/services/embeddings/__tests__/embeddingService.test.js +417 -417
  297. package/src/services/embeddings/__tests__/localProvider.test.js +263 -263
  298. package/src/services/embeddings/autoRecall.js +218 -219
  299. package/src/services/embeddings/indexers/__tests__/agentIndexer.test.js +232 -232
  300. package/src/services/embeddings/indexers/__tests__/memoryIndexer.test.js +418 -418
  301. package/src/services/embeddings/indexers/__tests__/reminisceIndexer.test.js +356 -357
  302. package/src/services/embeddings/indexers/__tests__/skillsIndexer.test.js +145 -145
  303. package/src/services/embeddings/indexers/__tests__/taskIndexer.test.js +146 -146
  304. package/src/services/embeddings/indexers/composioIndexer.js +279 -279
  305. package/src/services/embeddings/providerInterface.js +206 -206
  306. package/src/services/embeddings/providers/localProvider.js +11 -7
  307. package/src/services/embeddings/providers/openaiProvider.js +101 -101
  308. package/src/services/embeddings/vectorStore/inMemoryJsonStore.js +356 -356
  309. package/src/services/errorHandler.js +809 -809
  310. package/src/services/flowContextService.js +586 -586
  311. package/src/services/grounding/MockAdapter.js +125 -125
  312. package/src/services/modelRouterService.js +26 -31
  313. package/src/services/modelsService.js +322 -322
  314. package/src/services/ollamaService.js +452 -452
  315. package/src/services/projectDetector.js +403 -404
  316. package/src/services/promptService.js +418 -418
  317. package/src/services/qualityInspector.js +795 -795
  318. package/src/services/scheduleService.js +726 -726
  319. package/src/services/serviceRegistry.js +386 -386
  320. package/src/services/telegrafBot.js +174 -174
  321. package/src/services/telegramService.js +1972 -1972
  322. package/src/services/visualEditorBridge.js +1033 -1033
  323. package/src/services/visualEditorServer.js +1769 -1774
  324. package/src/services/whatsappService.js +667 -668
  325. package/src/tools/__tests__/agentCommunicationTool.findAgent.test.js +226 -226
  326. package/src/tools/__tests__/agentCommunicationTool.test.js +3 -3
  327. package/src/tools/__tests__/agentDelayTool.test.js +342 -342
  328. package/src/tools/__tests__/baseTool.test.js +3 -3
  329. package/src/tools/__tests__/codeMapTool.test.js +915 -915
  330. package/src/tools/__tests__/fileContentReplaceTool.test.js +309 -309
  331. package/src/tools/__tests__/fileTreeTool.test.js +274 -274
  332. package/src/tools/__tests__/filesystemTool.test.js +815 -815
  333. package/src/tools/__tests__/foundryWebSearchTool.test.js +252 -252
  334. package/src/tools/__tests__/imageTool.validator.test.js +194 -194
  335. package/src/tools/__tests__/jobDoneTool.test.js +580 -581
  336. package/src/tools/__tests__/memoryTool.forgetStale.test.js +272 -272
  337. package/src/tools/__tests__/memoryTool.reminisce.test.js +2 -2
  338. package/src/tools/__tests__/memoryTool.reminisceSemanticSearch.test.js +301 -301
  339. package/src/tools/__tests__/memoryTool.semanticSearch.test.js +405 -405
  340. package/src/tools/__tests__/memoryTool.teamPool.test.js +293 -293
  341. package/src/tools/__tests__/memoryTool.test.js +1 -1
  342. package/src/tools/__tests__/seekTool.test.js +282 -282
  343. package/src/tools/__tests__/skillsTool.search.test.js +164 -164
  344. package/src/tools/__tests__/skillsTool.test.js +226 -226
  345. package/src/tools/__tests__/staticAnalysisTool.test.js +509 -509
  346. package/src/tools/__tests__/taskManagerTool.discipline.test.js +137 -137
  347. package/src/tools/__tests__/taskManagerTool.search.test.js +143 -143
  348. package/src/tools/__tests__/taskManagerTool.test.js +866 -866
  349. package/src/tools/__tests__/terminalTool.test.js +448 -448
  350. package/src/tools/__tests__/toolShapeForgiveness.test.js +259 -260
  351. package/src/tools/__tests__/userPromptTool.test.js +297 -297
  352. package/src/tools/__tests__/videoTool.jobs.test.js +147 -147
  353. package/src/tools/__tests__/webTool.e2e.test.js +609 -603
  354. package/src/tools/__tests__/webTool.unit.test.js +195 -195
  355. package/src/tools/__tests__/webTool.visionModel.test.js +75 -75
  356. package/src/tools/agentCommunicationTool.js +8 -10
  357. package/src/tools/agentDelayTool.js +496 -497
  358. package/src/tools/asyncToolManager.js +602 -603
  359. package/src/tools/baseTool.js +12 -11
  360. package/src/tools/cloneDetectionTool.js +576 -581
  361. package/src/tools/codeMapTool.js +0 -6
  362. package/src/tools/composioTool.js +617 -617
  363. package/src/tools/dependencyResolverTool.js +1211 -1212
  364. package/src/tools/desktop/DesktopTool.js +629 -638
  365. package/src/tools/desktop/__tests__/DesktopTool.e2e.test.js +306 -306
  366. package/src/tools/desktop/__tests__/DesktopTool.test.js +507 -507
  367. package/src/tools/desktop/__tests__/osController.test.js +364 -364
  368. package/src/tools/desktop/osController.js +491 -491
  369. package/src/tools/docxTool.js +623 -623
  370. package/src/tools/excelTool.js +636 -636
  371. package/src/tools/fileContentReplaceTool.js +5 -7
  372. package/src/tools/fileSystemTool.js +12 -19
  373. package/src/tools/fileTreeTool.js +840 -840
  374. package/src/tools/foundryWebSearchTool.js +273 -273
  375. package/src/tools/helpTool.js +198 -198
  376. package/src/tools/imageTool.js +1397 -1397
  377. package/src/tools/importAnalyzerTool.js +1056 -1056
  378. package/src/tools/jobDoneTool.js +495 -495
  379. package/src/tools/memoryTool.js +1 -1
  380. package/src/tools/office/pres/__tests__/presSystem.test.js +365 -365
  381. package/src/tools/office/pres/archetypes/agenda.js +61 -61
  382. package/src/tools/office/pres/archetypes/bentoGrid.js +218 -219
  383. package/src/tools/office/pres/archetypes/bigStat.js +140 -142
  384. package/src/tools/office/pres/archetypes/closing.js +70 -70
  385. package/src/tools/office/pres/archetypes/hero.js +70 -70
  386. package/src/tools/office/pres/archetypes/productHero.js +93 -94
  387. package/src/tools/office/pres/archetypes/table.js +73 -74
  388. package/src/tools/office/pres/backgrounds/orb.js +66 -66
  389. package/src/tools/office/pres/components.js +422 -423
  390. package/src/tools/officeTool.js +441 -441
  391. package/src/tools/pdfTool.js +625 -627
  392. package/src/tools/platformControlTool.js +1081 -1081
  393. package/src/tools/seekTool.js +917 -918
  394. package/src/tools/skillsTool.js +1 -1
  395. package/src/tools/staticAnalysisTool.js +2143 -2146
  396. package/src/tools/taskManagerTool.js +3324 -3324
  397. package/src/tools/terminalTool.js +2615 -2618
  398. package/src/tools/videoTool.js +1303 -1303
  399. package/src/tools/visionTool.js +508 -508
  400. package/src/tools/visualEditorTool.js +1289 -1290
  401. package/src/tools/webTool.js +3368 -3368
  402. package/src/tools/whatsappTool.js +464 -464
  403. package/src/types/__tests__/agent.test.js +499 -499
  404. package/src/types/__tests__/contextReference.test.js +606 -606
  405. package/src/types/__tests__/conversation.test.js +555 -555
  406. package/src/types/__tests__/toolCommand.test.js +584 -584
  407. package/src/types/contextReference.js +974 -971
  408. package/src/types/conversation.js +729 -729
  409. package/src/types/toolCommand.js +746 -746
  410. package/src/utilities/__tests__/attachmentValidator.test.js +80 -80
  411. package/src/utilities/__tests__/auditReport.test.js +328 -328
  412. package/src/utilities/__tests__/directoryAccessManager.test.js +388 -388
  413. package/src/utilities/__tests__/jsonRepair.test.js +103 -104
  414. package/src/utilities/__tests__/modeTransitionReasons.test.js +105 -105
  415. package/src/utilities/__tests__/platformUtils.test.js +80 -87
  416. package/src/utilities/__tests__/structuredFileValidator.test.js +261 -263
  417. package/src/utilities/__tests__/toolConstants.test.js +92 -94
  418. package/src/utilities/__tests__/useIsTouchDevice.detect.test.js +114 -114
  419. package/src/utilities/__tests__/webUiUtilSync.test.js +117 -117
  420. package/src/utilities/attachmentValidator.js +284 -288
  421. package/src/utilities/authCache.js.backup-1779570472481 +121 -121
  422. package/src/utilities/browserStealth.js +631 -630
  423. package/src/utilities/configManager.js +616 -617
  424. package/src/utilities/directoryAccessManager.js +564 -565
  425. package/src/utilities/fileProcessor.js +308 -307
  426. package/src/utilities/humanBehavior.js +454 -453
  427. package/src/utilities/logger.js +479 -479
  428. package/src/utilities/structuredFileValidator.js +696 -699
  429. package/src/utilities/tagParser.js +5 -10
  430. package/src/utilities/userDataDir.js +308 -308
  431. package/node_modules/@isaacs/brace-expansion/dist/commonjs/index.js.map +0 -1
  432. package/node_modules/@isaacs/brace-expansion/dist/esm/index.js.map +0 -1
  433. package/node_modules/minipass/LICENSE +0 -15
  434. /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/LICENSE.md +0 -0
  435. /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/commonjs/index.d.ts +0 -0
  436. /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/commonjs/index.d.ts.map +0 -0
  437. /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/commonjs/index.js +0 -0
  438. /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/commonjs/index.js.map +0 -0
  439. /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/commonjs/package.json +0 -0
  440. /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/esm/index.d.ts +0 -0
  441. /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/esm/index.d.ts.map +0 -0
  442. /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/esm/index.js +0 -0
  443. /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/esm/index.js.map +0 -0
  444. /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/esm/package.json +0 -0
  445. /package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/LICENSE +0 -0
  446. /package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/dist/commonjs/index.d.ts +0 -0
  447. /package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/dist/commonjs/index.d.ts.map +0 -0
  448. /package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/dist/commonjs/package.json +0 -0
  449. /package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/dist/esm/index.d.ts +0 -0
  450. /package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/dist/esm/index.d.ts.map +0 -0
  451. /package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/dist/esm/package.json +0 -0
@@ -1,639 +1,638 @@
1
- /**
2
- * Discord Service — Integration / Flow Tests
3
- *
4
- * Tests complete flows across components rather than isolated units:
5
- * - Full message round-trip (Discord msg → agent → response back to channel)
6
- * - API route integration (HTTP → service → response)
7
- * - Broadcast chain integrity (multiple services wrapping broadcastToSession)
8
- * - Config persistence round-trip (save → reload → verify state)
9
- * - Multi-channel isolation (agent responses only go to originating channel)
10
- * - Agent lifecycle (deletion cleans up stale mappings)
11
- */
12
-
13
- import { jest, describe, test, expect, beforeEach, afterEach } from '@jest/globals';
14
- import { createMockLogger } from '../../__test-utils__/mockFactories.js';
15
-
16
- // Mock fs for config persistence
17
- const mockFs = {
18
- mkdir: jest.fn().mockResolvedValue(undefined),
19
- readFile: jest.fn().mockRejectedValue(new Error('ENOENT')),
20
- writeFile: jest.fn().mockResolvedValue(undefined)
21
- };
22
-
23
- jest.unstable_mockModule('fs', () => ({
24
- promises: mockFs
25
- }));
26
-
27
- jest.unstable_mockModule('../../utilities/userDataDir.js', () => ({
28
- getUserDataPaths: () => ({ base: '/mock/data' }),
29
- ensureUserDataDirs: jest.fn().mockResolvedValue(undefined)
30
- }));
31
-
32
- const { DiscordService, DISCORD_STATUS } = await import('../discordService.js');
33
-
34
- // --- Helpers ---
35
-
36
- function createService() {
37
- const logger = createMockLogger();
38
- const service = new DiscordService(logger);
39
- return service;
40
- }
41
-
42
- function createMockOrchestrator() {
43
- return {
44
- processRequest: jest.fn().mockResolvedValue({ success: true })
45
- };
46
- }
47
-
48
- function createMockAgentPool(agents = {}) {
49
- return {
50
- getAgent: jest.fn().mockImplementation(id => Promise.resolve(agents[id] || null)),
51
- getAllAgents: jest.fn().mockReturnValue(Object.values(agents))
52
- };
53
- }
54
-
55
- function createMockChannel(id = 'c1') {
56
- return {
57
- id,
58
- send: jest.fn().mockResolvedValue({}),
59
- sendTyping: jest.fn().mockResolvedValue(undefined)
60
- };
61
- }
62
-
63
- function createDiscordMessage(content, opts = {}) {
64
- const guildId = opts.guildId || 'guild-1';
65
- const channelId = opts.channelId || 'ch-1';
66
- return {
67
- content,
68
- author: { bot: opts.bot || false, id: opts.userId || 'user-42' },
69
- guild: opts.noDM ? null : { id: guildId },
70
- channel: {
71
- id: channelId,
72
- parentId: opts.parentId || null, // thread parent channel ID
73
- send: jest.fn().mockResolvedValue({}),
74
- sendTyping: jest.fn().mockResolvedValue(undefined)
75
- },
76
- reply: jest.fn().mockResolvedValue({})
77
- };
78
- }
79
-
80
- // ========================================================================
81
- // FLOW 1: Full message round-trip
82
- // Discord message → _handleMessage → orchestrator → broadcast → channel.send
83
- // ========================================================================
84
-
85
- describe('Flow: Full message round-trip', () => {
86
- let service, orchestrator, agentPool, mockChannel;
87
-
88
- beforeEach(() => {
89
- service = createService();
90
- orchestrator = createMockOrchestrator();
91
- agentPool = createMockAgentPool({
92
- 'agent-alpha': { id: 'agent-alpha', name: 'Alpha' }
93
- });
94
- mockChannel = createMockChannel('ch-1');
95
-
96
- service.setOrchestrator(orchestrator);
97
- service.setAgentPool(agentPool);
98
- service.status = DISCORD_STATUS.CONNECTED;
99
- service.client = {
100
- channels: { fetch: jest.fn().mockResolvedValue(mockChannel) }
101
- };
102
-
103
- // Map agent to channel
104
- service.channelMappings = { 'guild-1:ch-1': ['agent-alpha'] };
105
- });
106
-
107
- test('user sends message → agent processes → response appears in same channel', async () => {
108
- // Step 1: Incoming Discord message
109
- const msg = createDiscordMessage('explain recursion');
110
- await service._handleMessage(msg);
111
-
112
- // Step 2: Verify orchestrator received the request
113
- expect(orchestrator.processRequest).toHaveBeenCalledTimes(1);
114
- const request = orchestrator.processRequest.mock.calls[0][0];
115
- expect(request.interface).toBe('discord');
116
- expect(request.sessionId).toBe('discord-guild-1-ch-1');
117
- expect(request.payload.agentId).toBe('agent-alpha');
118
- expect(request.payload.message).toBe('explain recursion');
119
-
120
- // Step 3: Simulate broadcast from agentScheduler (what happens after AI responds)
121
- await service._handleBroadcastEvent('discord-guild-1-ch-1', {
122
- type: 'stream_complete',
123
- agentId: 'agent-alpha',
124
- content: '<external>Recursion is when a function calls itself.</external>'
125
- });
126
-
127
- // Step 4: Verify response was sent to the correct channel
128
- expect(service.client.channels.fetch).toHaveBeenCalledWith('ch-1');
129
- expect(mockChannel.send).toHaveBeenCalledTimes(1);
130
- const sent = mockChannel.send.mock.calls[0][0];
131
- expect(sent).toContain('Alpha');
132
- expect(sent).toContain('Recursion is when a function calls itself.');
133
- });
134
-
135
- test('full round-trip with long response triggers message splitting', async () => {
136
- const msg = createDiscordMessage('write a long essay');
137
- await service._handleMessage(msg);
138
-
139
- // Simulate a response longer than Discord's limit, wrapped for relay
140
- const longContent = 'A'.repeat(3500);
141
- await service._handleBroadcastEvent('discord-guild-1-ch-1', {
142
- type: 'stream_complete',
143
- agentId: 'agent-alpha',
144
- content: `<external>${longContent}</external>`
145
- });
146
-
147
- // Should have split into multiple messages
148
- expect(mockChannel.send).toHaveBeenCalled();
149
- const totalSent = mockChannel.send.mock.calls.map(c => c[0]).join('');
150
- expect(totalSent).toContain('Alpha');
151
- expect(totalSent.length).toBeGreaterThanOrEqual(3500);
152
- });
153
- });
154
-
155
- // ========================================================================
156
- // FLOW 2: Multi-agent channel routing
157
- // ========================================================================
158
-
159
- describe('Flow: Multi-agent channel routing', () => {
160
- let service, orchestrator, agentPool;
161
-
162
- beforeEach(() => {
163
- service = createService();
164
- orchestrator = createMockOrchestrator();
165
- agentPool = createMockAgentPool({
166
- 'a1': { id: 'a1', name: 'Coder' },
167
- 'a2': { id: 'a2', name: 'Reviewer' }
168
- });
169
-
170
- service.setOrchestrator(orchestrator);
171
- service.setAgentPool(agentPool);
172
- service.status = DISCORD_STATUS.CONNECTED;
173
- service.channelMappings = { 'g1:c1': ['a1', 'a2'] };
174
- });
175
-
176
- test('@mention routes to correct agent, then follow-up uses sticky', async () => {
177
- // First message: explicit @mention
178
- const msg1 = createDiscordMessage('@Coder fix the login bug', { guildId: 'g1', channelId: 'c1' });
179
- await service._handleMessage(msg1);
180
-
181
- expect(orchestrator.processRequest).toHaveBeenCalledTimes(1);
182
- expect(orchestrator.processRequest.mock.calls[0][0].payload.agentId).toBe('a1');
183
- expect(orchestrator.processRequest.mock.calls[0][0].payload.message).toBe('fix the login bug');
184
-
185
- // Second message: no mention — should use sticky (last addressed agent)
186
- const msg2 = createDiscordMessage('also add error handling', { guildId: 'g1', channelId: 'c1' });
187
- await service._handleMessage(msg2);
188
-
189
- expect(orchestrator.processRequest).toHaveBeenCalledTimes(2);
190
- expect(orchestrator.processRequest.mock.calls[1][0].payload.agentId).toBe('a1'); // sticky to Coder
191
-
192
- // Third message: switch to different agent via @mention
193
- const msg3 = createDiscordMessage('@Reviewer review the PR', { guildId: 'g1', channelId: 'c1' });
194
- await service._handleMessage(msg3);
195
-
196
- expect(orchestrator.processRequest).toHaveBeenCalledTimes(3);
197
- expect(orchestrator.processRequest.mock.calls[2][0].payload.agentId).toBe('a2'); // switched to Reviewer
198
-
199
- // Fourth message: no mention — sticky should now be Reviewer
200
- const msg4 = createDiscordMessage('any concerns?', { guildId: 'g1', channelId: 'c1' });
201
- await service._handleMessage(msg4);
202
-
203
- expect(orchestrator.processRequest.mock.calls[3][0].payload.agentId).toBe('a2');
204
- });
205
-
206
- test('invalid @mention with no sticky prompts user to choose', async () => {
207
- const msg = createDiscordMessage('@UnknownBot do something', { guildId: 'g1', channelId: 'c1' });
208
- await service._handleMessage(msg);
209
-
210
- // Should not route
211
- expect(orchestrator.processRequest).not.toHaveBeenCalled();
212
- // Should prompt user
213
- expect(msg.reply).toHaveBeenCalledTimes(1);
214
- expect(msg.reply.mock.calls[0][0]).toContain('@Coder');
215
- expect(msg.reply.mock.calls[0][0]).toContain('@Reviewer');
216
- });
217
- });
218
-
219
- // ========================================================================
220
- // FLOW 3: Multi-channel isolation
221
- // Agent responds to channel A only — not channel B even if mapped to both
222
- // ========================================================================
223
-
224
- describe('Flow: Multi-channel response isolation', () => {
225
- let service, agentPool;
226
- const channelA = createMockChannel('chA');
227
- const channelB = createMockChannel('chB');
228
-
229
- beforeEach(() => {
230
- service = createService();
231
- agentPool = createMockAgentPool({
232
- 'agent-1': { id: 'agent-1', name: 'SharedBot' }
233
- });
234
- service.setAgentPool(agentPool);
235
- service.setOrchestrator(createMockOrchestrator());
236
- service.status = DISCORD_STATUS.CONNECTED;
237
- service.client = {
238
- channels: {
239
- fetch: jest.fn().mockImplementation(id => {
240
- if (id === 'chA') return Promise.resolve(channelA);
241
- if (id === 'chB') return Promise.resolve(channelB);
242
- return Promise.resolve(null);
243
- })
244
- }
245
- };
246
-
247
- // Same agent mapped to TWO channels
248
- service.channelMappings = {
249
- 'g1:chA': ['agent-1'],
250
- 'g1:chB': ['agent-1']
251
- };
252
-
253
- channelA.send.mockClear();
254
- channelB.send.mockClear();
255
- });
256
-
257
- test('response only goes to channel where user initiated', async () => {
258
- // User sends message in channel A
259
- const msg = createDiscordMessage('hello', { guildId: 'g1', channelId: 'chA' });
260
- await service._handleMessage(msg);
261
-
262
- // Agent responds via broadcast (wrapped for external relay)
263
- await service._handleBroadcastEvent('discord-g1-chA', {
264
- type: 'stream_complete',
265
- agentId: 'agent-1',
266
- content: '<external>Hi there!</external>'
267
- });
268
-
269
- // Channel A should get the response
270
- expect(channelA.send).toHaveBeenCalled();
271
- // Channel B should NOT
272
- expect(channelB.send).not.toHaveBeenCalled();
273
- });
274
-
275
- test('separate conversations in separate channels stay isolated', async () => {
276
- // User sends in channel A
277
- const msgA = createDiscordMessage('question A', { guildId: 'g1', channelId: 'chA' });
278
- await service._handleMessage(msgA);
279
-
280
- // Different user sends in channel B
281
- const msgB = createDiscordMessage('question B', { guildId: 'g1', channelId: 'chB' });
282
- await service._handleMessage(msgB);
283
-
284
- // Agent responds (to question A) — wrapped for external relay
285
- await service._relayAgentResponse({
286
- agentId: 'agent-1',
287
- content: '<external>Answer A</external>'
288
- });
289
-
290
- // Both channels should get the response (both have recent interaction)
291
- expect(channelA.send).toHaveBeenCalled();
292
- expect(channelB.send).toHaveBeenCalled();
293
-
294
- // But if we clear channel A's interaction (expired)
295
- channelA.send.mockClear();
296
- channelB.send.mockClear();
297
- const keyA = 'agent-1:g1:chA';
298
- const interactionA = service.recentInteractions.get(keyA);
299
- if (interactionA) interactionA.timestamp = Date.now() - 31 * 60 * 1000; // expire it
300
-
301
- await service._relayAgentResponse({
302
- agentId: 'agent-1',
303
- content: '<external>Answer B</external>'
304
- });
305
-
306
- // Only channel B should get it now
307
- expect(channelA.send).not.toHaveBeenCalled();
308
- expect(channelB.send).toHaveBeenCalled();
309
- });
310
- });
311
-
312
- // ========================================================================
313
- // FLOW 4: Broadcast chain integrity (multiple services wrapping)
314
- // ========================================================================
315
-
316
- describe('Flow: Broadcast chain integrity', () => {
317
- test('Discord and Telegram can both wrap broadcastToSession without breaking each other', () => {
318
- const originalCalls = [];
319
- const telegramCalls = [];
320
- const discordCalls = [];
321
-
322
- const wsManager = {
323
- broadcastToSession: jest.fn((sid, msg) => originalCalls.push({ sid, msg }))
324
- };
325
-
326
- // Simulate Telegram wrapping first (like real init order)
327
- const telegramOriginal = wsManager.broadcastToSession.bind(wsManager);
328
- wsManager.broadcastToSession = (sid, msg) => {
329
- telegramOriginal(sid, msg);
330
- telegramCalls.push({ sid, msg });
331
- };
332
-
333
- // Now Discord wraps on top
334
- const service = createService();
335
- service._interceptBroadcasts(wsManager);
336
-
337
- // Fire a broadcast
338
- wsManager.broadcastToSession('s1', { type: 'stream_complete', agentId: 'a1', content: 'hello' });
339
-
340
- // Original should be called (through the chain)
341
- expect(originalCalls).toHaveLength(1);
342
- // Telegram should see it
343
- expect(telegramCalls).toHaveLength(1);
344
- // Discord's _handleBroadcastEvent should also have been called
345
- // (verified by the fact that the chain didn't throw)
346
-
347
- // Fire another all three layers still work
348
- wsManager.broadcastToSession('s2', { type: 'message_added' });
349
- expect(originalCalls).toHaveLength(2);
350
- expect(telegramCalls).toHaveLength(2);
351
- });
352
- });
353
-
354
- // ========================================================================
355
- // FLOW 5: Config persistence round-trip
356
- // ========================================================================
357
-
358
- describe('Flow: Config persistence round-trip', () => {
359
- test('assign agents save → reload → mappings restored → routing works', async () => {
360
- const service1 = createService();
361
- service1.setOrchestrator(createMockOrchestrator());
362
- service1.setAgentPool(createMockAgentPool({
363
- 'a1': { id: 'a1', name: 'Bot1' }
364
- }));
365
-
366
- // Step 1: Assign agents to channels
367
- await service1.assignAgentToChannel('g1:c1', 'a1');
368
- await service1.assignAgentToChannel('g1:c2', 'a1');
369
- service1.knownGuilds = { g1: { name: 'TestServer' } };
370
- service1.knownChannels = {
371
- 'g1:c1': { name: 'general', guildName: 'TestServer' },
372
- 'g1:c2': { name: 'dev', guildName: 'TestServer' }
373
- };
374
- await service1._saveConfig();
375
-
376
- // Step 2: Capture what was written to disk
377
- const lastWriteCall = mockFs.writeFile.mock.calls[mockFs.writeFile.mock.calls.length - 1];
378
- const savedJson = lastWriteCall[1];
379
-
380
- // Step 3: Create new service instance and load the saved config
381
- mockFs.readFile.mockResolvedValueOnce(savedJson);
382
- const service2 = createService();
383
- await service2._loadConfig();
384
-
385
- // Step 4: Verify mappings are restored
386
- expect(service2.channelMappings).toEqual({
387
- 'g1:c1': ['a1'],
388
- 'g1:c2': ['a1']
389
- });
390
- expect(service2.knownGuilds.g1.name).toBe('TestServer');
391
- expect(service2.knownChannels['g1:c1'].name).toBe('general');
392
-
393
- // Step 5: Verify routing works with restored mappings
394
- service2.setOrchestrator(createMockOrchestrator());
395
- service2.setAgentPool(createMockAgentPool({
396
- 'a1': { id: 'a1', name: 'Bot1' }
397
- }));
398
- service2.status = DISCORD_STATUS.CONNECTED;
399
-
400
- const msg = createDiscordMessage('hello', { guildId: 'g1', channelId: 'c1' });
401
- await service2._handleMessage(msg);
402
-
403
- expect(service2.orchestrator.processRequest).toHaveBeenCalledTimes(1);
404
- expect(service2.orchestrator.processRequest.mock.calls[0][0].payload.agentId).toBe('a1');
405
- });
406
- });
407
-
408
- // ========================================================================
409
- // FLOW 6: Agent deletion / stale mapping handling
410
- // ========================================================================
411
-
412
- describe('Flow: Agent lifecycle and stale mappings', () => {
413
- test('agent deleted from pool — routed message fails gracefully', async () => {
414
- const service = createService();
415
- const orchestrator = createMockOrchestrator();
416
- orchestrator.processRequest.mockRejectedValueOnce(new Error('Agent not found'));
417
-
418
- service.setOrchestrator(orchestrator);
419
- service.setAgentPool(createMockAgentPool({})); // empty — agent gone
420
- service.status = DISCORD_STATUS.CONNECTED;
421
- service.channelMappings = { 'g1:c1': ['deleted-agent'] };
422
-
423
- const msg = createDiscordMessage('hello', { guildId: 'g1', channelId: 'c1' });
424
- await service._handleMessage(msg);
425
-
426
- // Should attempt to route but handle error gracefully
427
- expect(orchestrator.processRequest).toHaveBeenCalled();
428
- // Should reply with error
429
- expect(msg.reply).toHaveBeenCalledTimes(1);
430
- expect(msg.reply.mock.calls[0][0]).toContain('Failed');
431
- });
432
-
433
- test('guild removal cleans up all mappings for that guild', async () => {
434
- const service = createService();
435
- service.channelMappings = {
436
- 'guild-A:c1': ['a1'],
437
- 'guild-A:c2': ['a2'],
438
- 'guild-B:c3': ['a3']
439
- };
440
- service.knownGuilds = {
441
- 'guild-A': { name: 'ServerA' },
442
- 'guild-B': { name: 'ServerB' }
443
- };
444
- service.knownChannels = {
445
- 'guild-A:c1': { name: 'gen' },
446
- 'guild-A:c2': { name: 'dev' },
447
- 'guild-B:c3': { name: 'main' }
448
- };
449
-
450
- // Simulate guildDelete event handler logic
451
- const guildId = 'guild-A';
452
- delete service.knownGuilds[guildId];
453
- for (const key of Object.keys(service.channelMappings)) {
454
- if (key.startsWith(`${guildId}:`)) {
455
- delete service.channelMappings[key];
456
- delete service.knownChannels[key];
457
- }
458
- }
459
-
460
- // guild-A channels should be gone
461
- expect(service.channelMappings['guild-A:c1']).toBeUndefined();
462
- expect(service.channelMappings['guild-A:c2']).toBeUndefined();
463
- expect(service.knownChannels['guild-A:c1']).toBeUndefined();
464
-
465
- // guild-B should be untouched
466
- expect(service.channelMappings['guild-B:c3']).toEqual(['a3']);
467
- expect(service.knownGuilds['guild-B'].name).toBe('ServerB');
468
- });
469
- });
470
-
471
- // ========================================================================
472
- // FLOW 7: Prompt relay flow
473
- // ========================================================================
474
-
475
- describe('Flow: User prompt relay', () => {
476
- test('user_prompt_request broadcast relayed to channel with recent interaction', async () => {
477
- const service = createService();
478
- const mockCh = createMockChannel('ch-1');
479
- service.status = DISCORD_STATUS.CONNECTED;
480
- service.client = {
481
- channels: { fetch: jest.fn().mockResolvedValue(mockCh) }
482
- };
483
- service.agentPool = createMockAgentPool({
484
- 'a1': { id: 'a1', name: 'Worker' }
485
- });
486
-
487
- // Record a recent interaction
488
- service.recentInteractions.set('a1:g1:ch-1', {
489
- channelKey: 'g1:ch-1',
490
- channelId: 'ch-1',
491
- guildId: 'g1',
492
- timestamp: Date.now()
493
- });
494
-
495
- // Simulate prompt request broadcast
496
- await service._handleBroadcastEvent('session', {
497
- type: 'user_prompt_request',
498
- data: { agentId: 'a1', prompt: 'Please provide the API key' }
499
- });
500
-
501
- expect(mockCh.send).toHaveBeenCalledTimes(1);
502
- const sent = mockCh.send.mock.calls[0][0];
503
- expect(sent).toContain('Input needed');
504
- expect(sent).toContain('API key');
505
- });
506
- });
507
-
508
- // ========================================================================
509
- // FLOW 8: Thread-level routing
510
- // ========================================================================
511
-
512
- describe('Flow: Thread-level routing', () => {
513
- let service, orchestrator;
514
-
515
- beforeEach(() => {
516
- service = createService();
517
- orchestrator = createMockOrchestrator();
518
- service.setOrchestrator(orchestrator);
519
- service.setAgentPool(createMockAgentPool({
520
- 'a1': { id: 'a1', name: 'Alpha' },
521
- 'a2': { id: 'a2', name: 'Beta' }
522
- }));
523
- service.status = DISCORD_STATUS.CONNECTED;
524
- });
525
-
526
- test('thread with its own mapping uses thread-specific agents', async () => {
527
- // Channel has agent Alpha, but thread has agent Beta
528
- service.channelMappings = {
529
- 'g1:parent-ch': ['a1'],
530
- 'g1:thread-1': ['a2']
531
- };
532
-
533
- // Message in the thread
534
- const msg = createDiscordMessage('hello from thread', {
535
- guildId: 'g1',
536
- channelId: 'thread-1',
537
- parentId: 'parent-ch'
538
- });
539
- await service._handleMessage(msg);
540
-
541
- // Should route to Beta (thread-specific), NOT Alpha (parent)
542
- expect(orchestrator.processRequest).toHaveBeenCalledTimes(1);
543
- expect(orchestrator.processRequest.mock.calls[0][0].payload.agentId).toBe('a2');
544
- });
545
-
546
- test('thread without its own mapping falls back to parent channel mapping', async () => {
547
- // Only the parent channel has an agent assigned
548
- service.channelMappings = {
549
- 'g1:parent-ch': ['a1']
550
- };
551
-
552
- // Message in a thread under that channel
553
- const msg = createDiscordMessage('hello from thread', {
554
- guildId: 'g1',
555
- channelId: 'thread-2',
556
- parentId: 'parent-ch'
557
- });
558
- await service._handleMessage(msg);
559
-
560
- // Should fall back to Alpha (parent channel agent)
561
- expect(orchestrator.processRequest).toHaveBeenCalledTimes(1);
562
- expect(orchestrator.processRequest.mock.calls[0][0].payload.agentId).toBe('a1');
563
- });
564
-
565
- test('thread with no mapping and unmapped parent is ignored', async () => {
566
- service.channelMappings = {}; // nothing mapped
567
-
568
- const msg = createDiscordMessage('hello', {
569
- guildId: 'g1',
570
- channelId: 'thread-3',
571
- parentId: 'unmapped-ch'
572
- });
573
- await service._handleMessage(msg);
574
-
575
- expect(orchestrator.processRequest).not.toHaveBeenCalled();
576
- });
577
-
578
- test('response to thread stays in thread, not parent channel', async () => {
579
- const threadChannel = createMockChannel('thread-1');
580
- const parentChannel = createMockChannel('parent-ch');
581
-
582
- service.client = {
583
- channels: {
584
- fetch: jest.fn().mockImplementation(id => {
585
- if (id === 'thread-1') return Promise.resolve(threadChannel);
586
- if (id === 'parent-ch') return Promise.resolve(parentChannel);
587
- return Promise.resolve(null);
588
- })
589
- }
590
- };
591
- service.channelMappings = { 'g1:parent-ch': ['a1'] };
592
-
593
- // User messages in thread (falls back to parent mapping)
594
- const msg = createDiscordMessage('question', {
595
- guildId: 'g1',
596
- channelId: 'thread-1',
597
- parentId: 'parent-ch'
598
- });
599
- await service._handleMessage(msg);
600
-
601
- // Agent responds — wrapped for external relay
602
- await service._handleBroadcastEvent('session', {
603
- type: 'stream_complete',
604
- agentId: 'a1',
605
- content: '<external>answer</external>'
606
- });
607
-
608
- // Response should go to the thread, not the parent
609
- expect(threadChannel.send).toHaveBeenCalled();
610
- expect(parentChannel.send).not.toHaveBeenCalled();
611
- });
612
-
613
- test('sticky agent is per-thread, not shared with parent channel', async () => {
614
- service.channelMappings = {
615
- 'g1:parent-ch': ['a1', 'a2'],
616
- 'g1:thread-1': ['a1', 'a2']
617
- };
618
-
619
- // In parent channel: address Alpha
620
- const msg1 = createDiscordMessage('@Alpha do X', { guildId: 'g1', channelId: 'parent-ch' });
621
- await service._handleMessage(msg1);
622
- expect(orchestrator.processRequest.mock.calls[0][0].payload.agentId).toBe('a1');
623
-
624
- // In thread: address Beta
625
- const msg2 = createDiscordMessage('@Beta do Y', { guildId: 'g1', channelId: 'thread-1', parentId: 'parent-ch' });
626
- await service._handleMessage(msg2);
627
- expect(orchestrator.processRequest.mock.calls[1][0].payload.agentId).toBe('a2');
628
-
629
- // Follow-up in parent (no mention) sticky should be Alpha
630
- const msg3 = createDiscordMessage('follow up', { guildId: 'g1', channelId: 'parent-ch' });
631
- await service._handleMessage(msg3);
632
- expect(orchestrator.processRequest.mock.calls[2][0].payload.agentId).toBe('a1');
633
-
634
- // Follow-up in thread (no mention) sticky should be Beta
635
- const msg4 = createDiscordMessage('thread follow up', { guildId: 'g1', channelId: 'thread-1', parentId: 'parent-ch' });
636
- await service._handleMessage(msg4);
637
- expect(orchestrator.processRequest.mock.calls[3][0].payload.agentId).toBe('a2');
638
- });
639
- });
1
+ /**
2
+ * Discord Service — Integration / Flow Tests
3
+ *
4
+ * Tests complete flows across components rather than isolated units:
5
+ * - Full message round-trip (Discord msg → agent → response back to channel)
6
+ * - API route integration (HTTP → service → response)
7
+ * - Broadcast chain integrity (multiple services wrapping broadcastToSession)
8
+ * - Config persistence round-trip (save → reload → verify state)
9
+ * - Multi-channel isolation (agent responses only go to originating channel)
10
+ * - Agent lifecycle (deletion cleans up stale mappings)
11
+ */
12
+
13
+ import { jest, describe, test, expect, beforeEach } from '@jest/globals';
14
+ import { createMockLogger } from '../../__test-utils__/mockFactories.js';
15
+
16
+ // Mock fs for config persistence
17
+ const mockFs = {
18
+ mkdir: jest.fn().mockResolvedValue(undefined),
19
+ readFile: jest.fn().mockRejectedValue(new Error('ENOENT')),
20
+ writeFile: jest.fn().mockResolvedValue(undefined)
21
+ };
22
+
23
+ jest.unstable_mockModule('fs', () => ({
24
+ promises: mockFs
25
+ }));
26
+
27
+ jest.unstable_mockModule('../../utilities/userDataDir.js', () => ({
28
+ getUserDataPaths: () => ({ base: '/mock/data' }),
29
+ ensureUserDataDirs: jest.fn().mockResolvedValue(undefined)
30
+ }));
31
+
32
+ const { DiscordService, DISCORD_STATUS } = await import('../discordService.js');
33
+
34
+ // --- Helpers ---
35
+
36
+ function createService() {
37
+ const logger = createMockLogger();
38
+ const service = new DiscordService(logger);
39
+ return service;
40
+ }
41
+
42
+ function createMockOrchestrator() {
43
+ return {
44
+ processRequest: jest.fn().mockResolvedValue({ success: true })
45
+ };
46
+ }
47
+
48
+ function createMockAgentPool(agents = {}) {
49
+ return {
50
+ getAgent: jest.fn().mockImplementation(id => Promise.resolve(agents[id] || null)),
51
+ getAllAgents: jest.fn().mockReturnValue(Object.values(agents))
52
+ };
53
+ }
54
+
55
+ function createMockChannel(id = 'c1') {
56
+ return {
57
+ id,
58
+ send: jest.fn().mockResolvedValue({}),
59
+ sendTyping: jest.fn().mockResolvedValue(undefined)
60
+ };
61
+ }
62
+
63
+ function createDiscordMessage(content, opts = {}) {
64
+ const guildId = opts.guildId || 'guild-1';
65
+ const channelId = opts.channelId || 'ch-1';
66
+ return {
67
+ content,
68
+ author: { bot: opts.bot || false, id: opts.userId || 'user-42' },
69
+ guild: opts.noDM ? null : { id: guildId },
70
+ channel: {
71
+ id: channelId,
72
+ parentId: opts.parentId || null, // thread parent channel ID
73
+ send: jest.fn().mockResolvedValue({}),
74
+ sendTyping: jest.fn().mockResolvedValue(undefined)
75
+ },
76
+ reply: jest.fn().mockResolvedValue({})
77
+ };
78
+ }
79
+
80
+ // ========================================================================
81
+ // FLOW 1: Full message round-trip
82
+ // Discord message → _handleMessage → orchestrator → broadcast → channel.send
83
+ // ========================================================================
84
+
85
+ describe('Flow: Full message round-trip', () => {
86
+ let service, orchestrator, agentPool, mockChannel;
87
+
88
+ beforeEach(() => {
89
+ service = createService();
90
+ orchestrator = createMockOrchestrator();
91
+ agentPool = createMockAgentPool({
92
+ 'agent-alpha': { id: 'agent-alpha', name: 'Alpha' }
93
+ });
94
+ mockChannel = createMockChannel('ch-1');
95
+
96
+ service.setOrchestrator(orchestrator);
97
+ service.setAgentPool(agentPool);
98
+ service.status = DISCORD_STATUS.CONNECTED;
99
+ service.client = {
100
+ channels: { fetch: jest.fn().mockResolvedValue(mockChannel) }
101
+ };
102
+
103
+ // Map agent to channel
104
+ service.channelMappings = { 'guild-1:ch-1': ['agent-alpha'] };
105
+ });
106
+
107
+ test('user sends message → agent processes → response appears in same channel', async () => {
108
+ // Step 1: Incoming Discord message
109
+ const msg = createDiscordMessage('explain recursion');
110
+ await service._handleMessage(msg);
111
+
112
+ // Step 2: Verify orchestrator received the request
113
+ expect(orchestrator.processRequest).toHaveBeenCalledTimes(1);
114
+ const request = orchestrator.processRequest.mock.calls[0][0];
115
+ expect(request.interface).toBe('discord');
116
+ expect(request.sessionId).toBe('discord-guild-1-ch-1');
117
+ expect(request.payload.agentId).toBe('agent-alpha');
118
+ expect(request.payload.message).toBe('explain recursion');
119
+
120
+ // Step 3: Simulate broadcast from agentScheduler (what happens after AI responds)
121
+ await service._handleBroadcastEvent('discord-guild-1-ch-1', {
122
+ type: 'stream_complete',
123
+ agentId: 'agent-alpha',
124
+ content: '<external>Recursion is when a function calls itself.</external>'
125
+ });
126
+
127
+ // Step 4: Verify response was sent to the correct channel
128
+ expect(service.client.channels.fetch).toHaveBeenCalledWith('ch-1');
129
+ expect(mockChannel.send).toHaveBeenCalledTimes(1);
130
+ const sent = mockChannel.send.mock.calls[0][0];
131
+ expect(sent).toContain('Alpha');
132
+ expect(sent).toContain('Recursion is when a function calls itself.');
133
+ });
134
+
135
+ test('full round-trip with long response triggers message splitting', async () => {
136
+ const msg = createDiscordMessage('write a long essay');
137
+ await service._handleMessage(msg);
138
+
139
+ // Simulate a response longer than Discord's limit, wrapped for relay
140
+ const longContent = 'A'.repeat(3500);
141
+ await service._handleBroadcastEvent('discord-guild-1-ch-1', {
142
+ type: 'stream_complete',
143
+ agentId: 'agent-alpha',
144
+ content: `<external>${longContent}</external>`
145
+ });
146
+
147
+ // Should have split into multiple messages
148
+ expect(mockChannel.send).toHaveBeenCalled();
149
+ const totalSent = mockChannel.send.mock.calls.map(c => c[0]).join('');
150
+ expect(totalSent).toContain('Alpha');
151
+ expect(totalSent.length).toBeGreaterThanOrEqual(3500);
152
+ });
153
+ });
154
+
155
+ // ========================================================================
156
+ // FLOW 2: Multi-agent channel routing
157
+ // ========================================================================
158
+
159
+ describe('Flow: Multi-agent channel routing', () => {
160
+ let service, orchestrator, agentPool;
161
+
162
+ beforeEach(() => {
163
+ service = createService();
164
+ orchestrator = createMockOrchestrator();
165
+ agentPool = createMockAgentPool({
166
+ 'a1': { id: 'a1', name: 'Coder' },
167
+ 'a2': { id: 'a2', name: 'Reviewer' }
168
+ });
169
+
170
+ service.setOrchestrator(orchestrator);
171
+ service.setAgentPool(agentPool);
172
+ service.status = DISCORD_STATUS.CONNECTED;
173
+ service.channelMappings = { 'g1:c1': ['a1', 'a2'] };
174
+ });
175
+
176
+ test('@mention routes to correct agent, then follow-up uses sticky', async () => {
177
+ // First message: explicit @mention
178
+ const msg1 = createDiscordMessage('@Coder fix the login bug', { guildId: 'g1', channelId: 'c1' });
179
+ await service._handleMessage(msg1);
180
+
181
+ expect(orchestrator.processRequest).toHaveBeenCalledTimes(1);
182
+ expect(orchestrator.processRequest.mock.calls[0][0].payload.agentId).toBe('a1');
183
+ expect(orchestrator.processRequest.mock.calls[0][0].payload.message).toBe('fix the login bug');
184
+
185
+ // Second message: no mention — should use sticky (last addressed agent)
186
+ const msg2 = createDiscordMessage('also add error handling', { guildId: 'g1', channelId: 'c1' });
187
+ await service._handleMessage(msg2);
188
+
189
+ expect(orchestrator.processRequest).toHaveBeenCalledTimes(2);
190
+ expect(orchestrator.processRequest.mock.calls[1][0].payload.agentId).toBe('a1'); // sticky to Coder
191
+
192
+ // Third message: switch to different agent via @mention
193
+ const msg3 = createDiscordMessage('@Reviewer review the PR', { guildId: 'g1', channelId: 'c1' });
194
+ await service._handleMessage(msg3);
195
+
196
+ expect(orchestrator.processRequest).toHaveBeenCalledTimes(3);
197
+ expect(orchestrator.processRequest.mock.calls[2][0].payload.agentId).toBe('a2'); // switched to Reviewer
198
+
199
+ // Fourth message: no mention — sticky should now be Reviewer
200
+ const msg4 = createDiscordMessage('any concerns?', { guildId: 'g1', channelId: 'c1' });
201
+ await service._handleMessage(msg4);
202
+
203
+ expect(orchestrator.processRequest.mock.calls[3][0].payload.agentId).toBe('a2');
204
+ });
205
+
206
+ test('invalid @mention with no sticky prompts user to choose', async () => {
207
+ const msg = createDiscordMessage('@UnknownBot do something', { guildId: 'g1', channelId: 'c1' });
208
+ await service._handleMessage(msg);
209
+
210
+ // Should not route
211
+ expect(orchestrator.processRequest).not.toHaveBeenCalled();
212
+ // Should prompt user
213
+ expect(msg.reply).toHaveBeenCalledTimes(1);
214
+ expect(msg.reply.mock.calls[0][0]).toContain('@Coder');
215
+ expect(msg.reply.mock.calls[0][0]).toContain('@Reviewer');
216
+ });
217
+ });
218
+
219
+ // ========================================================================
220
+ // FLOW 3: Multi-channel isolation
221
+ // Agent responds to channel A only — not channel B even if mapped to both
222
+ // ========================================================================
223
+
224
+ describe('Flow: Multi-channel response isolation', () => {
225
+ let service, agentPool;
226
+ const channelA = createMockChannel('chA');
227
+ const channelB = createMockChannel('chB');
228
+
229
+ beforeEach(() => {
230
+ service = createService();
231
+ agentPool = createMockAgentPool({
232
+ 'agent-1': { id: 'agent-1', name: 'SharedBot' }
233
+ });
234
+ service.setAgentPool(agentPool);
235
+ service.setOrchestrator(createMockOrchestrator());
236
+ service.status = DISCORD_STATUS.CONNECTED;
237
+ service.client = {
238
+ channels: {
239
+ fetch: jest.fn().mockImplementation(id => {
240
+ if (id === 'chA') return Promise.resolve(channelA);
241
+ if (id === 'chB') return Promise.resolve(channelB);
242
+ return Promise.resolve(null);
243
+ })
244
+ }
245
+ };
246
+
247
+ // Same agent mapped to TWO channels
248
+ service.channelMappings = {
249
+ 'g1:chA': ['agent-1'],
250
+ 'g1:chB': ['agent-1']
251
+ };
252
+
253
+ channelA.send.mockClear();
254
+ channelB.send.mockClear();
255
+ });
256
+
257
+ test('response only goes to channel where user initiated', async () => {
258
+ // User sends message in channel A
259
+ const msg = createDiscordMessage('hello', { guildId: 'g1', channelId: 'chA' });
260
+ await service._handleMessage(msg);
261
+
262
+ // Agent responds via broadcast (wrapped for external relay)
263
+ await service._handleBroadcastEvent('discord-g1-chA', {
264
+ type: 'stream_complete',
265
+ agentId: 'agent-1',
266
+ content: '<external>Hi there!</external>'
267
+ });
268
+
269
+ // Channel A should get the response
270
+ expect(channelA.send).toHaveBeenCalled();
271
+ // Channel B should NOT
272
+ expect(channelB.send).not.toHaveBeenCalled();
273
+ });
274
+
275
+ test('separate conversations in separate channels stay isolated', async () => {
276
+ // User sends in channel A
277
+ const msgA = createDiscordMessage('question A', { guildId: 'g1', channelId: 'chA' });
278
+ await service._handleMessage(msgA);
279
+
280
+ // Different user sends in channel B
281
+ const msgB = createDiscordMessage('question B', { guildId: 'g1', channelId: 'chB' });
282
+ await service._handleMessage(msgB);
283
+
284
+ // Agent responds (to question A) — wrapped for external relay
285
+ await service._relayAgentResponse({
286
+ agentId: 'agent-1',
287
+ content: '<external>Answer A</external>'
288
+ });
289
+
290
+ // Both channels should get the response (both have recent interaction)
291
+ expect(channelA.send).toHaveBeenCalled();
292
+ expect(channelB.send).toHaveBeenCalled();
293
+
294
+ // But if we clear channel A's interaction (expired)
295
+ channelA.send.mockClear();
296
+ channelB.send.mockClear();
297
+ const keyA = 'agent-1:g1:chA';
298
+ const interactionA = service.recentInteractions.get(keyA);
299
+ if (interactionA) interactionA.timestamp = Date.now() - 31 * 60 * 1000; // expire it
300
+
301
+ await service._relayAgentResponse({
302
+ agentId: 'agent-1',
303
+ content: '<external>Answer B</external>'
304
+ });
305
+
306
+ // Only channel B should get it now
307
+ expect(channelA.send).not.toHaveBeenCalled();
308
+ expect(channelB.send).toHaveBeenCalled();
309
+ });
310
+ });
311
+
312
+ // ========================================================================
313
+ // FLOW 4: Broadcast chain integrity (multiple services wrapping)
314
+ // ========================================================================
315
+
316
+ describe('Flow: Broadcast chain integrity', () => {
317
+ test('Discord and Telegram can both wrap broadcastToSession without breaking each other', () => {
318
+ const originalCalls = [];
319
+ const telegramCalls = [];
320
+
321
+ const wsManager = {
322
+ broadcastToSession: jest.fn((sid, msg) => originalCalls.push({ sid, msg }))
323
+ };
324
+
325
+ // Simulate Telegram wrapping first (like real init order)
326
+ const telegramOriginal = wsManager.broadcastToSession.bind(wsManager);
327
+ wsManager.broadcastToSession = (sid, msg) => {
328
+ telegramOriginal(sid, msg);
329
+ telegramCalls.push({ sid, msg });
330
+ };
331
+
332
+ // Now Discord wraps on top
333
+ const service = createService();
334
+ service._interceptBroadcasts(wsManager);
335
+
336
+ // Fire a broadcast
337
+ wsManager.broadcastToSession('s1', { type: 'stream_complete', agentId: 'a1', content: 'hello' });
338
+
339
+ // Original should be called (through the chain)
340
+ expect(originalCalls).toHaveLength(1);
341
+ // Telegram should see it
342
+ expect(telegramCalls).toHaveLength(1);
343
+ // Discord's _handleBroadcastEvent should also have been called
344
+ // (verified by the fact that the chain didn't throw)
345
+
346
+ // Fire another — all three layers still work
347
+ wsManager.broadcastToSession('s2', { type: 'message_added' });
348
+ expect(originalCalls).toHaveLength(2);
349
+ expect(telegramCalls).toHaveLength(2);
350
+ });
351
+ });
352
+
353
+ // ========================================================================
354
+ // FLOW 5: Config persistence round-trip
355
+ // ========================================================================
356
+
357
+ describe('Flow: Config persistence round-trip', () => {
358
+ test('assign agents save → reload → mappings restored → routing works', async () => {
359
+ const service1 = createService();
360
+ service1.setOrchestrator(createMockOrchestrator());
361
+ service1.setAgentPool(createMockAgentPool({
362
+ 'a1': { id: 'a1', name: 'Bot1' }
363
+ }));
364
+
365
+ // Step 1: Assign agents to channels
366
+ await service1.assignAgentToChannel('g1:c1', 'a1');
367
+ await service1.assignAgentToChannel('g1:c2', 'a1');
368
+ service1.knownGuilds = { g1: { name: 'TestServer' } };
369
+ service1.knownChannels = {
370
+ 'g1:c1': { name: 'general', guildName: 'TestServer' },
371
+ 'g1:c2': { name: 'dev', guildName: 'TestServer' }
372
+ };
373
+ await service1._saveConfig();
374
+
375
+ // Step 2: Capture what was written to disk
376
+ const lastWriteCall = mockFs.writeFile.mock.calls[mockFs.writeFile.mock.calls.length - 1];
377
+ const savedJson = lastWriteCall[1];
378
+
379
+ // Step 3: Create new service instance and load the saved config
380
+ mockFs.readFile.mockResolvedValueOnce(savedJson);
381
+ const service2 = createService();
382
+ await service2._loadConfig();
383
+
384
+ // Step 4: Verify mappings are restored
385
+ expect(service2.channelMappings).toEqual({
386
+ 'g1:c1': ['a1'],
387
+ 'g1:c2': ['a1']
388
+ });
389
+ expect(service2.knownGuilds.g1.name).toBe('TestServer');
390
+ expect(service2.knownChannels['g1:c1'].name).toBe('general');
391
+
392
+ // Step 5: Verify routing works with restored mappings
393
+ service2.setOrchestrator(createMockOrchestrator());
394
+ service2.setAgentPool(createMockAgentPool({
395
+ 'a1': { id: 'a1', name: 'Bot1' }
396
+ }));
397
+ service2.status = DISCORD_STATUS.CONNECTED;
398
+
399
+ const msg = createDiscordMessage('hello', { guildId: 'g1', channelId: 'c1' });
400
+ await service2._handleMessage(msg);
401
+
402
+ expect(service2.orchestrator.processRequest).toHaveBeenCalledTimes(1);
403
+ expect(service2.orchestrator.processRequest.mock.calls[0][0].payload.agentId).toBe('a1');
404
+ });
405
+ });
406
+
407
+ // ========================================================================
408
+ // FLOW 6: Agent deletion / stale mapping handling
409
+ // ========================================================================
410
+
411
+ describe('Flow: Agent lifecycle and stale mappings', () => {
412
+ test('agent deleted from pool routed message fails gracefully', async () => {
413
+ const service = createService();
414
+ const orchestrator = createMockOrchestrator();
415
+ orchestrator.processRequest.mockRejectedValueOnce(new Error('Agent not found'));
416
+
417
+ service.setOrchestrator(orchestrator);
418
+ service.setAgentPool(createMockAgentPool({})); // empty — agent gone
419
+ service.status = DISCORD_STATUS.CONNECTED;
420
+ service.channelMappings = { 'g1:c1': ['deleted-agent'] };
421
+
422
+ const msg = createDiscordMessage('hello', { guildId: 'g1', channelId: 'c1' });
423
+ await service._handleMessage(msg);
424
+
425
+ // Should attempt to route but handle error gracefully
426
+ expect(orchestrator.processRequest).toHaveBeenCalled();
427
+ // Should reply with error
428
+ expect(msg.reply).toHaveBeenCalledTimes(1);
429
+ expect(msg.reply.mock.calls[0][0]).toContain('Failed');
430
+ });
431
+
432
+ test('guild removal cleans up all mappings for that guild', async () => {
433
+ const service = createService();
434
+ service.channelMappings = {
435
+ 'guild-A:c1': ['a1'],
436
+ 'guild-A:c2': ['a2'],
437
+ 'guild-B:c3': ['a3']
438
+ };
439
+ service.knownGuilds = {
440
+ 'guild-A': { name: 'ServerA' },
441
+ 'guild-B': { name: 'ServerB' }
442
+ };
443
+ service.knownChannels = {
444
+ 'guild-A:c1': { name: 'gen' },
445
+ 'guild-A:c2': { name: 'dev' },
446
+ 'guild-B:c3': { name: 'main' }
447
+ };
448
+
449
+ // Simulate guildDelete event handler logic
450
+ const guildId = 'guild-A';
451
+ delete service.knownGuilds[guildId];
452
+ for (const key of Object.keys(service.channelMappings)) {
453
+ if (key.startsWith(`${guildId}:`)) {
454
+ delete service.channelMappings[key];
455
+ delete service.knownChannels[key];
456
+ }
457
+ }
458
+
459
+ // guild-A channels should be gone
460
+ expect(service.channelMappings['guild-A:c1']).toBeUndefined();
461
+ expect(service.channelMappings['guild-A:c2']).toBeUndefined();
462
+ expect(service.knownChannels['guild-A:c1']).toBeUndefined();
463
+
464
+ // guild-B should be untouched
465
+ expect(service.channelMappings['guild-B:c3']).toEqual(['a3']);
466
+ expect(service.knownGuilds['guild-B'].name).toBe('ServerB');
467
+ });
468
+ });
469
+
470
+ // ========================================================================
471
+ // FLOW 7: Prompt relay flow
472
+ // ========================================================================
473
+
474
+ describe('Flow: User prompt relay', () => {
475
+ test('user_prompt_request broadcast relayed to channel with recent interaction', async () => {
476
+ const service = createService();
477
+ const mockCh = createMockChannel('ch-1');
478
+ service.status = DISCORD_STATUS.CONNECTED;
479
+ service.client = {
480
+ channels: { fetch: jest.fn().mockResolvedValue(mockCh) }
481
+ };
482
+ service.agentPool = createMockAgentPool({
483
+ 'a1': { id: 'a1', name: 'Worker' }
484
+ });
485
+
486
+ // Record a recent interaction
487
+ service.recentInteractions.set('a1:g1:ch-1', {
488
+ channelKey: 'g1:ch-1',
489
+ channelId: 'ch-1',
490
+ guildId: 'g1',
491
+ timestamp: Date.now()
492
+ });
493
+
494
+ // Simulate prompt request broadcast
495
+ await service._handleBroadcastEvent('session', {
496
+ type: 'user_prompt_request',
497
+ data: { agentId: 'a1', prompt: 'Please provide the API key' }
498
+ });
499
+
500
+ expect(mockCh.send).toHaveBeenCalledTimes(1);
501
+ const sent = mockCh.send.mock.calls[0][0];
502
+ expect(sent).toContain('Input needed');
503
+ expect(sent).toContain('API key');
504
+ });
505
+ });
506
+
507
+ // ========================================================================
508
+ // FLOW 8: Thread-level routing
509
+ // ========================================================================
510
+
511
+ describe('Flow: Thread-level routing', () => {
512
+ let service, orchestrator;
513
+
514
+ beforeEach(() => {
515
+ service = createService();
516
+ orchestrator = createMockOrchestrator();
517
+ service.setOrchestrator(orchestrator);
518
+ service.setAgentPool(createMockAgentPool({
519
+ 'a1': { id: 'a1', name: 'Alpha' },
520
+ 'a2': { id: 'a2', name: 'Beta' }
521
+ }));
522
+ service.status = DISCORD_STATUS.CONNECTED;
523
+ });
524
+
525
+ test('thread with its own mapping uses thread-specific agents', async () => {
526
+ // Channel has agent Alpha, but thread has agent Beta
527
+ service.channelMappings = {
528
+ 'g1:parent-ch': ['a1'],
529
+ 'g1:thread-1': ['a2']
530
+ };
531
+
532
+ // Message in the thread
533
+ const msg = createDiscordMessage('hello from thread', {
534
+ guildId: 'g1',
535
+ channelId: 'thread-1',
536
+ parentId: 'parent-ch'
537
+ });
538
+ await service._handleMessage(msg);
539
+
540
+ // Should route to Beta (thread-specific), NOT Alpha (parent)
541
+ expect(orchestrator.processRequest).toHaveBeenCalledTimes(1);
542
+ expect(orchestrator.processRequest.mock.calls[0][0].payload.agentId).toBe('a2');
543
+ });
544
+
545
+ test('thread without its own mapping falls back to parent channel mapping', async () => {
546
+ // Only the parent channel has an agent assigned
547
+ service.channelMappings = {
548
+ 'g1:parent-ch': ['a1']
549
+ };
550
+
551
+ // Message in a thread under that channel
552
+ const msg = createDiscordMessage('hello from thread', {
553
+ guildId: 'g1',
554
+ channelId: 'thread-2',
555
+ parentId: 'parent-ch'
556
+ });
557
+ await service._handleMessage(msg);
558
+
559
+ // Should fall back to Alpha (parent channel agent)
560
+ expect(orchestrator.processRequest).toHaveBeenCalledTimes(1);
561
+ expect(orchestrator.processRequest.mock.calls[0][0].payload.agentId).toBe('a1');
562
+ });
563
+
564
+ test('thread with no mapping and unmapped parent is ignored', async () => {
565
+ service.channelMappings = {}; // nothing mapped
566
+
567
+ const msg = createDiscordMessage('hello', {
568
+ guildId: 'g1',
569
+ channelId: 'thread-3',
570
+ parentId: 'unmapped-ch'
571
+ });
572
+ await service._handleMessage(msg);
573
+
574
+ expect(orchestrator.processRequest).not.toHaveBeenCalled();
575
+ });
576
+
577
+ test('response to thread stays in thread, not parent channel', async () => {
578
+ const threadChannel = createMockChannel('thread-1');
579
+ const parentChannel = createMockChannel('parent-ch');
580
+
581
+ service.client = {
582
+ channels: {
583
+ fetch: jest.fn().mockImplementation(id => {
584
+ if (id === 'thread-1') return Promise.resolve(threadChannel);
585
+ if (id === 'parent-ch') return Promise.resolve(parentChannel);
586
+ return Promise.resolve(null);
587
+ })
588
+ }
589
+ };
590
+ service.channelMappings = { 'g1:parent-ch': ['a1'] };
591
+
592
+ // User messages in thread (falls back to parent mapping)
593
+ const msg = createDiscordMessage('question', {
594
+ guildId: 'g1',
595
+ channelId: 'thread-1',
596
+ parentId: 'parent-ch'
597
+ });
598
+ await service._handleMessage(msg);
599
+
600
+ // Agent responds — wrapped for external relay
601
+ await service._handleBroadcastEvent('session', {
602
+ type: 'stream_complete',
603
+ agentId: 'a1',
604
+ content: '<external>answer</external>'
605
+ });
606
+
607
+ // Response should go to the thread, not the parent
608
+ expect(threadChannel.send).toHaveBeenCalled();
609
+ expect(parentChannel.send).not.toHaveBeenCalled();
610
+ });
611
+
612
+ test('sticky agent is per-thread, not shared with parent channel', async () => {
613
+ service.channelMappings = {
614
+ 'g1:parent-ch': ['a1', 'a2'],
615
+ 'g1:thread-1': ['a1', 'a2']
616
+ };
617
+
618
+ // In parent channel: address Alpha
619
+ const msg1 = createDiscordMessage('@Alpha do X', { guildId: 'g1', channelId: 'parent-ch' });
620
+ await service._handleMessage(msg1);
621
+ expect(orchestrator.processRequest.mock.calls[0][0].payload.agentId).toBe('a1');
622
+
623
+ // In thread: address Beta
624
+ const msg2 = createDiscordMessage('@Beta do Y', { guildId: 'g1', channelId: 'thread-1', parentId: 'parent-ch' });
625
+ await service._handleMessage(msg2);
626
+ expect(orchestrator.processRequest.mock.calls[1][0].payload.agentId).toBe('a2');
627
+
628
+ // Follow-up in parent (no mention) → sticky should be Alpha
629
+ const msg3 = createDiscordMessage('follow up', { guildId: 'g1', channelId: 'parent-ch' });
630
+ await service._handleMessage(msg3);
631
+ expect(orchestrator.processRequest.mock.calls[2][0].payload.agentId).toBe('a1');
632
+
633
+ // Follow-up in thread (no mention) → sticky should be Beta
634
+ const msg4 = createDiscordMessage('thread follow up', { guildId: 'g1', channelId: 'thread-1', parentId: 'parent-ch' });
635
+ await service._handleMessage(msg4);
636
+ expect(orchestrator.processRequest.mock.calls[3][0].payload.agentId).toBe('a2');
637
+ });
638
+ });