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,507 +1,507 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* DesktopTool unit tests.
|
|
3
|
-
*
|
|
4
|
-
* All native deps mocked: osController + grounding adapter are stubs.
|
|
5
|
-
* Verifies:
|
|
6
|
-
* - kill switch (env)
|
|
7
|
-
* - per-agent allowlist
|
|
8
|
-
* - parameter dispatch + validation
|
|
9
|
-
* - intent → grounding → coord validation pipeline
|
|
10
|
-
* - drag's screenshot reuse optimisation
|
|
11
|
-
* - audit logging shape
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import { jest, describe, test, expect,
|
|
15
|
-
import { DesktopTool, DesktopAction } from '../DesktopTool.js';
|
|
16
|
-
|
|
17
|
-
// ─── fakes ─────────────────────────────────────────────────────────────
|
|
18
|
-
|
|
19
|
-
function fakeOsc(overrides = {}) {
|
|
20
|
-
const calls = [];
|
|
21
|
-
const record = (name) => jest.fn(async (...args) => { calls.push({ name, args }); });
|
|
22
|
-
return {
|
|
23
|
-
screenshot: jest.fn(async () => Buffer.from('png-bytes')),
|
|
24
|
-
screenSize: jest.fn(async () => ({ width: 1920, height: 1080, dpr: 1 })),
|
|
25
|
-
mouseMove: record('mouseMove'),
|
|
26
|
-
mouseClick: record('mouseClick'),
|
|
27
|
-
mouseScroll: record('mouseScroll'),
|
|
28
|
-
mouseDrag: record('mouseDrag'),
|
|
29
|
-
typeText: record('typeText'),
|
|
30
|
-
keyPress: record('keyPress'),
|
|
31
|
-
listWindows: jest.fn(async () => [{ title: 'Notepad' }, { title: 'Browser' }]),
|
|
32
|
-
focusWindow: jest.fn(async ({ titleMatch }) => ({
|
|
33
|
-
focused: titleMatch === 'note' || titleMatch === 'Notepad',
|
|
34
|
-
title: 'Notepad',
|
|
35
|
-
})),
|
|
36
|
-
describeCapabilities: jest.fn(async () => ({
|
|
37
|
-
os: 'win32', display: null,
|
|
38
|
-
screenshot: true, mouseInput: true, keyboardInput: true, windowFocus: true,
|
|
39
|
-
degradedReason: null, nativeLoaded: true,
|
|
40
|
-
})),
|
|
41
|
-
_calls: calls,
|
|
42
|
-
...overrides,
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/** A stub aiService that carries a base url + a fake key manager. */
|
|
47
|
-
function fakeAIService() {
|
|
48
|
-
return {
|
|
49
|
-
baseUrl: 'https://api.test.loxia.ai',
|
|
50
|
-
config: { apiKey: 'test-loxia-key' },
|
|
51
|
-
apiKeyManager: {
|
|
52
|
-
getKeysForRequest: jest.fn(() => ({ loxiaApiKey: 'test-loxia-key' })),
|
|
53
|
-
},
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Build a DesktopTool with a stubbed grounding adapter that returns
|
|
59
|
-
* the coords we tell it to.
|
|
60
|
-
*/
|
|
61
|
-
function makeTool({ osc = fakeOsc(), groundResult, aiService = fakeAIService() } = {}) {
|
|
62
|
-
const tool = DesktopTool.withDependencies({
|
|
63
|
-
aiService,
|
|
64
|
-
osController: osc,
|
|
65
|
-
config: {},
|
|
66
|
-
logger: { info: jest.fn(), warn: jest.fn(), error: jest.fn() },
|
|
67
|
-
});
|
|
68
|
-
// Pre-seed adapter cache so we don't hit the registry.
|
|
69
|
-
tool._adapterCache = {
|
|
70
|
-
ground: jest.fn(async () => groundResult || {
|
|
71
|
-
answer: 'x=500, y=400',
|
|
72
|
-
coords: { x: 500, y: 400 },
|
|
73
|
-
usage: { input_tokens: 1, output_tokens: 1 },
|
|
74
|
-
elapsed_ms: 5,
|
|
75
|
-
}),
|
|
76
|
-
};
|
|
77
|
-
return tool;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const FULL_ALLOW = { allowedActions: Object.values(DesktopAction) };
|
|
81
|
-
|
|
82
|
-
// ─── kill switch / allowlist ───────────────────────────────────────────
|
|
83
|
-
|
|
84
|
-
describe('DesktopTool — gating', () => {
|
|
85
|
-
const originalEnv = process.env.LOXIA_DESKTOP_TOOL_DISABLED;
|
|
86
|
-
afterEach(() => {
|
|
87
|
-
if (originalEnv === undefined) delete process.env.LOXIA_DESKTOP_TOOL_DISABLED;
|
|
88
|
-
else process.env.LOXIA_DESKTOP_TOOL_DISABLED = originalEnv;
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
test('env kill switch short-circuits every call', async () => {
|
|
92
|
-
process.env.LOXIA_DESKTOP_TOOL_DISABLED = '1';
|
|
93
|
-
const tool = makeTool();
|
|
94
|
-
const r = await tool.execute({ action: 'screenshot' }, { toolConfig: FULL_ALLOW });
|
|
95
|
-
expect(r.success).toBe(false);
|
|
96
|
-
expect(r.code).toBe('DESKTOP_DISABLED');
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
test('default (no allowedActions) refuses every action', async () => {
|
|
100
|
-
const tool = makeTool();
|
|
101
|
-
const r = await tool.execute({ action: 'screenshot' }, {});
|
|
102
|
-
expect(r.success).toBe(false);
|
|
103
|
-
expect(r.code).toBe('NOT_PERMITTED');
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
test('explicit allowlist gates per action', async () => {
|
|
107
|
-
const tool = makeTool();
|
|
108
|
-
const r1 = await tool.execute(
|
|
109
|
-
{ action: 'screenshot' },
|
|
110
|
-
{ toolConfig: { allowedActions: ['screenshot'] } },
|
|
111
|
-
);
|
|
112
|
-
expect(r1.success).toBe(true);
|
|
113
|
-
|
|
114
|
-
const r2 = await tool.execute(
|
|
115
|
-
{ action: 'click', x: 10, y: 10 },
|
|
116
|
-
{ toolConfig: { allowedActions: ['screenshot'] } }, // click NOT in list
|
|
117
|
-
);
|
|
118
|
-
expect(r2.success).toBe(false);
|
|
119
|
-
expect(r2.code).toBe('NOT_PERMITTED');
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
test('unknown action returns INVALID_ACTION', async () => {
|
|
123
|
-
const tool = makeTool();
|
|
124
|
-
const r = await tool.execute({ action: 'self_destruct' }, { toolConfig: FULL_ALLOW });
|
|
125
|
-
expect(r.success).toBe(false);
|
|
126
|
-
expect(r.code).toBe('INVALID_ACTION');
|
|
127
|
-
});
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
// ─── action dispatch ───────────────────────────────────────────────────
|
|
131
|
-
|
|
132
|
-
describe('DesktopTool — actions', () => {
|
|
133
|
-
test('screenshot saves to disk and returns the path (no inline base64)', async () => {
|
|
134
|
-
const tool = makeTool();
|
|
135
|
-
const r = await tool.execute({ action: 'screenshot' }, { toolConfig: FULL_ALLOW });
|
|
136
|
-
expect(r.success).toBe(true);
|
|
137
|
-
// Critical: we MUST NOT return raw base64 bytes in the tool result —
|
|
138
|
-
// the agent can't read PNG bytes from text, and dumping 200 KB of
|
|
139
|
-
// base64 into the conversation derails the next model turn.
|
|
140
|
-
expect(r).not.toHaveProperty('screenshotBase64');
|
|
141
|
-
expect(typeof r.screenshotPath).toBe('string');
|
|
142
|
-
expect(r.screenshotPath).toMatch(/screenshot-\d+\.png$/);
|
|
143
|
-
expect(r.size).toMatchObject({ width: 1920, height: 1080 });
|
|
144
|
-
expect(r.bytes).toBe(Buffer.from('png-bytes').length);
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
test('click with raw (x,y) skips grounding', async () => {
|
|
148
|
-
const osc = fakeOsc();
|
|
149
|
-
const tool = makeTool({ osc });
|
|
150
|
-
const r = await tool.execute(
|
|
151
|
-
{ action: 'click', x: 100, y: 200, button: 'right' },
|
|
152
|
-
{ toolConfig: FULL_ALLOW },
|
|
153
|
-
);
|
|
154
|
-
expect(r.success).toBe(true);
|
|
155
|
-
expect(r.coords).toEqual({ x: 100, y: 200 });
|
|
156
|
-
expect(r.groundedFrom).toBeNull();
|
|
157
|
-
expect(osc.mouseClick).toHaveBeenCalledWith(100, 200, { button: 'right', count: undefined });
|
|
158
|
-
// Grounding adapter NOT consulted.
|
|
159
|
-
expect(tool._adapterCache.ground).not.toHaveBeenCalled();
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
test('click with intent grounds, then clicks', async () => {
|
|
163
|
-
const osc = fakeOsc();
|
|
164
|
-
const tool = makeTool({ osc });
|
|
165
|
-
const r = await tool.execute(
|
|
166
|
-
{ action: 'click', intent: 'the Save button' },
|
|
167
|
-
{ toolConfig: FULL_ALLOW },
|
|
168
|
-
);
|
|
169
|
-
expect(r.success).toBe(true);
|
|
170
|
-
expect(r.coords).toEqual({ x: 500, y: 400 });
|
|
171
|
-
expect(r.groundedFrom).toBe('the Save button');
|
|
172
|
-
expect(tool._adapterCache.ground).toHaveBeenCalledTimes(1);
|
|
173
|
-
expect(osc.mouseClick).toHaveBeenCalledWith(500, 400, expect.any(Object));
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
test('click with neither coords nor intent fails INVALID_INPUT', async () => {
|
|
177
|
-
const tool = makeTool();
|
|
178
|
-
const r = await tool.execute({ action: 'click' }, { toolConfig: FULL_ALLOW });
|
|
179
|
-
expect(r.success).toBe(false);
|
|
180
|
-
expect(r.code).toBe('INVALID_INPUT');
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
test('grounding miss (no coords parsed) → GROUNDING_FAILED', async () => {
|
|
184
|
-
const tool = makeTool({
|
|
185
|
-
groundResult: { answer: 'I have no idea where that is', usage: {}, elapsed_ms: 1 },
|
|
186
|
-
});
|
|
187
|
-
const r = await tool.execute(
|
|
188
|
-
{ action: 'click', intent: 'mystery button' },
|
|
189
|
-
{ toolConfig: FULL_ALLOW },
|
|
190
|
-
);
|
|
191
|
-
expect(r.success).toBe(false);
|
|
192
|
-
expect(r.code).toBe('GROUNDING_FAILED');
|
|
193
|
-
expect(r.error).toMatch(/no idea/);
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
test('grounded coords outside screen → COORDS_OUT_OF_BOUNDS', async () => {
|
|
197
|
-
const tool = makeTool({
|
|
198
|
-
groundResult: { answer: 'x=9999, y=9999', coords: { x: 9999, y: 9999 } },
|
|
199
|
-
});
|
|
200
|
-
const r = await tool.execute(
|
|
201
|
-
{ action: 'click', intent: 'somewhere' },
|
|
202
|
-
{ toolConfig: FULL_ALLOW },
|
|
203
|
-
);
|
|
204
|
-
expect(r.success).toBe(false);
|
|
205
|
-
expect(r.code).toBe('COORDS_OUT_OF_BOUNDS');
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
test('type forwards text and length', async () => {
|
|
209
|
-
const osc = fakeOsc();
|
|
210
|
-
const tool = makeTool({ osc });
|
|
211
|
-
const r = await tool.execute(
|
|
212
|
-
{ action: 'type', text: 'hello world' },
|
|
213
|
-
{ toolConfig: FULL_ALLOW },
|
|
214
|
-
);
|
|
215
|
-
expect(r.success).toBe(true);
|
|
216
|
-
expect(osc.typeText).toHaveBeenCalledWith('hello world', expect.any(Object));
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
test('type with empty text → INVALID_INPUT', async () => {
|
|
220
|
-
const tool = makeTool();
|
|
221
|
-
const r = await tool.execute({ action: 'type', text: '' }, { toolConfig: FULL_ALLOW });
|
|
222
|
-
expect(r.success).toBe(false);
|
|
223
|
-
expect(r.code).toBe('INVALID_INPUT');
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
test('key_press forwards keys array', async () => {
|
|
227
|
-
const osc = fakeOsc();
|
|
228
|
-
const tool = makeTool({ osc });
|
|
229
|
-
const r = await tool.execute(
|
|
230
|
-
{ action: 'key_press', keys: ['Control', 'S'] },
|
|
231
|
-
{ toolConfig: FULL_ALLOW },
|
|
232
|
-
);
|
|
233
|
-
expect(r.success).toBe(true);
|
|
234
|
-
expect(osc.keyPress).toHaveBeenCalledWith(['Control', 'S']);
|
|
235
|
-
expect(r.output).toMatch(/Control\+S/);
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
test('key_press empty keys → INVALID_INPUT', async () => {
|
|
239
|
-
const tool = makeTool();
|
|
240
|
-
const r = await tool.execute({ action: 'key_press', keys: [] }, { toolConfig: FULL_ALLOW });
|
|
241
|
-
expect(r.success).toBe(false);
|
|
242
|
-
expect(r.code).toBe('INVALID_INPUT');
|
|
243
|
-
});
|
|
244
|
-
|
|
245
|
-
test('scroll requires dy', async () => {
|
|
246
|
-
const tool = makeTool();
|
|
247
|
-
const r = await tool.execute(
|
|
248
|
-
{ action: 'scroll', x: 100, y: 100 },
|
|
249
|
-
{ toolConfig: FULL_ALLOW },
|
|
250
|
-
);
|
|
251
|
-
expect(r.success).toBe(false);
|
|
252
|
-
expect(r.code).toBe('INVALID_INPUT');
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
test('scroll with raw coords + dy works', async () => {
|
|
256
|
-
const osc = fakeOsc();
|
|
257
|
-
const tool = makeTool({ osc });
|
|
258
|
-
const r = await tool.execute(
|
|
259
|
-
{ action: 'scroll', x: 100, y: 100, dy: -3 },
|
|
260
|
-
{ toolConfig: FULL_ALLOW },
|
|
261
|
-
);
|
|
262
|
-
expect(r.success).toBe(true);
|
|
263
|
-
expect(osc.mouseScroll).toHaveBeenCalledWith(100, 100, -3);
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
test('drag with raw from/to → two coord pairs, no grounding', async () => {
|
|
267
|
-
const osc = fakeOsc();
|
|
268
|
-
const tool = makeTool({ osc });
|
|
269
|
-
const r = await tool.execute(
|
|
270
|
-
{ action: 'drag', from: { x: 10, y: 20 }, to: { x: 50, y: 60 } },
|
|
271
|
-
{ toolConfig: FULL_ALLOW },
|
|
272
|
-
);
|
|
273
|
-
expect(r.success).toBe(true);
|
|
274
|
-
expect(r.from).toEqual({ x: 10, y: 20 });
|
|
275
|
-
expect(r.to).toEqual({ x: 50, y: 60 });
|
|
276
|
-
expect(osc.mouseDrag).toHaveBeenCalledTimes(1);
|
|
277
|
-
expect(tool._adapterCache.ground).not.toHaveBeenCalled();
|
|
278
|
-
});
|
|
279
|
-
|
|
280
|
-
test('list_windows returns titles, filters empty, caps to 50', async () => {
|
|
281
|
-
// Build a stub that mimics a real OS — most "windows" have empty
|
|
282
|
-
// titles (system handles) and the OS reports way more than 50.
|
|
283
|
-
const osc = fakeOsc({
|
|
284
|
-
listWindows: jest.fn(async () => {
|
|
285
|
-
const out = [{ title: 'Notepad' }, { title: 'Browser' }];
|
|
286
|
-
for (let i = 0; i < 200; i++) out.push({ title: '' }); // untitled handles
|
|
287
|
-
for (let i = 0; i < 60; i++) out.push({ title: `App-${i}` }); // 60 real titles
|
|
288
|
-
return out;
|
|
289
|
-
}),
|
|
290
|
-
});
|
|
291
|
-
const tool = makeTool({ osc });
|
|
292
|
-
const r = await tool.execute({ action: 'list_windows' }, { toolConfig: FULL_ALLOW });
|
|
293
|
-
expect(r.success).toBe(true);
|
|
294
|
-
expect(r.totalCount).toBe(262);
|
|
295
|
-
expect(r.titledCount).toBe(62); // 2 + 60 titled
|
|
296
|
-
expect(r.windows).toHaveLength(50); // capped
|
|
297
|
-
expect(r.truncated).toBe(true);
|
|
298
|
-
expect(r.output).toMatch(/Notepad/);
|
|
299
|
-
expect(r.output).toMatch(/filtered 200 untitled/);
|
|
300
|
-
});
|
|
301
|
-
|
|
302
|
-
test('focus_window without titleMatch → INVALID_INPUT', async () => {
|
|
303
|
-
const tool = makeTool();
|
|
304
|
-
const r = await tool.execute({ action: 'focus_window' }, { toolConfig: FULL_ALLOW });
|
|
305
|
-
expect(r.success).toBe(false);
|
|
306
|
-
expect(r.code).toBe('INVALID_INPUT');
|
|
307
|
-
});
|
|
308
|
-
|
|
309
|
-
test('focus_window match found', async () => {
|
|
310
|
-
const tool = makeTool();
|
|
311
|
-
const r = await tool.execute(
|
|
312
|
-
{ action: 'focus_window', titleMatch: 'note' },
|
|
313
|
-
{ toolConfig: FULL_ALLOW },
|
|
314
|
-
);
|
|
315
|
-
expect(r.success).toBe(true);
|
|
316
|
-
expect(r.focused).toBe(true);
|
|
317
|
-
});
|
|
318
|
-
|
|
319
|
-
test('describe_capabilities returns the capability snapshot', async () => {
|
|
320
|
-
const tool = makeTool();
|
|
321
|
-
const r = await tool.execute({ action: 'describe_capabilities' }, { toolConfig: FULL_ALLOW });
|
|
322
|
-
expect(r.success).toBe(true);
|
|
323
|
-
expect(r.capabilities.os).toBe('win32');
|
|
324
|
-
expect(r.output).toMatch(/screenshot=true/);
|
|
325
|
-
});
|
|
326
|
-
});
|
|
327
|
-
|
|
328
|
-
// ─── adapter build ─────────────────────────────────────────────────────
|
|
329
|
-
|
|
330
|
-
// ─── REGRESSION: every tool result is action-tagged ────────────────────
|
|
331
|
-
//
|
|
332
|
-
// Background: in a real run on 2026-05-13 the agent thought a click on
|
|
333
|
-
// the Netflix taskbar icon had succeeded because msg[5] in the
|
|
334
|
-
// conversation came back with `success:true` and a screenshot path —
|
|
335
|
-
// but msg[5] was actually a stale duplicate-screenshot result that
|
|
336
|
-
// landed where the click result was expected (msg[7] later carried the
|
|
337
|
-
// real click failure, by which point the agent had moved on).
|
|
338
|
-
//
|
|
339
|
-
// Fix: every action's result `output` MUST begin with "[action: <id>]"
|
|
340
|
-
// so the model can ALWAYS map a tool result back to the action it
|
|
341
|
-
// belongs to, even when results arrive out-of-order in batched messages.
|
|
342
|
-
//
|
|
343
|
-
// These tests lock the invariant. Adding a new action without tagging
|
|
344
|
-
// the output will fail here and force the author to update both
|
|
345
|
-
// places.
|
|
346
|
-
|
|
347
|
-
describe('REGRESSION: every desktop result includes its action tag', () => {
|
|
348
|
-
function expectActionTag(result, expected) {
|
|
349
|
-
expect(typeof result.output).toBe('string');
|
|
350
|
-
expect(result.output.startsWith(`[action: ${expected}]`)).toBe(true);
|
|
351
|
-
// Structured field too, so consumers don't have to parse the prefix.
|
|
352
|
-
expect(result.action).toBe(expected);
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
test('screenshot output begins with [action: screenshot]', async () => {
|
|
356
|
-
const r = await makeTool().execute({ action: 'screenshot' }, { toolConfig: FULL_ALLOW });
|
|
357
|
-
expect(r.success).toBe(true);
|
|
358
|
-
expectActionTag(r, 'screenshot');
|
|
359
|
-
});
|
|
360
|
-
|
|
361
|
-
test('click output begins with [action: click]', async () => {
|
|
362
|
-
const r = await makeTool().execute(
|
|
363
|
-
{ action: 'click', x: 100, y: 200 },
|
|
364
|
-
{ toolConfig: FULL_ALLOW },
|
|
365
|
-
);
|
|
366
|
-
expect(r.success).toBe(true);
|
|
367
|
-
expectActionTag(r, 'click');
|
|
368
|
-
});
|
|
369
|
-
|
|
370
|
-
test('type output begins with [action: type]', async () => {
|
|
371
|
-
const r = await makeTool().execute(
|
|
372
|
-
{ action: 'type', text: 'x' },
|
|
373
|
-
{ toolConfig: FULL_ALLOW },
|
|
374
|
-
);
|
|
375
|
-
expect(r.success).toBe(true);
|
|
376
|
-
expectActionTag(r, 'type');
|
|
377
|
-
});
|
|
378
|
-
|
|
379
|
-
test('key_press output begins with [action: key_press]', async () => {
|
|
380
|
-
const r = await makeTool().execute(
|
|
381
|
-
{ action: 'key_press', keys: ['LeftWin'] },
|
|
382
|
-
{ toolConfig: FULL_ALLOW },
|
|
383
|
-
);
|
|
384
|
-
expect(r.success).toBe(true);
|
|
385
|
-
expectActionTag(r, 'key_press');
|
|
386
|
-
});
|
|
387
|
-
|
|
388
|
-
test('scroll output begins with [action: scroll]', async () => {
|
|
389
|
-
const r = await makeTool().execute(
|
|
390
|
-
{ action: 'scroll', x: 1, y: 1, dy: 5 },
|
|
391
|
-
{ toolConfig: FULL_ALLOW },
|
|
392
|
-
);
|
|
393
|
-
expect(r.success).toBe(true);
|
|
394
|
-
expectActionTag(r, 'scroll');
|
|
395
|
-
});
|
|
396
|
-
|
|
397
|
-
test('drag output begins with [action: drag]', async () => {
|
|
398
|
-
const r = await makeTool().execute(
|
|
399
|
-
{ action: 'drag', from: { x: 1, y: 1 }, to: { x: 2, y: 2 } },
|
|
400
|
-
{ toolConfig: FULL_ALLOW },
|
|
401
|
-
);
|
|
402
|
-
expect(r.success).toBe(true);
|
|
403
|
-
expectActionTag(r, 'drag');
|
|
404
|
-
});
|
|
405
|
-
|
|
406
|
-
test('list_windows output begins with [action: list_windows]', async () => {
|
|
407
|
-
const r = await makeTool().execute({ action: 'list_windows' }, { toolConfig: FULL_ALLOW });
|
|
408
|
-
expect(r.success).toBe(true);
|
|
409
|
-
expectActionTag(r, 'list_windows');
|
|
410
|
-
});
|
|
411
|
-
|
|
412
|
-
test('focus_window output begins with [action: focus_window]', async () => {
|
|
413
|
-
const r = await makeTool().execute(
|
|
414
|
-
{ action: 'focus_window', titleMatch: 'note' },
|
|
415
|
-
{ toolConfig: FULL_ALLOW },
|
|
416
|
-
);
|
|
417
|
-
expect(r.success).toBe(true);
|
|
418
|
-
expectActionTag(r, 'focus_window');
|
|
419
|
-
});
|
|
420
|
-
|
|
421
|
-
test('describe_capabilities output begins with [action: describe_capabilities]', async () => {
|
|
422
|
-
const r = await makeTool().execute({ action: 'describe_capabilities' }, { toolConfig: FULL_ALLOW });
|
|
423
|
-
expect(r.success).toBe(true);
|
|
424
|
-
expectActionTag(r, 'describe_capabilities');
|
|
425
|
-
});
|
|
426
|
-
|
|
427
|
-
test('failure outputs also tag the action (so an error mid-batch is unambiguous)', async () => {
|
|
428
|
-
const r = await makeTool().execute(
|
|
429
|
-
{ action: 'type', text: '' }, // invalid, triggers _fail
|
|
430
|
-
{ toolConfig: FULL_ALLOW },
|
|
431
|
-
);
|
|
432
|
-
expect(r.success).toBe(false);
|
|
433
|
-
expect(r.output).toMatch(/^\[action: type\]/);
|
|
434
|
-
expect(r.action).toBe('type');
|
|
435
|
-
});
|
|
436
|
-
|
|
437
|
-
test('grounding-failure click result tags the action', async () => {
|
|
438
|
-
const tool = makeTool({
|
|
439
|
-
groundResult: { answer: 'nope', usage: {}, elapsed_ms: 1 },
|
|
440
|
-
});
|
|
441
|
-
const r = await tool.execute(
|
|
442
|
-
{ action: 'click', intent: 'foo' },
|
|
443
|
-
{ toolConfig: FULL_ALLOW },
|
|
444
|
-
);
|
|
445
|
-
expect(r.success).toBe(false);
|
|
446
|
-
expect(r.output).toMatch(/^\[action: click\]/);
|
|
447
|
-
expect(r.action).toBe('click');
|
|
448
|
-
});
|
|
449
|
-
});
|
|
450
|
-
|
|
451
|
-
describe('DesktopTool — adapter construction', () => {
|
|
452
|
-
test('intent path without aiService → NOT_INITIALISED', async () => {
|
|
453
|
-
const tool = new DesktopTool({ osController: fakeOsc() });
|
|
454
|
-
tool._adapterCache = null; // force real build
|
|
455
|
-
const r = await tool.execute(
|
|
456
|
-
{ action: 'click', intent: 'foo' },
|
|
457
|
-
{ toolConfig: FULL_ALLOW },
|
|
458
|
-
);
|
|
459
|
-
expect(r.success).toBe(false);
|
|
460
|
-
expect(r.code).toBe('NOT_INITIALISED');
|
|
461
|
-
});
|
|
462
|
-
});
|
|
463
|
-
|
|
464
|
-
// ─── parseParameters ───────────────────────────────────────────────────
|
|
465
|
-
|
|
466
|
-
describe('DesktopTool — parseParameters', () => {
|
|
467
|
-
test('parses {parameters: ...} envelope', () => {
|
|
468
|
-
const tool = makeTool();
|
|
469
|
-
const out = tool.parseParameters('{"parameters": {"action": "screenshot"}}');
|
|
470
|
-
expect(out).toEqual({ action: 'screenshot' });
|
|
471
|
-
});
|
|
472
|
-
|
|
473
|
-
test('parses bare object', () => {
|
|
474
|
-
const tool = makeTool();
|
|
475
|
-
const out = tool.parseParameters('{"action": "click", "x": 1, "y": 2}');
|
|
476
|
-
expect(out).toEqual({ action: 'click', x: 1, y: 2 });
|
|
477
|
-
});
|
|
478
|
-
|
|
479
|
-
test('rejects non-JSON', () => {
|
|
480
|
-
const tool = makeTool();
|
|
481
|
-
expect(() => tool.parseParameters('<click/>')).toThrow(/JSON/);
|
|
482
|
-
});
|
|
483
|
-
});
|
|
484
|
-
|
|
485
|
-
// ─── drag screenshot reuse ─────────────────────────────────────────────
|
|
486
|
-
|
|
487
|
-
describe('DesktopTool — drag screenshot reuse', () => {
|
|
488
|
-
test('drag with two intents calls screenshot once, grounds twice', async () => {
|
|
489
|
-
const osc = fakeOsc();
|
|
490
|
-
const tool = makeTool({ osc });
|
|
491
|
-
tool._adapterCache = {
|
|
492
|
-
ground: jest.fn(async ({ intent }) => ({
|
|
493
|
-
answer: 'ok',
|
|
494
|
-
coords: intent === 'A' ? { x: 100, y: 100 } : { x: 200, y: 200 },
|
|
495
|
-
})),
|
|
496
|
-
};
|
|
497
|
-
const r = await tool.execute(
|
|
498
|
-
{ action: 'drag', fromIntent: 'A', toIntent: 'B' },
|
|
499
|
-
{ toolConfig: FULL_ALLOW },
|
|
500
|
-
);
|
|
501
|
-
expect(r.success).toBe(true);
|
|
502
|
-
expect(osc.screenshot).toHaveBeenCalledTimes(1); // reused for second leg
|
|
503
|
-
expect(tool._adapterCache.ground).toHaveBeenCalledTimes(2);
|
|
504
|
-
expect(r.from).toEqual({ x: 100, y: 100 });
|
|
505
|
-
expect(r.to).toEqual({ x: 200, y: 200 });
|
|
506
|
-
});
|
|
507
|
-
});
|
|
1
|
+
/**
|
|
2
|
+
* DesktopTool unit tests.
|
|
3
|
+
*
|
|
4
|
+
* All native deps mocked: osController + grounding adapter are stubs.
|
|
5
|
+
* Verifies:
|
|
6
|
+
* - kill switch (env)
|
|
7
|
+
* - per-agent allowlist
|
|
8
|
+
* - parameter dispatch + validation
|
|
9
|
+
* - intent → grounding → coord validation pipeline
|
|
10
|
+
* - drag's screenshot reuse optimisation
|
|
11
|
+
* - audit logging shape
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { jest, describe, test, expect, afterEach } from '@jest/globals';
|
|
15
|
+
import { DesktopTool, DesktopAction } from '../DesktopTool.js';
|
|
16
|
+
|
|
17
|
+
// ─── fakes ─────────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
function fakeOsc(overrides = {}) {
|
|
20
|
+
const calls = [];
|
|
21
|
+
const record = (name) => jest.fn(async (...args) => { calls.push({ name, args }); });
|
|
22
|
+
return {
|
|
23
|
+
screenshot: jest.fn(async () => Buffer.from('png-bytes')),
|
|
24
|
+
screenSize: jest.fn(async () => ({ width: 1920, height: 1080, dpr: 1 })),
|
|
25
|
+
mouseMove: record('mouseMove'),
|
|
26
|
+
mouseClick: record('mouseClick'),
|
|
27
|
+
mouseScroll: record('mouseScroll'),
|
|
28
|
+
mouseDrag: record('mouseDrag'),
|
|
29
|
+
typeText: record('typeText'),
|
|
30
|
+
keyPress: record('keyPress'),
|
|
31
|
+
listWindows: jest.fn(async () => [{ title: 'Notepad' }, { title: 'Browser' }]),
|
|
32
|
+
focusWindow: jest.fn(async ({ titleMatch }) => ({
|
|
33
|
+
focused: titleMatch === 'note' || titleMatch === 'Notepad',
|
|
34
|
+
title: 'Notepad',
|
|
35
|
+
})),
|
|
36
|
+
describeCapabilities: jest.fn(async () => ({
|
|
37
|
+
os: 'win32', display: null,
|
|
38
|
+
screenshot: true, mouseInput: true, keyboardInput: true, windowFocus: true,
|
|
39
|
+
degradedReason: null, nativeLoaded: true,
|
|
40
|
+
})),
|
|
41
|
+
_calls: calls,
|
|
42
|
+
...overrides,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** A stub aiService that carries a base url + a fake key manager. */
|
|
47
|
+
function fakeAIService() {
|
|
48
|
+
return {
|
|
49
|
+
baseUrl: 'https://api.test.loxia.ai',
|
|
50
|
+
config: { apiKey: 'test-loxia-key' },
|
|
51
|
+
apiKeyManager: {
|
|
52
|
+
getKeysForRequest: jest.fn(() => ({ loxiaApiKey: 'test-loxia-key' })),
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Build a DesktopTool with a stubbed grounding adapter that returns
|
|
59
|
+
* the coords we tell it to.
|
|
60
|
+
*/
|
|
61
|
+
function makeTool({ osc = fakeOsc(), groundResult, aiService = fakeAIService() } = {}) {
|
|
62
|
+
const tool = DesktopTool.withDependencies({
|
|
63
|
+
aiService,
|
|
64
|
+
osController: osc,
|
|
65
|
+
config: {},
|
|
66
|
+
logger: { info: jest.fn(), warn: jest.fn(), error: jest.fn() },
|
|
67
|
+
});
|
|
68
|
+
// Pre-seed adapter cache so we don't hit the registry.
|
|
69
|
+
tool._adapterCache = {
|
|
70
|
+
ground: jest.fn(async () => groundResult || {
|
|
71
|
+
answer: 'x=500, y=400',
|
|
72
|
+
coords: { x: 500, y: 400 },
|
|
73
|
+
usage: { input_tokens: 1, output_tokens: 1 },
|
|
74
|
+
elapsed_ms: 5,
|
|
75
|
+
}),
|
|
76
|
+
};
|
|
77
|
+
return tool;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const FULL_ALLOW = { allowedActions: Object.values(DesktopAction) };
|
|
81
|
+
|
|
82
|
+
// ─── kill switch / allowlist ───────────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
describe('DesktopTool — gating', () => {
|
|
85
|
+
const originalEnv = process.env.LOXIA_DESKTOP_TOOL_DISABLED;
|
|
86
|
+
afterEach(() => {
|
|
87
|
+
if (originalEnv === undefined) delete process.env.LOXIA_DESKTOP_TOOL_DISABLED;
|
|
88
|
+
else process.env.LOXIA_DESKTOP_TOOL_DISABLED = originalEnv;
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test('env kill switch short-circuits every call', async () => {
|
|
92
|
+
process.env.LOXIA_DESKTOP_TOOL_DISABLED = '1';
|
|
93
|
+
const tool = makeTool();
|
|
94
|
+
const r = await tool.execute({ action: 'screenshot' }, { toolConfig: FULL_ALLOW });
|
|
95
|
+
expect(r.success).toBe(false);
|
|
96
|
+
expect(r.code).toBe('DESKTOP_DISABLED');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('default (no allowedActions) refuses every action', async () => {
|
|
100
|
+
const tool = makeTool();
|
|
101
|
+
const r = await tool.execute({ action: 'screenshot' }, {});
|
|
102
|
+
expect(r.success).toBe(false);
|
|
103
|
+
expect(r.code).toBe('NOT_PERMITTED');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('explicit allowlist gates per action', async () => {
|
|
107
|
+
const tool = makeTool();
|
|
108
|
+
const r1 = await tool.execute(
|
|
109
|
+
{ action: 'screenshot' },
|
|
110
|
+
{ toolConfig: { allowedActions: ['screenshot'] } },
|
|
111
|
+
);
|
|
112
|
+
expect(r1.success).toBe(true);
|
|
113
|
+
|
|
114
|
+
const r2 = await tool.execute(
|
|
115
|
+
{ action: 'click', x: 10, y: 10 },
|
|
116
|
+
{ toolConfig: { allowedActions: ['screenshot'] } }, // click NOT in list
|
|
117
|
+
);
|
|
118
|
+
expect(r2.success).toBe(false);
|
|
119
|
+
expect(r2.code).toBe('NOT_PERMITTED');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test('unknown action returns INVALID_ACTION', async () => {
|
|
123
|
+
const tool = makeTool();
|
|
124
|
+
const r = await tool.execute({ action: 'self_destruct' }, { toolConfig: FULL_ALLOW });
|
|
125
|
+
expect(r.success).toBe(false);
|
|
126
|
+
expect(r.code).toBe('INVALID_ACTION');
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// ─── action dispatch ───────────────────────────────────────────────────
|
|
131
|
+
|
|
132
|
+
describe('DesktopTool — actions', () => {
|
|
133
|
+
test('screenshot saves to disk and returns the path (no inline base64)', async () => {
|
|
134
|
+
const tool = makeTool();
|
|
135
|
+
const r = await tool.execute({ action: 'screenshot' }, { toolConfig: FULL_ALLOW });
|
|
136
|
+
expect(r.success).toBe(true);
|
|
137
|
+
// Critical: we MUST NOT return raw base64 bytes in the tool result —
|
|
138
|
+
// the agent can't read PNG bytes from text, and dumping 200 KB of
|
|
139
|
+
// base64 into the conversation derails the next model turn.
|
|
140
|
+
expect(r).not.toHaveProperty('screenshotBase64');
|
|
141
|
+
expect(typeof r.screenshotPath).toBe('string');
|
|
142
|
+
expect(r.screenshotPath).toMatch(/screenshot-\d+\.png$/);
|
|
143
|
+
expect(r.size).toMatchObject({ width: 1920, height: 1080 });
|
|
144
|
+
expect(r.bytes).toBe(Buffer.from('png-bytes').length);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test('click with raw (x,y) skips grounding', async () => {
|
|
148
|
+
const osc = fakeOsc();
|
|
149
|
+
const tool = makeTool({ osc });
|
|
150
|
+
const r = await tool.execute(
|
|
151
|
+
{ action: 'click', x: 100, y: 200, button: 'right' },
|
|
152
|
+
{ toolConfig: FULL_ALLOW },
|
|
153
|
+
);
|
|
154
|
+
expect(r.success).toBe(true);
|
|
155
|
+
expect(r.coords).toEqual({ x: 100, y: 200 });
|
|
156
|
+
expect(r.groundedFrom).toBeNull();
|
|
157
|
+
expect(osc.mouseClick).toHaveBeenCalledWith(100, 200, { button: 'right', count: undefined });
|
|
158
|
+
// Grounding adapter NOT consulted.
|
|
159
|
+
expect(tool._adapterCache.ground).not.toHaveBeenCalled();
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test('click with intent grounds, then clicks', async () => {
|
|
163
|
+
const osc = fakeOsc();
|
|
164
|
+
const tool = makeTool({ osc });
|
|
165
|
+
const r = await tool.execute(
|
|
166
|
+
{ action: 'click', intent: 'the Save button' },
|
|
167
|
+
{ toolConfig: FULL_ALLOW },
|
|
168
|
+
);
|
|
169
|
+
expect(r.success).toBe(true);
|
|
170
|
+
expect(r.coords).toEqual({ x: 500, y: 400 });
|
|
171
|
+
expect(r.groundedFrom).toBe('the Save button');
|
|
172
|
+
expect(tool._adapterCache.ground).toHaveBeenCalledTimes(1);
|
|
173
|
+
expect(osc.mouseClick).toHaveBeenCalledWith(500, 400, expect.any(Object));
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test('click with neither coords nor intent fails INVALID_INPUT', async () => {
|
|
177
|
+
const tool = makeTool();
|
|
178
|
+
const r = await tool.execute({ action: 'click' }, { toolConfig: FULL_ALLOW });
|
|
179
|
+
expect(r.success).toBe(false);
|
|
180
|
+
expect(r.code).toBe('INVALID_INPUT');
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test('grounding miss (no coords parsed) → GROUNDING_FAILED', async () => {
|
|
184
|
+
const tool = makeTool({
|
|
185
|
+
groundResult: { answer: 'I have no idea where that is', usage: {}, elapsed_ms: 1 },
|
|
186
|
+
});
|
|
187
|
+
const r = await tool.execute(
|
|
188
|
+
{ action: 'click', intent: 'mystery button' },
|
|
189
|
+
{ toolConfig: FULL_ALLOW },
|
|
190
|
+
);
|
|
191
|
+
expect(r.success).toBe(false);
|
|
192
|
+
expect(r.code).toBe('GROUNDING_FAILED');
|
|
193
|
+
expect(r.error).toMatch(/no idea/);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test('grounded coords outside screen → COORDS_OUT_OF_BOUNDS', async () => {
|
|
197
|
+
const tool = makeTool({
|
|
198
|
+
groundResult: { answer: 'x=9999, y=9999', coords: { x: 9999, y: 9999 } },
|
|
199
|
+
});
|
|
200
|
+
const r = await tool.execute(
|
|
201
|
+
{ action: 'click', intent: 'somewhere' },
|
|
202
|
+
{ toolConfig: FULL_ALLOW },
|
|
203
|
+
);
|
|
204
|
+
expect(r.success).toBe(false);
|
|
205
|
+
expect(r.code).toBe('COORDS_OUT_OF_BOUNDS');
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test('type forwards text and length', async () => {
|
|
209
|
+
const osc = fakeOsc();
|
|
210
|
+
const tool = makeTool({ osc });
|
|
211
|
+
const r = await tool.execute(
|
|
212
|
+
{ action: 'type', text: 'hello world' },
|
|
213
|
+
{ toolConfig: FULL_ALLOW },
|
|
214
|
+
);
|
|
215
|
+
expect(r.success).toBe(true);
|
|
216
|
+
expect(osc.typeText).toHaveBeenCalledWith('hello world', expect.any(Object));
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test('type with empty text → INVALID_INPUT', async () => {
|
|
220
|
+
const tool = makeTool();
|
|
221
|
+
const r = await tool.execute({ action: 'type', text: '' }, { toolConfig: FULL_ALLOW });
|
|
222
|
+
expect(r.success).toBe(false);
|
|
223
|
+
expect(r.code).toBe('INVALID_INPUT');
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test('key_press forwards keys array', async () => {
|
|
227
|
+
const osc = fakeOsc();
|
|
228
|
+
const tool = makeTool({ osc });
|
|
229
|
+
const r = await tool.execute(
|
|
230
|
+
{ action: 'key_press', keys: ['Control', 'S'] },
|
|
231
|
+
{ toolConfig: FULL_ALLOW },
|
|
232
|
+
);
|
|
233
|
+
expect(r.success).toBe(true);
|
|
234
|
+
expect(osc.keyPress).toHaveBeenCalledWith(['Control', 'S']);
|
|
235
|
+
expect(r.output).toMatch(/Control\+S/);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test('key_press empty keys → INVALID_INPUT', async () => {
|
|
239
|
+
const tool = makeTool();
|
|
240
|
+
const r = await tool.execute({ action: 'key_press', keys: [] }, { toolConfig: FULL_ALLOW });
|
|
241
|
+
expect(r.success).toBe(false);
|
|
242
|
+
expect(r.code).toBe('INVALID_INPUT');
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test('scroll requires dy', async () => {
|
|
246
|
+
const tool = makeTool();
|
|
247
|
+
const r = await tool.execute(
|
|
248
|
+
{ action: 'scroll', x: 100, y: 100 },
|
|
249
|
+
{ toolConfig: FULL_ALLOW },
|
|
250
|
+
);
|
|
251
|
+
expect(r.success).toBe(false);
|
|
252
|
+
expect(r.code).toBe('INVALID_INPUT');
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
test('scroll with raw coords + dy works', async () => {
|
|
256
|
+
const osc = fakeOsc();
|
|
257
|
+
const tool = makeTool({ osc });
|
|
258
|
+
const r = await tool.execute(
|
|
259
|
+
{ action: 'scroll', x: 100, y: 100, dy: -3 },
|
|
260
|
+
{ toolConfig: FULL_ALLOW },
|
|
261
|
+
);
|
|
262
|
+
expect(r.success).toBe(true);
|
|
263
|
+
expect(osc.mouseScroll).toHaveBeenCalledWith(100, 100, -3);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test('drag with raw from/to → two coord pairs, no grounding', async () => {
|
|
267
|
+
const osc = fakeOsc();
|
|
268
|
+
const tool = makeTool({ osc });
|
|
269
|
+
const r = await tool.execute(
|
|
270
|
+
{ action: 'drag', from: { x: 10, y: 20 }, to: { x: 50, y: 60 } },
|
|
271
|
+
{ toolConfig: FULL_ALLOW },
|
|
272
|
+
);
|
|
273
|
+
expect(r.success).toBe(true);
|
|
274
|
+
expect(r.from).toEqual({ x: 10, y: 20 });
|
|
275
|
+
expect(r.to).toEqual({ x: 50, y: 60 });
|
|
276
|
+
expect(osc.mouseDrag).toHaveBeenCalledTimes(1);
|
|
277
|
+
expect(tool._adapterCache.ground).not.toHaveBeenCalled();
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
test('list_windows returns titles, filters empty, caps to 50', async () => {
|
|
281
|
+
// Build a stub that mimics a real OS — most "windows" have empty
|
|
282
|
+
// titles (system handles) and the OS reports way more than 50.
|
|
283
|
+
const osc = fakeOsc({
|
|
284
|
+
listWindows: jest.fn(async () => {
|
|
285
|
+
const out = [{ title: 'Notepad' }, { title: 'Browser' }];
|
|
286
|
+
for (let i = 0; i < 200; i++) out.push({ title: '' }); // untitled handles
|
|
287
|
+
for (let i = 0; i < 60; i++) out.push({ title: `App-${i}` }); // 60 real titles
|
|
288
|
+
return out;
|
|
289
|
+
}),
|
|
290
|
+
});
|
|
291
|
+
const tool = makeTool({ osc });
|
|
292
|
+
const r = await tool.execute({ action: 'list_windows' }, { toolConfig: FULL_ALLOW });
|
|
293
|
+
expect(r.success).toBe(true);
|
|
294
|
+
expect(r.totalCount).toBe(262);
|
|
295
|
+
expect(r.titledCount).toBe(62); // 2 + 60 titled
|
|
296
|
+
expect(r.windows).toHaveLength(50); // capped
|
|
297
|
+
expect(r.truncated).toBe(true);
|
|
298
|
+
expect(r.output).toMatch(/Notepad/);
|
|
299
|
+
expect(r.output).toMatch(/filtered 200 untitled/);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
test('focus_window without titleMatch → INVALID_INPUT', async () => {
|
|
303
|
+
const tool = makeTool();
|
|
304
|
+
const r = await tool.execute({ action: 'focus_window' }, { toolConfig: FULL_ALLOW });
|
|
305
|
+
expect(r.success).toBe(false);
|
|
306
|
+
expect(r.code).toBe('INVALID_INPUT');
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
test('focus_window match found', async () => {
|
|
310
|
+
const tool = makeTool();
|
|
311
|
+
const r = await tool.execute(
|
|
312
|
+
{ action: 'focus_window', titleMatch: 'note' },
|
|
313
|
+
{ toolConfig: FULL_ALLOW },
|
|
314
|
+
);
|
|
315
|
+
expect(r.success).toBe(true);
|
|
316
|
+
expect(r.focused).toBe(true);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
test('describe_capabilities returns the capability snapshot', async () => {
|
|
320
|
+
const tool = makeTool();
|
|
321
|
+
const r = await tool.execute({ action: 'describe_capabilities' }, { toolConfig: FULL_ALLOW });
|
|
322
|
+
expect(r.success).toBe(true);
|
|
323
|
+
expect(r.capabilities.os).toBe('win32');
|
|
324
|
+
expect(r.output).toMatch(/screenshot=true/);
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
// ─── adapter build ─────────────────────────────────────────────────────
|
|
329
|
+
|
|
330
|
+
// ─── REGRESSION: every tool result is action-tagged ────────────────────
|
|
331
|
+
//
|
|
332
|
+
// Background: in a real run on 2026-05-13 the agent thought a click on
|
|
333
|
+
// the Netflix taskbar icon had succeeded because msg[5] in the
|
|
334
|
+
// conversation came back with `success:true` and a screenshot path —
|
|
335
|
+
// but msg[5] was actually a stale duplicate-screenshot result that
|
|
336
|
+
// landed where the click result was expected (msg[7] later carried the
|
|
337
|
+
// real click failure, by which point the agent had moved on).
|
|
338
|
+
//
|
|
339
|
+
// Fix: every action's result `output` MUST begin with "[action: <id>]"
|
|
340
|
+
// so the model can ALWAYS map a tool result back to the action it
|
|
341
|
+
// belongs to, even when results arrive out-of-order in batched messages.
|
|
342
|
+
//
|
|
343
|
+
// These tests lock the invariant. Adding a new action without tagging
|
|
344
|
+
// the output will fail here and force the author to update both
|
|
345
|
+
// places.
|
|
346
|
+
|
|
347
|
+
describe('REGRESSION: every desktop result includes its action tag', () => {
|
|
348
|
+
function expectActionTag(result, expected) {
|
|
349
|
+
expect(typeof result.output).toBe('string');
|
|
350
|
+
expect(result.output.startsWith(`[action: ${expected}]`)).toBe(true);
|
|
351
|
+
// Structured field too, so consumers don't have to parse the prefix.
|
|
352
|
+
expect(result.action).toBe(expected);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
test('screenshot output begins with [action: screenshot]', async () => {
|
|
356
|
+
const r = await makeTool().execute({ action: 'screenshot' }, { toolConfig: FULL_ALLOW });
|
|
357
|
+
expect(r.success).toBe(true);
|
|
358
|
+
expectActionTag(r, 'screenshot');
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
test('click output begins with [action: click]', async () => {
|
|
362
|
+
const r = await makeTool().execute(
|
|
363
|
+
{ action: 'click', x: 100, y: 200 },
|
|
364
|
+
{ toolConfig: FULL_ALLOW },
|
|
365
|
+
);
|
|
366
|
+
expect(r.success).toBe(true);
|
|
367
|
+
expectActionTag(r, 'click');
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
test('type output begins with [action: type]', async () => {
|
|
371
|
+
const r = await makeTool().execute(
|
|
372
|
+
{ action: 'type', text: 'x' },
|
|
373
|
+
{ toolConfig: FULL_ALLOW },
|
|
374
|
+
);
|
|
375
|
+
expect(r.success).toBe(true);
|
|
376
|
+
expectActionTag(r, 'type');
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
test('key_press output begins with [action: key_press]', async () => {
|
|
380
|
+
const r = await makeTool().execute(
|
|
381
|
+
{ action: 'key_press', keys: ['LeftWin'] },
|
|
382
|
+
{ toolConfig: FULL_ALLOW },
|
|
383
|
+
);
|
|
384
|
+
expect(r.success).toBe(true);
|
|
385
|
+
expectActionTag(r, 'key_press');
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
test('scroll output begins with [action: scroll]', async () => {
|
|
389
|
+
const r = await makeTool().execute(
|
|
390
|
+
{ action: 'scroll', x: 1, y: 1, dy: 5 },
|
|
391
|
+
{ toolConfig: FULL_ALLOW },
|
|
392
|
+
);
|
|
393
|
+
expect(r.success).toBe(true);
|
|
394
|
+
expectActionTag(r, 'scroll');
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
test('drag output begins with [action: drag]', async () => {
|
|
398
|
+
const r = await makeTool().execute(
|
|
399
|
+
{ action: 'drag', from: { x: 1, y: 1 }, to: { x: 2, y: 2 } },
|
|
400
|
+
{ toolConfig: FULL_ALLOW },
|
|
401
|
+
);
|
|
402
|
+
expect(r.success).toBe(true);
|
|
403
|
+
expectActionTag(r, 'drag');
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
test('list_windows output begins with [action: list_windows]', async () => {
|
|
407
|
+
const r = await makeTool().execute({ action: 'list_windows' }, { toolConfig: FULL_ALLOW });
|
|
408
|
+
expect(r.success).toBe(true);
|
|
409
|
+
expectActionTag(r, 'list_windows');
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
test('focus_window output begins with [action: focus_window]', async () => {
|
|
413
|
+
const r = await makeTool().execute(
|
|
414
|
+
{ action: 'focus_window', titleMatch: 'note' },
|
|
415
|
+
{ toolConfig: FULL_ALLOW },
|
|
416
|
+
);
|
|
417
|
+
expect(r.success).toBe(true);
|
|
418
|
+
expectActionTag(r, 'focus_window');
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
test('describe_capabilities output begins with [action: describe_capabilities]', async () => {
|
|
422
|
+
const r = await makeTool().execute({ action: 'describe_capabilities' }, { toolConfig: FULL_ALLOW });
|
|
423
|
+
expect(r.success).toBe(true);
|
|
424
|
+
expectActionTag(r, 'describe_capabilities');
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
test('failure outputs also tag the action (so an error mid-batch is unambiguous)', async () => {
|
|
428
|
+
const r = await makeTool().execute(
|
|
429
|
+
{ action: 'type', text: '' }, // invalid, triggers _fail
|
|
430
|
+
{ toolConfig: FULL_ALLOW },
|
|
431
|
+
);
|
|
432
|
+
expect(r.success).toBe(false);
|
|
433
|
+
expect(r.output).toMatch(/^\[action: type\]/);
|
|
434
|
+
expect(r.action).toBe('type');
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
test('grounding-failure click result tags the action', async () => {
|
|
438
|
+
const tool = makeTool({
|
|
439
|
+
groundResult: { answer: 'nope', usage: {}, elapsed_ms: 1 },
|
|
440
|
+
});
|
|
441
|
+
const r = await tool.execute(
|
|
442
|
+
{ action: 'click', intent: 'foo' },
|
|
443
|
+
{ toolConfig: FULL_ALLOW },
|
|
444
|
+
);
|
|
445
|
+
expect(r.success).toBe(false);
|
|
446
|
+
expect(r.output).toMatch(/^\[action: click\]/);
|
|
447
|
+
expect(r.action).toBe('click');
|
|
448
|
+
});
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
describe('DesktopTool — adapter construction', () => {
|
|
452
|
+
test('intent path without aiService → NOT_INITIALISED', async () => {
|
|
453
|
+
const tool = new DesktopTool({ osController: fakeOsc() });
|
|
454
|
+
tool._adapterCache = null; // force real build
|
|
455
|
+
const r = await tool.execute(
|
|
456
|
+
{ action: 'click', intent: 'foo' },
|
|
457
|
+
{ toolConfig: FULL_ALLOW },
|
|
458
|
+
);
|
|
459
|
+
expect(r.success).toBe(false);
|
|
460
|
+
expect(r.code).toBe('NOT_INITIALISED');
|
|
461
|
+
});
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
// ─── parseParameters ───────────────────────────────────────────────────
|
|
465
|
+
|
|
466
|
+
describe('DesktopTool — parseParameters', () => {
|
|
467
|
+
test('parses {parameters: ...} envelope', () => {
|
|
468
|
+
const tool = makeTool();
|
|
469
|
+
const out = tool.parseParameters('{"parameters": {"action": "screenshot"}}');
|
|
470
|
+
expect(out).toEqual({ action: 'screenshot' });
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
test('parses bare object', () => {
|
|
474
|
+
const tool = makeTool();
|
|
475
|
+
const out = tool.parseParameters('{"action": "click", "x": 1, "y": 2}');
|
|
476
|
+
expect(out).toEqual({ action: 'click', x: 1, y: 2 });
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
test('rejects non-JSON', () => {
|
|
480
|
+
const tool = makeTool();
|
|
481
|
+
expect(() => tool.parseParameters('<click/>')).toThrow(/JSON/);
|
|
482
|
+
});
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
// ─── drag screenshot reuse ─────────────────────────────────────────────
|
|
486
|
+
|
|
487
|
+
describe('DesktopTool — drag screenshot reuse', () => {
|
|
488
|
+
test('drag with two intents calls screenshot once, grounds twice', async () => {
|
|
489
|
+
const osc = fakeOsc();
|
|
490
|
+
const tool = makeTool({ osc });
|
|
491
|
+
tool._adapterCache = {
|
|
492
|
+
ground: jest.fn(async ({ intent }) => ({
|
|
493
|
+
answer: 'ok',
|
|
494
|
+
coords: intent === 'A' ? { x: 100, y: 100 } : { x: 200, y: 200 },
|
|
495
|
+
})),
|
|
496
|
+
};
|
|
497
|
+
const r = await tool.execute(
|
|
498
|
+
{ action: 'drag', fromIntent: 'A', toIntent: 'B' },
|
|
499
|
+
{ toolConfig: FULL_ALLOW },
|
|
500
|
+
);
|
|
501
|
+
expect(r.success).toBe(true);
|
|
502
|
+
expect(osc.screenshot).toHaveBeenCalledTimes(1); // reused for second leg
|
|
503
|
+
expect(tool._adapterCache.ground).toHaveBeenCalledTimes(2);
|
|
504
|
+
expect(r.from).toEqual({ x: 100, y: 100 });
|
|
505
|
+
expect(r.to).toEqual({ x: 200, y: 200 });
|
|
506
|
+
});
|
|
507
|
+
});
|