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.
- package/node_modules/glob/README.md +31 -5
- package/node_modules/glob/dist/commonjs/glob.d.ts +8 -0
- package/node_modules/glob/dist/commonjs/glob.d.ts.map +1 -1
- package/node_modules/glob/dist/commonjs/glob.js +2 -1
- package/node_modules/glob/dist/commonjs/glob.js.map +1 -1
- package/node_modules/glob/dist/commonjs/index.min.js +3 -3
- package/node_modules/glob/dist/commonjs/index.min.js.map +4 -4
- package/node_modules/glob/dist/commonjs/pattern.d.ts +3 -0
- package/node_modules/glob/dist/commonjs/pattern.d.ts.map +1 -1
- package/node_modules/glob/dist/commonjs/pattern.js +4 -0
- package/node_modules/glob/dist/commonjs/pattern.js.map +1 -1
- package/node_modules/glob/dist/esm/glob.d.ts +8 -0
- package/node_modules/glob/dist/esm/glob.d.ts.map +1 -1
- package/node_modules/glob/dist/esm/glob.js +2 -1
- package/node_modules/glob/dist/esm/glob.js.map +1 -1
- package/node_modules/glob/dist/esm/index.min.js +3 -3
- package/node_modules/glob/dist/esm/index.min.js.map +4 -4
- package/node_modules/glob/dist/esm/pattern.d.ts +3 -0
- package/node_modules/glob/dist/esm/pattern.d.ts.map +1 -1
- package/node_modules/glob/dist/esm/pattern.js +4 -0
- package/node_modules/glob/dist/esm/pattern.js.map +1 -1
- package/node_modules/{@isaacs → glob/node_modules}/balanced-match/README.md +7 -10
- package/node_modules/{@isaacs → glob/node_modules}/balanced-match/package.json +7 -18
- package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/README.md +3 -6
- package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/dist/commonjs/index.js +6 -4
- package/node_modules/glob/node_modules/brace-expansion/dist/commonjs/index.js.map +1 -0
- package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/dist/esm/index.js +6 -4
- package/node_modules/glob/node_modules/brace-expansion/dist/esm/index.js.map +1 -0
- package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/package.json +11 -7
- package/node_modules/glob/node_modules/minimatch/README.md +76 -1
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/assert-valid-pattern.d.ts +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/assert-valid-pattern.d.ts.map +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/assert-valid-pattern.js.map +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/ast.d.ts +4 -2
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/ast.d.ts.map +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/ast.js +309 -55
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/ast.js.map +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/brace-expressions.d.ts.map +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/brace-expressions.js +2 -4
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/brace-expressions.js.map +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/escape.d.ts +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/escape.d.ts.map +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/escape.js +4 -4
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/escape.js.map +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/index.d.ts +81 -1
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/index.d.ts.map +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/index.js +232 -134
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/index.js.map +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/unescape.d.ts +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/unescape.d.ts.map +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/unescape.js +8 -8
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/unescape.js.map +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/esm/assert-valid-pattern.d.ts +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/esm/assert-valid-pattern.d.ts.map +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/esm/assert-valid-pattern.js.map +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/esm/ast.d.ts +4 -2
- package/node_modules/glob/node_modules/minimatch/dist/esm/ast.d.ts.map +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/esm/ast.js +309 -55
- package/node_modules/glob/node_modules/minimatch/dist/esm/ast.js.map +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/esm/brace-expressions.d.ts.map +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/esm/brace-expressions.js +2 -4
- package/node_modules/glob/node_modules/minimatch/dist/esm/brace-expressions.js.map +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/esm/escape.d.ts +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/esm/escape.d.ts.map +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/esm/escape.js +4 -4
- package/node_modules/glob/node_modules/minimatch/dist/esm/escape.js.map +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/esm/index.d.ts +81 -1
- package/node_modules/glob/node_modules/minimatch/dist/esm/index.d.ts.map +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/esm/index.js +232 -134
- package/node_modules/glob/node_modules/minimatch/dist/esm/index.js.map +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/esm/unescape.d.ts +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/esm/unescape.d.ts.map +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/esm/unescape.js +8 -8
- package/node_modules/glob/node_modules/minimatch/dist/esm/unescape.js.map +1 -1
- package/node_modules/glob/node_modules/minimatch/package.json +17 -11
- package/node_modules/glob/package.json +10 -13
- package/node_modules/minipass/LICENSE.md +55 -0
- package/node_modules/minipass/dist/commonjs/index.d.ts +12 -16
- package/node_modules/minipass/dist/commonjs/index.d.ts.map +1 -1
- package/node_modules/minipass/dist/commonjs/index.js +13 -3
- package/node_modules/minipass/dist/commonjs/index.js.map +1 -1
- package/node_modules/minipass/dist/esm/index.d.ts +12 -16
- package/node_modules/minipass/dist/esm/index.d.ts.map +1 -1
- package/node_modules/minipass/dist/esm/index.js +3 -1
- package/node_modules/minipass/dist/esm/index.js.map +1 -1
- package/node_modules/minipass/package.json +9 -14
- package/node_modules/path-scurry/node_modules/lru-cache/README.md +96 -10
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/diagnostics-channel-browser.d.ts.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/diagnostics-channel-browser.js.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/diagnostics-channel.d.ts +5 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/diagnostics-channel.js +7 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/index.d.ts +1400 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/index.d.ts.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/index.js +1726 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/index.js.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/index.min.js +2 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/index.min.js.map +7 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/perf.d.ts +12 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/perf.d.ts.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/perf.js +10 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/perf.js.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/diagnostics-channel-cjs.cjs.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/diagnostics-channel-cjs.d.cts.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/diagnostics-channel.d.ts +5 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/diagnostics-channel.js +7 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/index.d.ts +109 -32
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/index.d.ts.map +1 -1
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/index.js +334 -197
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/index.js.map +1 -1
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/index.min.js +1 -1
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/index.min.js.map +4 -4
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/diagnostics-channel-node.d.ts.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/diagnostics-channel-node.js.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/diagnostics-channel.d.ts +5 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/diagnostics-channel.js +9 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/index.d.ts +1400 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/index.d.ts.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/index.js +1726 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/index.js.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/index.min.js +2 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/index.min.js.map +7 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/perf.d.ts +12 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/perf.d.ts.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/perf.js +10 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/perf.js.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/perf.d.ts +12 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/perf.d.ts.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/perf.js +10 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/perf.js.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/diagnostics-channel-browser.d.ts.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/diagnostics-channel-browser.js.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/diagnostics-channel.d.ts +5 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/diagnostics-channel.js +4 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/index.d.ts +1400 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/index.d.ts.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/index.js +1722 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/index.js.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/index.min.js +2 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/index.min.js.map +7 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/perf.d.ts +12 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/perf.d.ts.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/perf.js +7 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/perf.js.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/diagnostics-channel-esm.d.mts.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/diagnostics-channel-esm.mjs.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/diagnostics-channel.d.ts +5 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/diagnostics-channel.js +19 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/index.d.ts +109 -32
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/index.d.ts.map +1 -1
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/index.js +333 -196
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/index.js.map +1 -1
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/index.min.js +1 -1
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/index.min.js.map +4 -4
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/diagnostics-channel-node.d.ts.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/diagnostics-channel-node.js.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/diagnostics-channel.d.ts +5 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/diagnostics-channel.js +6 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/index.d.ts +1400 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/index.d.ts.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/index.js +1722 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/index.js.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/index.min.js +2 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/index.min.js.map +7 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/perf.d.ts +12 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/perf.d.ts.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/perf.js +7 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/perf.js.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/perf.d.ts +12 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/perf.d.ts.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/perf.js +7 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/perf.js.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/package.json +71 -18
- package/node_modules/path-scurry/package.json +8 -24
- package/package.json +1 -1
- package/scripts/debug-balance-probe.mjs +35 -35
- package/scripts/push-image.sh +43 -43
- package/scripts/setup-acr.sh +65 -65
- package/scripts/verify-optional-deps.js +96 -1
- package/src/__tests__/composioCliFlags.test.js +239 -239
- package/src/analyzers/CSSAnalyzer.js +298 -297
- package/src/analyzers/ConfigValidator.js +691 -690
- package/src/analyzers/ESLintAnalyzer.js +320 -320
- package/src/analyzers/JavaScriptAnalyzer.js +260 -261
- package/src/analyzers/PrettierFormatter.js +246 -247
- package/src/analyzers/PythonAnalyzer.js +283 -283
- package/src/analyzers/SecurityAnalyzer.js +729 -729
- package/src/analyzers/SparrowAnalyzer.js +341 -341
- package/src/analyzers/TypeScriptAnalyzer.js +247 -247
- package/src/analyzers/__tests__/CSSAnalyzer.test.js +41 -41
- package/src/analyzers/__tests__/ConfigValidator.test.js +362 -362
- package/src/analyzers/__tests__/JavaScriptAnalyzer.test.js +40 -40
- package/src/analyzers/__tests__/PythonAnalyzer.test.js +205 -208
- package/src/analyzers/__tests__/SecurityAnalyzer.test.js +303 -303
- package/src/analyzers/__tests__/TypeScriptAnalyzer.test.js +187 -187
- package/src/analyzers/codeCloneDetector/analyzer.js +344 -344
- package/src/analyzers/codeCloneDetector/detector.js +250 -250
- package/src/analyzers/codeCloneDetector/index.js +194 -192
- package/src/analyzers/codeCloneDetector/parser.js +199 -199
- package/src/core/__tests__/agentPool.test.js +866 -866
- package/src/core/__tests__/agentPoolAutoResume.test.js +209 -209
- package/src/core/__tests__/agentPoolWakeOnMessage.test.js +315 -315
- package/src/core/__tests__/agentScheduler.emptyResponseChatStall.test.js +213 -213
- package/src/core/__tests__/agentScheduler.errorCategorisation.test.js +246 -246
- package/src/core/__tests__/agentScheduler.firstChunkTimeout.test.js +138 -138
- package/src/core/__tests__/agentScheduler.modeTransitions.test.js +233 -233
- package/src/core/__tests__/agentScheduler.nativePromptPick.test.js +319 -319
- package/src/core/__tests__/agentScheduler.taskLifecycleInstruction.test.js +78 -78
- package/src/core/__tests__/agentScheduler.visualizer.test.js +258 -258
- package/src/core/__tests__/flowCheckpointStore.test.js +140 -140
- package/src/core/__tests__/flowEndToEnd.test.js +565 -565
- package/src/core/__tests__/flowFieldMapping.test.js +188 -189
- package/src/core/__tests__/flowLintClientMirror.test.js +96 -98
- package/src/core/__tests__/flowSavePayload.test.js +170 -169
- package/src/core/__tests__/flowTemplates.test.js +311 -311
- package/src/core/__tests__/flowVersionStore.test.js +123 -123
- package/src/core/__tests__/messageProcessor.test.js +669 -669
- package/src/core/__tests__/stateManager.test.js +0 -1
- package/src/core/agentPool.js +2474 -2475
- package/src/core/agentScheduler.js +1 -4
- package/src/core/contextManager.js +708 -708
- package/src/core/flowExecutor.js +1510 -1510
- package/src/core/flowFieldMapping.js +136 -138
- package/src/core/messageProcessor.js +953 -954
- package/src/core/orchestrator.js +593 -595
- package/src/core/stateManager.js +1765 -1752
- package/src/index.js +1221 -1221
- package/src/interfaces/__tests__/archivedAgentDelete.test.js +207 -207
- package/src/interfaces/__tests__/bulkAgentRoute.test.js +361 -361
- package/src/interfaces/__tests__/imageServing.test.js +228 -228
- package/src/interfaces/__tests__/remoteSessionAuth.test.js +308 -308
- package/src/interfaces/__tests__/videoJobsRoutes.test.js +178 -179
- package/src/interfaces/__tests__/webServer.marketplace.test.js +629 -629
- package/src/interfaces/schedulerRoutes.js +50 -50
- package/src/interfaces/terminal/__tests__/smoke/connection.test.js +341 -350
- package/src/interfaces/terminal/__tests__/smoke/enhancements.test.js +156 -156
- package/src/interfaces/terminal/__tests__/smoke/imports.test.js +325 -330
- package/src/interfaces/terminal/__tests__/smoke/tools.test.js +385 -388
- package/src/interfaces/terminal/api/session.js +265 -266
- package/src/interfaces/terminal/api/websocket.js +496 -497
- package/src/interfaces/terminal/components/AgentCreator.js +691 -705
- package/src/interfaces/terminal/components/AgentEditor.js +676 -678
- package/src/interfaces/terminal/components/AgentSwitcher.js +331 -330
- package/src/interfaces/terminal/components/ErrorPanel.js +263 -264
- package/src/interfaces/terminal/components/Header.js +28 -28
- package/src/interfaces/terminal/components/Layout.js +598 -603
- package/src/interfaces/terminal/components/MessageList.js +280 -281
- package/src/interfaces/terminal/components/SettingsPanel.js +410 -415
- package/src/interfaces/terminal/components/StatusBar.js +2 -0
- package/src/interfaces/terminal/index.js +168 -168
- package/src/interfaces/terminal/state/useAgentControl.js +496 -496
- package/src/interfaces/terminal/state/useAgents.js +537 -537
- package/src/interfaces/terminal/state/useMessages.js +629 -630
- package/src/interfaces/terminal/state/useTools.js +554 -554
- package/src/interfaces/terminal/utils/debugLogger.js +44 -44
- package/src/interfaces/terminal/utils/settingsStorage.js +232 -232
- package/src/interfaces/webServer.js +7578 -7579
- package/src/interfaces/webServer.js.bak +7046 -7046
- package/src/modules/fileExplorer/__tests__/zipDownload.test.js +237 -237
- package/src/modules/fileExplorer/controller.js +470 -469
- package/src/modules/fileExplorer/routes.js +285 -286
- package/src/modules/widget/__tests__/isDisabled.test.js +41 -41
- package/src/modules/widget/__tests__/routes.test.js +677 -678
- package/src/modules/widget/__tests__/runtime.test.js +401 -401
- package/src/modules/widget/__tests__/versioning.test.js +309 -309
- package/src/modules/widget/__tests__/webComponentRuntime.test.js +565 -565
- package/src/modules/widget/__tests__/widgetTool.test.js +316 -316
- package/src/modules/widget/routes.js +435 -435
- package/src/modules/widget/runtime/bundle.js +640 -640
- package/src/modules/widget/runtime/webComponentBundle.js +470 -470
- package/src/modules/widget/schema.js +182 -181
- package/src/modules/widget/widgetTool.js +1389 -1389
- package/src/services/__tests__/agentActivityService.test.js +401 -402
- package/src/services/__tests__/benchmarkService.test.js +184 -184
- package/src/services/__tests__/contextInjectionService.test.js +246 -246
- package/src/services/__tests__/conversationQuery.test.js +721 -723
- package/src/services/__tests__/credentialVault.test.js +469 -469
- package/src/services/__tests__/discordService.integration.test.js +638 -639
- package/src/services/__tests__/flowContextService.test.js +590 -590
- package/src/services/__tests__/memoryService.test.js +1 -1
- package/src/services/__tests__/messageSource.test.js +380 -380
- package/src/services/__tests__/modelRouterNaming.test.js +111 -111
- package/src/services/__tests__/projectDetector.test.js +34 -34
- package/src/services/__tests__/promptService.test.js +242 -242
- package/src/services/__tests__/telegramService.test.js +941 -941
- package/src/services/__tests__/tokenCountingService.test.js +48 -48
- package/src/services/agentActivityService.js +419 -420
- package/src/services/aiService.js +2997 -3001
- package/src/services/apiKeyManager.js +359 -359
- package/src/services/benchmarkService.js +196 -196
- package/src/services/codebaseKnowledgeService.js +2 -2
- package/src/services/composioService.js +738 -738
- package/src/services/conversationCompactionService.js +1258 -1257
- package/src/services/credentialVault.js +685 -685
- package/src/services/discordService.js +792 -793
- package/src/services/embeddings/__tests__/azureCustomProvider.test.js +232 -232
- package/src/services/embeddings/__tests__/embeddingService.test.js +417 -417
- package/src/services/embeddings/__tests__/localProvider.test.js +263 -263
- package/src/services/embeddings/autoRecall.js +218 -219
- package/src/services/embeddings/indexers/__tests__/agentIndexer.test.js +232 -232
- package/src/services/embeddings/indexers/__tests__/memoryIndexer.test.js +418 -418
- package/src/services/embeddings/indexers/__tests__/reminisceIndexer.test.js +356 -357
- package/src/services/embeddings/indexers/__tests__/skillsIndexer.test.js +145 -145
- package/src/services/embeddings/indexers/__tests__/taskIndexer.test.js +146 -146
- package/src/services/embeddings/indexers/composioIndexer.js +279 -279
- package/src/services/embeddings/providerInterface.js +206 -206
- package/src/services/embeddings/providers/localProvider.js +11 -7
- package/src/services/embeddings/providers/openaiProvider.js +101 -101
- package/src/services/embeddings/vectorStore/inMemoryJsonStore.js +356 -356
- package/src/services/errorHandler.js +809 -809
- package/src/services/flowContextService.js +586 -586
- package/src/services/grounding/MockAdapter.js +125 -125
- package/src/services/modelRouterService.js +26 -31
- package/src/services/modelsService.js +322 -322
- package/src/services/ollamaService.js +452 -452
- package/src/services/projectDetector.js +403 -404
- package/src/services/promptService.js +418 -418
- package/src/services/qualityInspector.js +795 -795
- package/src/services/scheduleService.js +726 -726
- package/src/services/serviceRegistry.js +386 -386
- package/src/services/telegrafBot.js +174 -174
- package/src/services/telegramService.js +1972 -1972
- package/src/services/visualEditorBridge.js +1033 -1033
- package/src/services/visualEditorServer.js +1769 -1774
- package/src/services/whatsappService.js +667 -668
- package/src/tools/__tests__/agentCommunicationTool.findAgent.test.js +226 -226
- package/src/tools/__tests__/agentCommunicationTool.test.js +3 -3
- package/src/tools/__tests__/agentDelayTool.test.js +342 -342
- package/src/tools/__tests__/baseTool.test.js +3 -3
- package/src/tools/__tests__/codeMapTool.test.js +915 -915
- package/src/tools/__tests__/fileContentReplaceTool.test.js +309 -309
- package/src/tools/__tests__/fileTreeTool.test.js +274 -274
- package/src/tools/__tests__/filesystemTool.test.js +815 -815
- package/src/tools/__tests__/foundryWebSearchTool.test.js +252 -252
- package/src/tools/__tests__/imageTool.validator.test.js +194 -194
- package/src/tools/__tests__/jobDoneTool.test.js +580 -581
- package/src/tools/__tests__/memoryTool.forgetStale.test.js +272 -272
- package/src/tools/__tests__/memoryTool.reminisce.test.js +2 -2
- package/src/tools/__tests__/memoryTool.reminisceSemanticSearch.test.js +301 -301
- package/src/tools/__tests__/memoryTool.semanticSearch.test.js +405 -405
- package/src/tools/__tests__/memoryTool.teamPool.test.js +293 -293
- package/src/tools/__tests__/memoryTool.test.js +1 -1
- package/src/tools/__tests__/seekTool.test.js +282 -282
- package/src/tools/__tests__/skillsTool.search.test.js +164 -164
- package/src/tools/__tests__/skillsTool.test.js +226 -226
- package/src/tools/__tests__/staticAnalysisTool.test.js +509 -509
- package/src/tools/__tests__/taskManagerTool.discipline.test.js +137 -137
- package/src/tools/__tests__/taskManagerTool.search.test.js +143 -143
- package/src/tools/__tests__/taskManagerTool.test.js +866 -866
- package/src/tools/__tests__/terminalTool.test.js +448 -448
- package/src/tools/__tests__/toolShapeForgiveness.test.js +259 -260
- package/src/tools/__tests__/userPromptTool.test.js +297 -297
- package/src/tools/__tests__/videoTool.jobs.test.js +147 -147
- package/src/tools/__tests__/webTool.e2e.test.js +609 -603
- package/src/tools/__tests__/webTool.unit.test.js +195 -195
- package/src/tools/__tests__/webTool.visionModel.test.js +75 -75
- package/src/tools/agentCommunicationTool.js +8 -10
- package/src/tools/agentDelayTool.js +496 -497
- package/src/tools/asyncToolManager.js +602 -603
- package/src/tools/baseTool.js +12 -11
- package/src/tools/cloneDetectionTool.js +576 -581
- package/src/tools/codeMapTool.js +0 -6
- package/src/tools/composioTool.js +617 -617
- package/src/tools/dependencyResolverTool.js +1211 -1212
- package/src/tools/desktop/DesktopTool.js +629 -638
- package/src/tools/desktop/__tests__/DesktopTool.e2e.test.js +306 -306
- package/src/tools/desktop/__tests__/DesktopTool.test.js +507 -507
- package/src/tools/desktop/__tests__/osController.test.js +364 -364
- package/src/tools/desktop/osController.js +491 -491
- package/src/tools/docxTool.js +623 -623
- package/src/tools/excelTool.js +636 -636
- package/src/tools/fileContentReplaceTool.js +5 -7
- package/src/tools/fileSystemTool.js +12 -19
- package/src/tools/fileTreeTool.js +840 -840
- package/src/tools/foundryWebSearchTool.js +273 -273
- package/src/tools/helpTool.js +198 -198
- package/src/tools/imageTool.js +1397 -1397
- package/src/tools/importAnalyzerTool.js +1056 -1056
- package/src/tools/jobDoneTool.js +495 -495
- package/src/tools/memoryTool.js +1 -1
- package/src/tools/office/pres/__tests__/presSystem.test.js +365 -365
- package/src/tools/office/pres/archetypes/agenda.js +61 -61
- package/src/tools/office/pres/archetypes/bentoGrid.js +218 -219
- package/src/tools/office/pres/archetypes/bigStat.js +140 -142
- package/src/tools/office/pres/archetypes/closing.js +70 -70
- package/src/tools/office/pres/archetypes/hero.js +70 -70
- package/src/tools/office/pres/archetypes/productHero.js +93 -94
- package/src/tools/office/pres/archetypes/table.js +73 -74
- package/src/tools/office/pres/backgrounds/orb.js +66 -66
- package/src/tools/office/pres/components.js +422 -423
- package/src/tools/officeTool.js +441 -441
- package/src/tools/pdfTool.js +625 -627
- package/src/tools/platformControlTool.js +1081 -1081
- package/src/tools/seekTool.js +917 -918
- package/src/tools/skillsTool.js +1 -1
- package/src/tools/staticAnalysisTool.js +2143 -2146
- package/src/tools/taskManagerTool.js +3324 -3324
- package/src/tools/terminalTool.js +2615 -2618
- package/src/tools/videoTool.js +1303 -1303
- package/src/tools/visionTool.js +508 -508
- package/src/tools/visualEditorTool.js +1289 -1290
- package/src/tools/webTool.js +3368 -3368
- package/src/tools/whatsappTool.js +464 -464
- package/src/types/__tests__/agent.test.js +499 -499
- package/src/types/__tests__/contextReference.test.js +606 -606
- package/src/types/__tests__/conversation.test.js +555 -555
- package/src/types/__tests__/toolCommand.test.js +584 -584
- package/src/types/contextReference.js +974 -971
- package/src/types/conversation.js +729 -729
- package/src/types/toolCommand.js +746 -746
- package/src/utilities/__tests__/attachmentValidator.test.js +80 -80
- package/src/utilities/__tests__/auditReport.test.js +328 -328
- package/src/utilities/__tests__/directoryAccessManager.test.js +388 -388
- package/src/utilities/__tests__/jsonRepair.test.js +103 -104
- package/src/utilities/__tests__/modeTransitionReasons.test.js +105 -105
- package/src/utilities/__tests__/platformUtils.test.js +80 -87
- package/src/utilities/__tests__/structuredFileValidator.test.js +261 -263
- package/src/utilities/__tests__/toolConstants.test.js +92 -94
- package/src/utilities/__tests__/useIsTouchDevice.detect.test.js +114 -114
- package/src/utilities/__tests__/webUiUtilSync.test.js +117 -117
- package/src/utilities/attachmentValidator.js +284 -288
- package/src/utilities/authCache.js.backup-1779570472481 +121 -121
- package/src/utilities/browserStealth.js +631 -630
- package/src/utilities/configManager.js +616 -617
- package/src/utilities/directoryAccessManager.js +564 -565
- package/src/utilities/fileProcessor.js +308 -307
- package/src/utilities/humanBehavior.js +454 -453
- package/src/utilities/logger.js +479 -479
- package/src/utilities/structuredFileValidator.js +696 -699
- package/src/utilities/tagParser.js +5 -10
- package/src/utilities/userDataDir.js +308 -308
- package/node_modules/@isaacs/brace-expansion/dist/commonjs/index.js.map +0 -1
- package/node_modules/@isaacs/brace-expansion/dist/esm/index.js.map +0 -1
- package/node_modules/minipass/LICENSE +0 -15
- /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/LICENSE.md +0 -0
- /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/commonjs/index.d.ts +0 -0
- /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/commonjs/index.d.ts.map +0 -0
- /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/commonjs/index.js +0 -0
- /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/commonjs/index.js.map +0 -0
- /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/commonjs/package.json +0 -0
- /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/esm/index.d.ts +0 -0
- /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/esm/index.d.ts.map +0 -0
- /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/esm/index.js +0 -0
- /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/esm/index.js.map +0 -0
- /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/esm/package.json +0 -0
- /package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/LICENSE +0 -0
- /package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/dist/commonjs/index.d.ts +0 -0
- /package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/dist/commonjs/index.d.ts.map +0 -0
- /package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/dist/commonjs/package.json +0 -0
- /package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/dist/esm/index.d.ts +0 -0
- /package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/dist/esm/index.d.ts.map +0 -0
- /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
|
|
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
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
//
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
expect(
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
//
|
|
355
|
-
//
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
service1.
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
await service1.assignAgentToChannel('g1:
|
|
368
|
-
|
|
369
|
-
service1.
|
|
370
|
-
|
|
371
|
-
'g1:
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
const
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
'g1:
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
expect(service2.
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
service2.
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
expect(service2.orchestrator.processRequest).
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
//
|
|
409
|
-
//
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
const
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
service.
|
|
419
|
-
service.
|
|
420
|
-
service.
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
expect(msg.reply).
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
'guild-A:
|
|
437
|
-
'guild-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
'guild-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
'guild-A:
|
|
446
|
-
'guild-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
delete service.
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
expect(service.channelMappings['guild-A:
|
|
462
|
-
expect(service.
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
expect(service.
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
//
|
|
472
|
-
//
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
const
|
|
478
|
-
|
|
479
|
-
service.
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
expect(sent).toContain('
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
//
|
|
509
|
-
//
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
orchestrator
|
|
518
|
-
service.
|
|
519
|
-
|
|
520
|
-
'
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
'g1:
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
expect(orchestrator.processRequest).
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
expect(orchestrator.processRequest).
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
const
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
if (id === '
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
};
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
expect(
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
'g1:
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
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
|
+
});
|