@xenon-device-management/xenon 2.13.2 → 2.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (350) hide show
  1. package/README.md +17 -8
  2. package/lib/package.json +5 -5
  3. package/lib/public/assets/{AnnotationOverlay-BXruClDk.js → AnnotationOverlay-Btq3dZRI.js} +1 -1
  4. package/lib/public/assets/{ApiKeyGate-qTS4LPLC.js → ApiKeyGate-C9OoAgg_.js} +1 -1
  5. package/lib/public/assets/DeviceMosaicView-DBC_xczB.js +2 -0
  6. package/lib/public/assets/{EmptyState-Cr17PdDv.js → EmptyState-DqWU2-vM.js} +1 -1
  7. package/lib/public/assets/{FieldGroup-Bvn08svv.js → FieldGroup-CveQ3xrA.js} +1 -1
  8. package/lib/public/assets/{FilterMenu-DDSO8aDf.js → FilterMenu-BfKDSfHz.js} +1 -1
  9. package/lib/public/assets/{Menu-B-Qn2X3T.js → Menu-CtdCybzA.js} +1 -1
  10. package/lib/public/assets/{Modal-Cg9V5JYU.js → Modal-Bsvj1tm2.js} +1 -1
  11. package/lib/public/assets/{RecordingPage-BTUs9VTW.js → RecordingPage-CRofOiyx.js} +1 -1
  12. package/lib/public/assets/{RecordingsPage-DYcfxITK.js → RecordingsPage-jNQHb2IR.js} +2 -2
  13. package/lib/public/assets/{SegmentedControl-Zy7suhT5.js → SegmentedControl-C8HtTha-.js} +1 -1
  14. package/lib/public/assets/{SettingCard-CWYmycoe.js → SettingCard-D8K1LZNo.js} +1 -1
  15. package/lib/public/assets/{Table-io9fwa98.js → Table-zNTX92Pn.js} +1 -1
  16. package/lib/public/assets/{activity-Bdf_uIWl.js → activity-CJ8LwZq9.js} +1 -1
  17. package/lib/public/assets/ai-settings-CgxQZqrc.js +16 -0
  18. package/lib/public/assets/{api-keys-CPqtVpm3.js → api-keys-ECpo-zTo.js} +1 -1
  19. package/lib/public/assets/{apps-BgZan0H9.js → apps-BjGymyf3.js} +2 -2
  20. package/lib/public/assets/{arrow-left-B27ckB31.js → arrow-left-BWfCbeHP.js} +1 -1
  21. package/lib/public/assets/{arrow-right-DOF8ITCz.js → arrow-right-DC7UGf_n.js} +1 -1
  22. package/lib/public/assets/{arrow-up-right-eFG5KTFS.js → arrow-up-right-BOjwybDk.js} +1 -1
  23. package/lib/public/assets/{auth-shell-DlC3_27j.js → auth-shell-CxMTo9aH.js} +2 -2
  24. package/lib/public/assets/{builds-page-BuXpPfKo.js → builds-page-DOCm4HkA.js} +1 -1
  25. package/lib/public/assets/{button-fIW8owMI.js → button-Cg08-EMT.js} +1 -1
  26. package/lib/public/assets/{calendar-WNjmrF-a.js → calendar-CA1ikLJs.js} +1 -1
  27. package/lib/public/assets/{check-BdTh1Sq7.js → check-BMTaH72W.js} +1 -1
  28. package/lib/public/assets/{chevron-right-B4BKlsqk.js → chevron-right-B2QOv9Nx.js} +1 -1
  29. package/lib/public/assets/{circle-check-zJU80_EF.js → circle-check-CYXBoUqT.js} +1 -1
  30. package/lib/public/assets/{circle-x-5Uwquo0h.js → circle-x-DTwnSAnl.js} +1 -1
  31. package/lib/public/assets/{clock-BtUprDsZ.js → clock-C8_HViQP.js} +1 -1
  32. package/lib/public/assets/{copy-DGSbZI3u.js → copy-Be7LNMXV.js} +1 -1
  33. package/lib/public/assets/derive-UnLRp4r3.js +1 -0
  34. package/lib/public/assets/device-explorer-BOqtfGXz.js +282 -0
  35. package/lib/public/assets/{download-WG6l7BgD.js → download-B56lwxWP.js} +1 -1
  36. package/lib/public/assets/forgot-password-CCU6bzLn.js +1 -0
  37. package/lib/public/assets/h264Stream-C-OtmIuk.js +26 -0
  38. package/lib/public/assets/index-B9KQ6LsY.js +15 -0
  39. package/lib/public/assets/{index-DI1fGIyj.js → index-BOxe43p1.js} +5 -5
  40. package/lib/public/assets/{index-HDQVe1gi.js → index-CfxsUslp.js} +1 -1
  41. package/lib/public/assets/{input-BjDRKCea.js → input-CZHFjbSg.js} +1 -1
  42. package/lib/public/assets/{line-chart-C0zdgd0v.js → line-chart-Gl_Dz3X_.js} +1 -1
  43. package/lib/public/assets/{list-checks-BdW5CjM8.js → list-checks-CCgjeyGY.js} +1 -1
  44. package/lib/public/assets/{lock-Dy3L4I8E.js → lock-CkOwgDcx.js} +1 -1
  45. package/lib/public/assets/{login-BHbY9CLG.js → login-D5cc4w7M.js} +1 -1
  46. package/lib/public/assets/maintenance-settings-t7qr3fvX.js +6 -0
  47. package/lib/public/assets/{monitor-8xjkeHHi.js → monitor-DEJixKQO.js} +1 -1
  48. package/lib/public/assets/{mouse-pointer-2-CXdKMXQ8.js → mouse-pointer-2-CtOrk4M8.js} +1 -1
  49. package/lib/public/assets/{network-Cj8JOKON.js → network-Dii9qdvk.js} +1 -1
  50. package/lib/public/assets/{overview-CTg7F0im.js → overview-rolAACrO.js} +1 -1
  51. package/lib/public/assets/{page-header--aTyJXq6.js → page-header-COQZxJfR.js} +1 -1
  52. package/lib/public/assets/{play-B-FodgHV.js → play-DrN1Se_R.js} +1 -1
  53. package/lib/public/assets/{plus-CsIk10Cq.js → plus-5aq-9uRx.js} +1 -1
  54. package/lib/public/assets/{profile-page-DkFKkMfl.js → profile-page-6i5kY6kF.js} +1 -1
  55. package/lib/public/assets/{reset-password-I2-zqLlI.js → reset-password-sp2IBrc7.js} +1 -1
  56. package/lib/public/assets/runbook-page-DJDjeopU.js +253 -0
  57. package/lib/public/assets/{select-DvCSv25z.js → select-Bpyn7rWO.js} +1 -1
  58. package/lib/public/assets/{selector-detail-redirect-BLVfMJs_.js → selector-detail-redirect-BH74vmKv.js} +1 -1
  59. package/lib/public/assets/{selector-health-page-oqadRokn.js → selector-health-page-Cw_g218H.js} +2 -2
  60. package/lib/public/assets/session-detail-page-AZJE6XXE.js +4 -0
  61. package/lib/public/assets/settings-C3uyGhyc.js +6 -0
  62. package/lib/public/assets/{settings-Dep_3kbA.css → settings-K8UHWMdI.css} +1 -1
  63. package/lib/public/assets/{stat-tile-DV9Ix-4O.js → stat-tile-xVqAKIlI.js} +1 -1
  64. package/lib/public/assets/{tablet-6b_SEN2X.js → tablet-BxUScAeO.js} +1 -1
  65. package/lib/public/assets/{teams-BjZyfuHh.js → teams-B5ljGkY2.js} +1 -1
  66. package/lib/public/assets/{trash-2-CTZpPSdd.js → trash-2-B701i8sy.js} +1 -1
  67. package/lib/public/assets/{upload-P3XzwQUb.js → upload-C2gr44RS.js} +1 -1
  68. package/lib/public/assets/{use-builds-data-1cOCMf-z.js → use-builds-data-NeQOFEoy.js} +1 -1
  69. package/lib/public/assets/{use-password-reset-mode-DgF9w088.js → use-password-reset-mode-DEzJq5ef.js} +1 -1
  70. package/lib/public/assets/{users-DMF9Mk5H.js → users-D_vv_yz4.js} +1 -1
  71. package/lib/public/assets/{users-CiNTKUiz.js → users-MY6eNe6s.js} +1 -1
  72. package/lib/public/assets/{video-off-CHp1KU_T.js → video-off-OK1lNZQn.js} +1 -1
  73. package/lib/public/assets/webhook-settings-BU-C9lBf.js +1 -0
  74. package/lib/public/assets/{zap-DRz0tDq5.js → zap-DO9ry1iz.js} +1 -1
  75. package/lib/public/index.html +1 -1
  76. package/lib/schema.json +23 -25
  77. package/lib/src/XenonCapabilityManager.js +22 -5
  78. package/lib/src/app/dashboardPluginLink.js +117 -0
  79. package/lib/src/app/index.js +13 -34
  80. package/lib/src/app/openapi/control.yaml +73 -20
  81. package/lib/src/app/openapi/grid.yaml +31 -7
  82. package/lib/src/app/openapi/identity.yaml +103 -31
  83. package/lib/src/app/openapi/platform.yaml +165 -39
  84. package/lib/src/app/openapi/sessions.yaml +10 -3
  85. package/lib/src/app/routers/apikeys.js +14 -2
  86. package/lib/src/app/routers/auth.js +29 -5
  87. package/lib/src/app/routers/control.js +20 -2
  88. package/lib/src/app/routers/dashboard.js +69 -26
  89. package/lib/src/app/routers/grid.js +37 -13
  90. package/lib/src/app/routers/profile.js +21 -0
  91. package/lib/src/app/routers/streamType.js +7 -2
  92. package/lib/src/app/routers/users.js +10 -5
  93. package/lib/src/app/routers/webhook.js +8 -4
  94. package/lib/src/app/swagger.js +2 -0
  95. package/lib/src/app/ws/h264StreamWs.js +10 -1
  96. package/lib/src/config.js +51 -5
  97. package/lib/src/dashboard/commands.js +185 -141
  98. package/lib/src/dashboard/event-manager.js +88 -67
  99. package/lib/src/dashboard/services/failure-analysis-service.js +136 -45
  100. package/lib/src/dashboard/sessionCommandSummary.js +39 -0
  101. package/lib/src/data-service/appleFamily.js +43 -0
  102. package/lib/src/data-service/db.js +11 -0
  103. package/lib/src/data-service/device-service.js +6 -1
  104. package/lib/src/data-service/device-store.js +76 -3
  105. package/lib/src/data-service/deviceClaims.js +37 -0
  106. package/lib/src/data-service/deviceFieldOwners.js +25 -11
  107. package/lib/src/data-service/deviceSettings.js +79 -0
  108. package/lib/src/data-service/prisma-store.js +169 -15
  109. package/lib/src/data-service/web-config-service.js +50 -92
  110. package/lib/src/device-managers/AndroidDeviceManager.js +9 -31
  111. package/lib/src/device-managers/android/AndroidH264StreamService.js +69 -20
  112. package/lib/src/device-managers/android/H264Multiplexer.js +38 -4
  113. package/lib/src/device-managers/ios/WDAClient.js +44 -43
  114. package/lib/src/device-managers/shellCommands.js +96 -0
  115. package/lib/src/device-utils.js +114 -40
  116. package/lib/src/gateway/defaultGateway.js +1 -1
  117. package/lib/src/gateway/forwardToNode.js +2 -0
  118. package/lib/src/gateway/healReport.js +87 -0
  119. package/lib/src/gateway/sessionGateway.js +12 -2
  120. package/lib/src/generated/client/edge.js +18 -3
  121. package/lib/src/generated/client/index-browser.js +15 -0
  122. package/lib/src/generated/client/index.d.ts +2086 -379
  123. package/lib/src/generated/client/index.js +18 -3
  124. package/lib/src/generated/client/package.json +1 -1
  125. package/lib/src/generated/client/schema.prisma +38 -7
  126. package/lib/src/generated/client/wasm.js +15 -0
  127. package/lib/src/interceptors/CommandInterceptor.js +271 -188
  128. package/lib/src/interceptors/xenonScripts.js +55 -0
  129. package/lib/src/interfaces/IPluginArgs.js +1 -2
  130. package/lib/src/middleware/authMiddleware.js +15 -3
  131. package/lib/src/middleware/commandCaller.js +3 -3
  132. package/lib/src/middleware/verifyCredential.js +58 -1
  133. package/lib/src/plugin.js +23 -4
  134. package/lib/src/prisma.js +1 -1
  135. package/lib/src/scripts/database-check.js +124 -0
  136. package/lib/src/services/AICommandService.js +9 -2
  137. package/lib/src/services/AIService.js +281 -32
  138. package/lib/src/services/CleanupService.js +6 -1
  139. package/lib/src/services/EmailService.js +29 -0
  140. package/lib/src/services/InterceptorService.js +70 -13
  141. package/lib/src/services/NetworkConditioningService.js +85 -88
  142. package/lib/src/services/NotificationService.js +36 -29
  143. package/lib/src/services/OrphanSweeper.js +7 -0
  144. package/lib/src/services/PortAllocator.js +5 -0
  145. package/lib/src/services/SelectorVerificationJob.js +15 -4
  146. package/lib/src/services/ServerManager.js +105 -9
  147. package/lib/src/services/SessionLifecycleService.js +133 -75
  148. package/lib/src/services/ShutdownCoordinator.js +33 -0
  149. package/lib/src/services/SocketServer.js +19 -3
  150. package/lib/src/services/TeamService.js +20 -3
  151. package/lib/src/services/bug-report/video-slice.js +4 -1
  152. package/lib/src/services/device-access/leaseOverride.js +7 -5
  153. package/lib/src/services/healing/FuzzyXmlHealingProvider.js +51 -0
  154. package/lib/src/services/healing/HealEtalonService.js +3 -2
  155. package/lib/src/services/healing/HealingOrchestrator.js +85 -71
  156. package/lib/src/services/healing/OcrHealingProvider.js +41 -64
  157. package/lib/src/services/healing/ResilioTreeHealingProvider.js +41 -50
  158. package/lib/src/services/healing/VisualAiHealingProvider.js +1 -1
  159. package/lib/src/services/healing/fingerprintIdentity.js +126 -0
  160. package/lib/src/services/healing/resilioPath.js +274 -0
  161. package/lib/src/services/interceptor/AndroidProxyAdapter.js +18 -0
  162. package/lib/src/services/interceptor/interceptorOptions.js +44 -0
  163. package/lib/src/services/logcat/SessionDeviceLogs.js +271 -0
  164. package/lib/src/services/logcat/deviceLogBook.js +163 -0
  165. package/lib/src/services/network/AndroidRadios.js +71 -0
  166. package/lib/src/services/network/PhoneNetworkLedger.js +184 -0
  167. package/lib/src/services/network/PhoneNetworkRestore.js +330 -0
  168. package/lib/src/services/network/adbForPhone.js +60 -0
  169. package/lib/src/services/network/phoneNetworkLock.js +20 -0
  170. package/lib/src/services/network/xenonProxy.js +107 -0
  171. package/lib/src/services/ocr/ocrData.js +118 -0
  172. package/lib/src/services/ocr/vendor/LICENSE-tessdata.txt +202 -0
  173. package/lib/src/services/ocr/vendor/eng.traineddata.gz +0 -0
  174. package/lib/src/services/omni-vision/OmniVisionService.js +132 -95
  175. package/lib/src/services/omni-vision/VisionAssertionService.js +28 -25
  176. package/lib/src/services/omni-vision/ocrTextMatch.js +105 -0
  177. package/lib/src/services/omni-vision/screenScale.js +68 -0
  178. package/lib/src/services/passwordResetLink.js +50 -9
  179. package/lib/src/services/passwordResetMode.js +3 -1
  180. package/lib/src/services/publicUrl.js +45 -0
  181. package/lib/src/services/recording/concurrency-gate.js +12 -3
  182. package/lib/src/services/session/sessionIdentity.js +5 -3
  183. package/lib/src/services/session/shutdownReason.js +26 -0
  184. package/lib/src/services/settings/SelfHealingSwitch.js +70 -0
  185. package/lib/src/services/settings/aiEngineSettings.js +219 -0
  186. package/lib/src/services/settings/labSettings.js +149 -0
  187. package/lib/src/services/token/mintedLifetime.js +51 -0
  188. package/lib/src/services/webhookEvents.js +173 -0
  189. package/lib/src/sessions/SessionManager.js +8 -2
  190. package/lib/src/sessions/sessionMemory.js +25 -0
  191. package/lib/test/helpers/fake-adb.js +58 -0
  192. package/lib/test/helpers/fake-ai-provider.js +188 -0
  193. package/lib/test/helpers/fake-device-table.js +63 -24
  194. package/lib/test/helpers/loki-stores.js +33 -0
  195. package/lib/test/helpers/page-sources.js +164 -0
  196. package/lib/test/helpers/scratch-database.js +58 -13
  197. package/lib/test/integration/audit-ingest.spec.js +10 -10
  198. package/lib/test/integration/auth-flow.spec.js +9 -14
  199. package/lib/test/integration/auth-rate-limit.spec.js +6 -3
  200. package/lib/test/integration/bug-report-route.spec.js +22 -2
  201. package/lib/test/integration/forgot-password-rate-limit.spec.js +6 -3
  202. package/lib/test/integration/forgot-password.spec.js +17 -18
  203. package/lib/test/integration/legacy-deprecation-headers.spec.js +5 -2
  204. package/lib/test/integration/migration.spec.js +6 -1
  205. package/lib/test/integration/node-pair-auth-rest.spec.js +5 -9
  206. package/lib/test/integration/ports-router.spec.js +4 -4
  207. package/lib/test/integration/prisma-device-store-add-devices.spec.js +2 -3
  208. package/lib/test/integration/prisma-device-store-team-filter.spec.js +2 -5
  209. package/lib/test/integration/profile-tokens.spec.js +10 -16
  210. package/lib/test/integration/recordings-router.spec.js +16 -16
  211. package/lib/test/integration/reset-revokes-sessions.spec.js +4 -9
  212. package/lib/test/integration/role-matrix.spec.js +4 -9
  213. package/lib/test/integration/sdk-leases-router.spec.js +9 -9
  214. package/lib/test/integration/sdk-version.spec.js +2 -2
  215. package/lib/test/integration/selector-health-endpoint.spec.js +5 -5
  216. package/lib/test/integration/team-membership-migration.spec.js +73 -12
  217. package/lib/test/integration/team-visibility-apps.spec.js +15 -22
  218. package/lib/test/integration/team-visibility-control.spec.js +96 -69
  219. package/lib/test/integration/team-visibility-dashboard.spec.js +7 -23
  220. package/lib/test/integration/team-visibility-grid.spec.js +16 -15
  221. package/lib/test/integration/team-visibility-leases.spec.js +8 -11
  222. package/lib/test/integration/team-visibility-recordings.spec.js +6 -19
  223. package/lib/test/integration/team-visibility-sessions.spec.js +6 -19
  224. package/lib/test/integration/users-flow.spec.js +7 -12
  225. package/lib/test/integration/users-lockout.spec.js +5 -3
  226. package/lib/test/unit/EmailService.test.js +66 -0
  227. package/lib/test/unit/ResilioTreeHealingProvider.test.js +46 -19
  228. package/lib/test/unit/TeamService.test.js +3 -0
  229. package/lib/test/unit/XenonCapabilityManager.spec.js +4 -2
  230. package/lib/test/unit/ai-call-time-limit.spec.js +130 -0
  231. package/lib/test/unit/ai-engine-settings.spec.js +338 -0
  232. package/lib/test/unit/ai-provider-switch.spec.js +130 -0
  233. package/lib/test/unit/ai-rate-limit.spec.js +126 -0
  234. package/lib/test/unit/android-h264-viewers.spec.js +272 -0
  235. package/lib/test/unit/android-preview-hold.spec.js +3 -2
  236. package/lib/test/unit/apikeys-router.spec.js +84 -0
  237. package/lib/test/unit/apple-family.spec.js +36 -0
  238. package/lib/test/unit/authMiddleware.bearer.spec.js +9 -0
  239. package/lib/test/unit/authMiddleware.credentialLifetime.spec.js +149 -0
  240. package/lib/test/unit/bug-report/video-slice.spec.js +19 -0
  241. package/lib/test/unit/cleanup-settings-db.spec.js +103 -0
  242. package/lib/test/unit/cleanup-settings.spec.js +244 -0
  243. package/lib/test/unit/dashboard-api-paths.spec.js +101 -0
  244. package/lib/test/unit/dashboard-commands.spec.js +249 -0
  245. package/lib/test/unit/dashboard-events-device-logs.spec.js +207 -0
  246. package/lib/test/unit/dashboard-events-team-scope.spec.js +9 -6
  247. package/lib/test/unit/dashboard-plugin-link.spec.js +213 -0
  248. package/lib/test/unit/database-check.spec.js +108 -0
  249. package/lib/test/unit/default-plugin-args.spec.js +33 -0
  250. package/lib/test/unit/device-log-book.spec.js +180 -0
  251. package/lib/test/unit/device-offline-webhook.spec.js +81 -0
  252. package/lib/test/unit/device-setting-columns.spec.js +66 -0
  253. package/lib/test/unit/device-settings-loki.spec.js +87 -0
  254. package/lib/test/unit/device-settings-survive.spec.js +555 -0
  255. package/lib/test/unit/device-sync-lost-update.spec.js +5 -0
  256. package/lib/test/unit/emulator-boot.spec.js +91 -0
  257. package/lib/test/unit/enable-dashboard-option.spec.js +57 -0
  258. package/lib/test/unit/event-log-session-data.spec.js +244 -0
  259. package/lib/test/unit/failure-analysis.spec.js +182 -0
  260. package/lib/test/unit/failure-categories.spec.js +128 -0
  261. package/lib/test/unit/fingerprint-identity.spec.js +433 -0
  262. package/lib/test/unit/h264-multiplexer.spec.js +46 -0
  263. package/lib/test/unit/heal-report.spec.js +71 -0
  264. package/lib/test/unit/healing-instrumentation.spec.js +6 -1
  265. package/lib/test/unit/healing-tier-gate.spec.js +73 -29
  266. package/lib/test/unit/healing-tiers-every-session.spec.js +219 -0
  267. package/lib/test/unit/hub-node-gateway-appium-server.spec.js +219 -0
  268. package/lib/test/unit/hub-node-same-host.spec.js +5 -0
  269. package/lib/test/unit/interceptor-visual-elements.spec.js +192 -0
  270. package/lib/test/unit/iphone-ipad-only.spec.js +132 -0
  271. package/lib/test/unit/json-logging-option.spec.js +74 -0
  272. package/lib/test/unit/lab-settings.spec.js +218 -0
  273. package/lib/test/unit/lease/lease-override.spec.js +23 -6
  274. package/lib/test/unit/lease/session-lease-proof.spec.js +16 -4
  275. package/lib/test/unit/max-sessions.spec.js +240 -0
  276. package/lib/test/unit/ocr-healing-tier-real-ocr.spec.js +113 -0
  277. package/lib/test/unit/ocr-healing-tier.spec.js +122 -0
  278. package/lib/test/unit/ocr-offline.spec.js +213 -0
  279. package/lib/test/unit/ocr-text-match.spec.js +44 -0
  280. package/lib/test/unit/omni-virtual-element-store.spec.js +301 -0
  281. package/lib/test/unit/omni-vision-analyze-screen.spec.js +72 -0
  282. package/lib/test/unit/omni-vision-ios-points.spec.js +159 -0
  283. package/lib/test/unit/omni-vision-tap-text.spec.js +129 -0
  284. package/lib/test/unit/omni-vision.spec.js +6 -8
  285. package/lib/test/unit/package-engines.spec.js +27 -0
  286. package/lib/test/unit/partial-config-file.spec.js +14 -0
  287. package/lib/test/unit/passwordResetMode.test.js +10 -2
  288. package/lib/test/unit/phone-network/boot-clean-up.spec.js +64 -0
  289. package/lib/test/unit/phone-network/interceptor-options.spec.js +77 -0
  290. package/lib/test/unit/phone-network/interceptor-proxy-restore.spec.js +145 -0
  291. package/lib/test/unit/phone-network/network-conditioning.spec.js +145 -0
  292. package/lib/test/unit/phone-network/phone-network-restore.spec.js +246 -0
  293. package/lib/test/unit/phone-network/session-end-paths.spec.js +182 -0
  294. package/lib/test/unit/phone-network/session-start.spec.js +110 -0
  295. package/lib/test/unit/phone-network/xenon-proxy.spec.js +69 -0
  296. package/lib/test/unit/plugin.spec.js +3 -3
  297. package/lib/test/unit/profile-router.test.js +122 -1
  298. package/lib/test/unit/queue-requester.spec.js +1 -1
  299. package/lib/test/unit/recording-options.spec.js +181 -0
  300. package/lib/test/unit/reset-link-address.spec.js +266 -0
  301. package/lib/test/unit/reset-link.test.js +10 -4
  302. package/lib/test/unit/resilio-path-healing.spec.js +270 -0
  303. package/lib/test/unit/selector-learning-every-session.spec.js +179 -0
  304. package/lib/test/unit/selector-verification-clean-build.spec.js +130 -0
  305. package/lib/test/unit/selector-verification-job.spec.js +13 -13
  306. package/lib/test/unit/self-healing-boot.spec.js +80 -0
  307. package/lib/test/unit/self-healing-setting.spec.js +273 -0
  308. package/lib/test/unit/session-app-resolution.spec.js +5 -1
  309. package/lib/test/unit/session-attribution-wiring.spec.js +41 -13
  310. package/lib/test/unit/session-create-credential-owner.spec.js +242 -0
  311. package/lib/test/unit/session-credential-strip.spec.js +1 -1
  312. package/lib/test/unit/session-device-logs.spec.js +347 -0
  313. package/lib/test/unit/session-end-failure-analysis.spec.js +133 -0
  314. package/lib/test/unit/session-failed-webhook.spec.js +170 -0
  315. package/lib/test/unit/session-log-indexes.spec.js +64 -3
  316. package/lib/test/unit/shell-commands.spec.js +133 -0
  317. package/lib/test/unit/socketServer.scopedEmit.spec.js +2 -0
  318. package/lib/test/unit/socketServer.sessionCookie.spec.js +2 -1
  319. package/lib/test/unit/stream-start-pipelines.spec.js +264 -0
  320. package/lib/test/unit/stream-status-type.spec.js +4 -0
  321. package/lib/test/unit/token-mint-limits.spec.js +165 -0
  322. package/lib/test/unit/unexpected-shutdown-reason.spec.js +163 -0
  323. package/lib/test/unit/verify-credential.spec.js +33 -0
  324. package/lib/test/unit/vision-assertion.spec.js +119 -0
  325. package/lib/test/unit/wda-client-shell.spec.js +133 -0
  326. package/lib/test/unit/web-config-service.spec.js +98 -0
  327. package/lib/test/unit/webhook-delivery.spec.js +24 -0
  328. package/lib/test/unit/webhook-payloads.spec.js +273 -0
  329. package/lib/test/unit/xenon-execute-scripts.spec.js +138 -0
  330. package/lib/tsconfig.tsbuildinfo +1 -1
  331. package/lib/web/src/components/webhook-settings/webhookEventVariables.json +30 -0
  332. package/package.json +5 -5
  333. package/prisma/migrations/20261004140000_keep_device_settings/migration.sql +19 -0
  334. package/prisma/migrations/20261004174413_locator_etalon_path/migration.sql +2 -0
  335. package/prisma/schema.prisma +38 -7
  336. package/schema.json +23 -25
  337. package/scripts/generate-types-from-schema.js +1 -2
  338. package/scripts/lib/always-present-plugin-args.js +4 -1
  339. package/lib/public/assets/BugReportButton-BoGhlI7F.js +0 -15
  340. package/lib/public/assets/DeviceMosaicView-8J0oYqMM.js +0 -2
  341. package/lib/public/assets/ai-settings-Bce5SJob.js +0 -21
  342. package/lib/public/assets/device-explorer-C-KnhMHI.js +0 -279
  343. package/lib/public/assets/forgot-password-DnudqFFo.js +0 -1
  344. package/lib/public/assets/maintenance-settings-zDVlOMVK.js +0 -6
  345. package/lib/public/assets/recording-group-store-WVM3hoXn.js +0 -26
  346. package/lib/public/assets/runbook-page-DJTVCJX7.js +0 -124
  347. package/lib/public/assets/session-detail-page-CGn55Pvl.js +0 -4
  348. package/lib/public/assets/settings-MsVU2ukE.js +0 -6
  349. package/lib/public/assets/webhook-settings-DiS8geIm.js +0 -1
  350. /package/lib/public/assets/{BugReportButton-CPBjfiv4.css → index-CPBjfiv4.css} +0 -0
@@ -0,0 +1,130 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ require("reflect-metadata");
16
+ const chai_1 = require("chai");
17
+ const sinon_1 = __importDefault(require("sinon"));
18
+ const AIService_1 = require("../../src/services/AIService");
19
+ const CircuitBreaker_1 = require("../../src/services/CircuitBreaker");
20
+ const HealingOrchestrator_1 = require("../../src/services/healing/HealingOrchestrator");
21
+ const fake_ai_provider_1 = require("../helpers/fake-ai-provider");
22
+ /**
23
+ * An AI call made while a test command runs has a time limit.
24
+ *
25
+ * The LLM and visual healing tiers run inside a failing findElement, the
26
+ * visual assertion and the screen description inside an execute script, and
27
+ * an ai-icon find inside findElement or device control's Test locator. Only
28
+ * Ollama had a time limit (30 s). The OpenAI and Anthropic SDKs wait up to 10
29
+ * minutes a try, with two retries, and Gemini has none, so one provider that
30
+ * didn't answer held the command, and the test, for as long, past the
31
+ * client's own timeout.
32
+ */
33
+ describe('AI calls made during a test command', () => {
34
+ const ai = (0, fake_ai_provider_1.useFakeAiProviders)();
35
+ const SCREEN = 'c2NyZWVuc2hvdA==';
36
+ afterEach(() => sinon_1.default.restore());
37
+ /** What a call came to: its answer, or its error. */
38
+ const outcomeOf = (p) => p.then((value) => ({ value, error: undefined }), (error) => ({ value: undefined, error }));
39
+ const calls = [
40
+ {
41
+ name: 'an LLM heal finds nothing',
42
+ call: (s) => s.healLocator({ selector: '//x', strategy: 'xpath', xml: '<hierarchy/>' }),
43
+ expectOutcome: ({ value, error }) => {
44
+ (0, chai_1.expect)(error).to.equal(undefined);
45
+ (0, chai_1.expect)(value).to.equal(null);
46
+ },
47
+ },
48
+ {
49
+ name: 'a visual heal or ai-icon find finds nothing',
50
+ call: (s) => s.visualFind(SCREEN, 'the cart icon'),
51
+ expectOutcome: ({ value, error }) => {
52
+ (0, chai_1.expect)(error).to.equal(undefined);
53
+ (0, chai_1.expect)(value).to.equal(null);
54
+ },
55
+ },
56
+ {
57
+ name: "Test locator fails, saying the provider didn't answer",
58
+ call: (s) => s.visualFind(SCREEN, 'the cart icon', { throwOnError: true }),
59
+ expectOutcome: ({ error }) => {
60
+ (0, chai_1.expect)(error === null || error === void 0 ? void 0 : error.message).to.equal("The AI provider didn't answer within 30 s");
61
+ },
62
+ },
63
+ {
64
+ name: "a visual assertion fails, saying the provider didn't answer",
65
+ call: (s) => s.assertVisual(SCREEN, 'The cart is empty'),
66
+ expectOutcome: ({ error }) => {
67
+ (0, chai_1.expect)(error === null || error === void 0 ? void 0 : error.message).to.equal("The AI provider didn't answer within 30 s, so the condition was not checked. Try again later.");
68
+ },
69
+ },
70
+ {
71
+ name: "a screen description fails, saying the provider didn't answer",
72
+ call: (s) => s.describeScreen(SCREEN),
73
+ expectOutcome: ({ error }) => {
74
+ (0, chai_1.expect)(error === null || error === void 0 ? void 0 : error.message).to.equal("The AI provider didn't answer within 30 s");
75
+ },
76
+ },
77
+ ];
78
+ for (const { name, call, expectOutcome } of calls) {
79
+ it(`gives up at the limit, cancelling the request: ${name}`, () => __awaiter(void 0, void 0, void 0, function* () {
80
+ var _a, _b;
81
+ const { model } = ai.use('anthropic');
82
+ ai.answer('hang');
83
+ const clock = sinon_1.default.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'] });
84
+ const service = new AIService_1.AIService();
85
+ const outcome = outcomeOf(call(service));
86
+ yield clock.tickAsync(AIService_1.AI_CALL_TIMEOUT_MS - 1);
87
+ (0, chai_1.expect)(ai.calls).to.have.length(1);
88
+ (0, chai_1.expect)((_a = ai.calls[0].signal) === null || _a === void 0 ? void 0 : _a.aborted).to.equal(false);
89
+ yield clock.tickAsync(1);
90
+ (0, chai_1.expect)((_b = ai.calls[0].signal) === null || _b === void 0 ? void 0 : _b.aborted).to.equal(true);
91
+ expectOutcome(yield outcome);
92
+ const breaker = CircuitBreaker_1.CIRCUIT_BREAKERS.snapshot().find((b) => b.key === `ai:anthropic:${model}`);
93
+ (0, chai_1.expect)(breaker === null || breaker === void 0 ? void 0 : breaker.consecutiveFailures).to.equal(1);
94
+ }));
95
+ }
96
+ it("Test connection fails at the limit, saying the provider didn't answer", () => __awaiter(void 0, void 0, void 0, function* () {
97
+ var _a;
98
+ ai.use('openai');
99
+ ai.answer('hang');
100
+ const clock = sinon_1.default.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'] });
101
+ const result = new AIService_1.AIService().testConnection({});
102
+ yield clock.tickAsync(AIService_1.AI_CALL_TIMEOUT_MS);
103
+ (0, chai_1.expect)(yield result).to.deep.equal({
104
+ success: false,
105
+ message: "Connection failed: The AI provider didn't answer within 30 s",
106
+ });
107
+ (0, chai_1.expect)((_a = ai.calls[0].signal) === null || _a === void 0 ? void 0 : _a.aborted).to.equal(true);
108
+ }));
109
+ it('a failing findElement is answered within two time limits, after both AI tiers', () => __awaiter(void 0, void 0, void 0, function* () {
110
+ ai.use('gemini');
111
+ ai.answer('hang');
112
+ const clock = sinon_1.default.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'] });
113
+ const driver = {
114
+ getPageSource: () => __awaiter(void 0, void 0, void 0, function* () { return '<hierarchy/>'; }),
115
+ getScreenshot: () => __awaiter(void 0, void 0, void 0, function* () { return SCREEN; }),
116
+ findElement: sinon_1.default.stub().rejects(new Error('no such element')),
117
+ };
118
+ let healed = 'not answered';
119
+ const healing = new HealingOrchestrator_1.HealingOrchestrator({})
120
+ // The visual tier (4), then the LLM tier (5).
121
+ .attemptHealing('sess-time-limit', driver, 'id', 'login', [4, 5])
122
+ .then((result) => (healed = result));
123
+ yield clock.tickAsync(AIService_1.AI_CALL_TIMEOUT_MS);
124
+ (0, chai_1.expect)(ai.calls).to.have.length(2);
125
+ yield clock.tickAsync(AIService_1.AI_CALL_TIMEOUT_MS);
126
+ yield healing;
127
+ (0, chai_1.expect)(healed).to.equal(null);
128
+ (0, chai_1.expect)(ai.calls.map((c) => { var _a; return (_a = c.signal) === null || _a === void 0 ? void 0 : _a.aborted; })).to.deep.equal([true, true]);
129
+ }));
130
+ });
@@ -0,0 +1,338 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ require("reflect-metadata");
16
+ const chai_1 = require("chai");
17
+ const express_1 = __importDefault(require("express"));
18
+ const sinon_1 = __importDefault(require("sinon"));
19
+ const typedi_1 = require("typedi");
20
+ const loopbackRequest_1 = __importDefault(require("../helpers/loopbackRequest"));
21
+ const dashboard_1 = __importDefault(require("../../src/app/routers/dashboard"));
22
+ const config_1 = require("../../src/config");
23
+ const web_config_service_1 = require("../../src/data-service/web-config-service");
24
+ const aiEngineSettings_1 = require("../../src/services/settings/aiEngineSettings");
25
+ const logger_1 = __importDefault(require("../../src/logger"));
26
+ const container_registration_1 = require("../helpers/container-registration");
27
+ const scratch_database_1 = require("../helpers/scratch-database");
28
+ /**
29
+ * The AI engine page's choice of provider was kept in memory only: `POST
30
+ * /config` changed the running server's settings and saved nothing, so a
31
+ * restart went back to the provider the server was started with. The model and
32
+ * base URL the same route accepts went the same way.
33
+ *
34
+ * They follow the other lab settings now: a value saved in the dashboard wins
35
+ * over the plugin option or environment variable the server started with, which
36
+ * wins over the default, and it applies to the next AI call without a restart.
37
+ * API keys are never saved or sent: they stay in the server's environment.
38
+ */
39
+ describe('the AI engine settings', () => {
40
+ const scratch = (0, scratch_database_1.useScratchDatabase)();
41
+ const AI_FIELDS = [
42
+ 'aiProvider',
43
+ 'aiModel',
44
+ 'aiBaseUrl',
45
+ 'geminiModel',
46
+ 'openaiModel',
47
+ 'anthropicModel',
48
+ 'ollamaModel',
49
+ 'geminiApiKey',
50
+ 'openaiApiKey',
51
+ 'anthropicApiKey',
52
+ ];
53
+ let startedWith;
54
+ let restoreContainer;
55
+ /** The AI settings the server was started with (its option, else its environment). */
56
+ const startup = (over = {}) => {
57
+ const blank = {};
58
+ for (const field of AI_FIELDS)
59
+ blank[field] = undefined;
60
+ Object.assign(config_1.config, blank, { aiProvider: 'gemini' }, over);
61
+ };
62
+ /** A server process starting: nothing in memory, the settings read from the database. */
63
+ const restart = () => __awaiter(void 0, void 0, void 0, function* () {
64
+ typedi_1.Container.set(aiEngineSettings_1.AiEngineSettings, new aiEngineSettings_1.AiEngineSettings());
65
+ yield typedi_1.Container.get(aiEngineSettings_1.AiEngineSettings).load();
66
+ });
67
+ beforeEach(() => __awaiter(void 0, void 0, void 0, function* () {
68
+ startedWith = {};
69
+ for (const field of AI_FIELDS)
70
+ startedWith[field] = config_1.config[field];
71
+ restoreContainer = (0, container_registration_1.saveRegistrations)(aiEngineSettings_1.AiEngineSettings);
72
+ // A service of its own, as at boot, so nothing saved leaks in from another spec.
73
+ typedi_1.Container.set(aiEngineSettings_1.AiEngineSettings, new aiEngineSettings_1.AiEngineSettings());
74
+ yield scratch.db.webConfig.deleteMany({});
75
+ }));
76
+ afterEach(() => {
77
+ Object.assign(config_1.config, startedWith);
78
+ restoreContainer();
79
+ sinon_1.default.restore();
80
+ });
81
+ function app() {
82
+ const a = (0, express_1.default)();
83
+ a.use(express_1.default.json());
84
+ a.use((req, _res, next) => {
85
+ req.auth = {
86
+ kind: 'user-session',
87
+ userId: 'usr_x',
88
+ role: 'SUPER_ADMIN',
89
+ scopes: 'admin,devices,sessions,read',
90
+ rateLimit: 1000,
91
+ };
92
+ next();
93
+ });
94
+ dashboard_1.default.register(a);
95
+ return a;
96
+ }
97
+ const save = (body) => (0, loopbackRequest_1.default)(app()).post('/config').send(body).timeout(5000);
98
+ const read = () => (0, loopbackRequest_1.default)(app()).get('/config').timeout(5000);
99
+ describe('the provider chosen on the page', () => {
100
+ it('is saved, and shown by GET /config', () => __awaiter(void 0, void 0, void 0, function* () {
101
+ startup({ anthropicApiKey: 'sk-ant-test' });
102
+ const saved = yield save({ aiProvider: 'anthropic' });
103
+ (0, chai_1.expect)(saved.status).to.equal(200);
104
+ (0, chai_1.expect)(yield typedi_1.Container.get(web_config_service_1.WebConfigService).getConfig()).to.deep.equal({
105
+ aiProvider: 'anthropic',
106
+ });
107
+ (0, chai_1.expect)((yield read()).body.aiProvider).to.equal('anthropic');
108
+ }));
109
+ it('applies to the running server at once, with no restart', () => __awaiter(void 0, void 0, void 0, function* () {
110
+ startup();
111
+ yield save({ aiProvider: 'openai' });
112
+ (0, chai_1.expect)(config_1.config.aiProvider).to.equal('openai');
113
+ }));
114
+ it('is still the provider after a restart', () => __awaiter(void 0, void 0, void 0, function* () {
115
+ startup();
116
+ yield save({ aiProvider: 'ollama' });
117
+ startup(); // the process starts again with its option or environment: gemini
118
+ yield restart();
119
+ (0, chai_1.expect)(config_1.config.aiProvider).to.equal('ollama');
120
+ (0, chai_1.expect)((yield read()).body.aiProvider).to.equal('ollama');
121
+ }));
122
+ it('wins over the provider the server was started with', () => __awaiter(void 0, void 0, void 0, function* () {
123
+ startup({ aiProvider: 'openai' });
124
+ yield save({ aiProvider: 'anthropic' });
125
+ startup({ aiProvider: 'openai' });
126
+ yield restart();
127
+ (0, chai_1.expect)(config_1.config.aiProvider).to.equal('anthropic');
128
+ }));
129
+ it('leaves the started-with provider in charge when nothing was saved', () => __awaiter(void 0, void 0, void 0, function* () {
130
+ startup({ aiProvider: 'anthropic' });
131
+ yield restart();
132
+ (0, chai_1.expect)(config_1.config.aiProvider).to.equal('anthropic');
133
+ (0, chai_1.expect)((yield read()).body.aiProvider).to.equal('anthropic');
134
+ }));
135
+ it('goes back to the started-with provider when the saved one is cleared', () => __awaiter(void 0, void 0, void 0, function* () {
136
+ startup({ aiProvider: 'openai' });
137
+ yield save({ aiProvider: 'anthropic' });
138
+ yield save({ aiProvider: '' });
139
+ (0, chai_1.expect)(config_1.config.aiProvider).to.equal('openai');
140
+ startup({ aiProvider: 'openai' });
141
+ yield restart();
142
+ (0, chai_1.expect)(config_1.config.aiProvider).to.equal('openai');
143
+ }));
144
+ it('is saved without touching the other settings', () => __awaiter(void 0, void 0, void 0, function* () {
145
+ startup();
146
+ yield save({ buildCleanupDays: 7 });
147
+ yield save({ aiProvider: 'openai' });
148
+ (0, chai_1.expect)(yield typedi_1.Container.get(web_config_service_1.WebConfigService).getConfig()).to.deep.equal({
149
+ buildCleanupDays: 7,
150
+ aiProvider: 'openai',
151
+ });
152
+ }));
153
+ });
154
+ describe('the models and base URL', () => {
155
+ it('are saved, applied at once, and kept across a restart', () => __awaiter(void 0, void 0, void 0, function* () {
156
+ startup();
157
+ const saved = yield save({
158
+ ollamaModel: 'llava',
159
+ aiBaseUrl: 'http://gpu-box.lab:11434',
160
+ geminiModel: 'gemini-2.5-pro',
161
+ openaiModel: 'gpt-4.1',
162
+ anthropicModel: 'claude-opus-4-1',
163
+ aiModel: 'llava:13b',
164
+ });
165
+ (0, chai_1.expect)(saved.status).to.equal(200);
166
+ (0, chai_1.expect)(config_1.config.ollamaModel).to.equal('llava');
167
+ (0, chai_1.expect)(config_1.config.aiBaseUrl).to.equal('http://gpu-box.lab:11434');
168
+ startup();
169
+ yield restart();
170
+ (0, chai_1.expect)(config_1.config).to.include({
171
+ ollamaModel: 'llava',
172
+ aiBaseUrl: 'http://gpu-box.lab:11434',
173
+ geminiModel: 'gemini-2.5-pro',
174
+ openaiModel: 'gpt-4.1',
175
+ anthropicModel: 'claude-opus-4-1',
176
+ aiModel: 'llava:13b',
177
+ });
178
+ const shown = (yield read()).body;
179
+ (0, chai_1.expect)(shown).to.include({ ollamaModel: 'llava', aiBaseUrl: 'http://gpu-box.lab:11434' });
180
+ }));
181
+ it('win over the values the server was started with', () => __awaiter(void 0, void 0, void 0, function* () {
182
+ startup({ ollamaModel: 'llama3', aiBaseUrl: 'http://localhost:11434' });
183
+ yield save({ ollamaModel: 'llava', aiBaseUrl: 'http://gpu-box.lab:11434' });
184
+ startup({ ollamaModel: 'llama3', aiBaseUrl: 'http://localhost:11434' });
185
+ yield restart();
186
+ (0, chai_1.expect)(config_1.config.ollamaModel).to.equal('llava');
187
+ (0, chai_1.expect)(config_1.config.aiBaseUrl).to.equal('http://gpu-box.lab:11434');
188
+ }));
189
+ it('go back to the started-with values when cleared with an empty value', () => __awaiter(void 0, void 0, void 0, function* () {
190
+ startup({ ollamaModel: 'llama3', aiBaseUrl: 'http://localhost:11434' });
191
+ yield save({ ollamaModel: 'llava', aiBaseUrl: 'http://gpu-box.lab:11434' });
192
+ yield save({ ollamaModel: '', aiBaseUrl: '' });
193
+ (0, chai_1.expect)(config_1.config.ollamaModel).to.equal('llama3');
194
+ (0, chai_1.expect)(config_1.config.aiBaseUrl).to.equal('http://localhost:11434');
195
+ }));
196
+ it('go back to no value at all when cleared and the server was started with none', () => __awaiter(void 0, void 0, void 0, function* () {
197
+ startup();
198
+ yield save({ geminiModel: 'gemini-2.5-pro' });
199
+ yield save({ geminiModel: '' });
200
+ (0, chai_1.expect)(config_1.config.geminiModel).to.equal(undefined);
201
+ (0, chai_1.expect)((yield read()).body.geminiModel).to.equal(undefined);
202
+ }));
203
+ it('take surrounding spaces off a model name', () => __awaiter(void 0, void 0, void 0, function* () {
204
+ startup();
205
+ yield save({ openaiModel: ' gpt-4.1 ' });
206
+ (0, chai_1.expect)(config_1.config.openaiModel).to.equal('gpt-4.1');
207
+ }));
208
+ });
209
+ describe('what is refused, with a 400 naming the setting and nothing saved', () => {
210
+ var _a;
211
+ const refused = [
212
+ ['aiProvider', 'gpt'],
213
+ ['aiProvider', 'Gemini'],
214
+ ['aiProvider', 1],
215
+ ['aiProvider', null],
216
+ ['ollamaModel', 7],
217
+ ['ollamaModel', 'llama 3'],
218
+ ['geminiModel', 'a'.repeat(201)],
219
+ ['aiModel', 'gpt-4o\nx'],
220
+ ['aiBaseUrl', 'localhost:11434'],
221
+ ['aiBaseUrl', 'ftp://models.lab/'],
222
+ ['aiBaseUrl', 'not a url'],
223
+ // A key in the address would be a key saved in the database.
224
+ ['aiBaseUrl', 'https://user:secret@llm.lab/v1'],
225
+ ['aiBaseUrl', 'https://llm.lab/v1?key=secret'],
226
+ ];
227
+ for (const [field, value] of refused) {
228
+ it(`${field}: ${(_a = JSON.stringify(value)) === null || _a === void 0 ? void 0 : _a.slice(0, 40)}`, () => __awaiter(void 0, void 0, void 0, function* () {
229
+ startup();
230
+ const answer = yield save({ [field]: value, buildCleanupDays: 9 });
231
+ (0, chai_1.expect)(answer.status).to.equal(400);
232
+ (0, chai_1.expect)(answer.body).to.include({ error: 'invalid_setting', field });
233
+ (0, chai_1.expect)(answer.body.message).to.be.a('string').and.not.equal('');
234
+ (0, chai_1.expect)(yield typedi_1.Container.get(web_config_service_1.WebConfigService).getConfig()).to.deep.equal({});
235
+ (0, chai_1.expect)(config_1.config.aiProvider).to.equal('gemini');
236
+ }));
237
+ }
238
+ });
239
+ describe('API keys', () => {
240
+ it('are never saved, and never change the running server', () => __awaiter(void 0, void 0, void 0, function* () {
241
+ startup({ geminiApiKey: 'server-key' });
242
+ const answer = yield save({
243
+ aiProvider: 'gemini',
244
+ geminiApiKey: 'page-key',
245
+ openaiApiKey: 'page-key',
246
+ anthropicApiKey: 'page-key',
247
+ });
248
+ (0, chai_1.expect)(answer.status).to.equal(200);
249
+ (0, chai_1.expect)(config_1.config.geminiApiKey).to.equal('server-key');
250
+ (0, chai_1.expect)(config_1.config.openaiApiKey).to.equal(undefined);
251
+ const rows = yield scratch.db.webConfig.findMany();
252
+ (0, chai_1.expect)(rows.map((row) => row.name)).to.deep.equal(['aiProvider']);
253
+ (0, chai_1.expect)(JSON.stringify(rows)).not.to.include('page-key');
254
+ }));
255
+ it('are never sent from the started-with base URL either', () => __awaiter(void 0, void 0, void 0, function* () {
256
+ // A key in the address: POST /config refuses one, but the environment
257
+ // can still hold it. GET /config is open to every admin.
258
+ startup({
259
+ aiProvider: 'openai',
260
+ aiBaseUrl: 'https://lab:pw-secret@llm.lab/v1?key=key-secret&region=eu',
261
+ });
262
+ const shown = (yield read()).body.aiBaseUrl;
263
+ (0, chai_1.expect)(shown).not.to.include('pw-secret');
264
+ (0, chai_1.expect)(shown).not.to.include('key-secret');
265
+ (0, chai_1.expect)(shown).to.include('llm.lab/v1');
266
+ // The server still uses the address as it was given.
267
+ (0, chai_1.expect)(config_1.config.aiBaseUrl).to.equal('https://lab:pw-secret@llm.lab/v1?key=key-secret&region=eu');
268
+ }));
269
+ it('are never sent, only whether one is set', () => __awaiter(void 0, void 0, void 0, function* () {
270
+ startup({ geminiApiKey: 'server-key', aiProvider: 'gemini' });
271
+ const shown = (yield read()).body;
272
+ (0, chai_1.expect)(JSON.stringify(shown)).not.to.include('server-key');
273
+ (0, chai_1.expect)(shown).to.include({ geminiSet: true, openaiSet: false, anthropicSet: false });
274
+ }));
275
+ });
276
+ describe('the model parameters the page used to show', () => {
277
+ it('are not sent: temperature, max tokens and top P were never used by any AI call', () => __awaiter(void 0, void 0, void 0, function* () {
278
+ startup();
279
+ yield save({ aiTemperature: 0.2, aiMaxTokens: 1024, aiTopP: 0.5 });
280
+ const shown = (yield read()).body;
281
+ (0, chai_1.expect)(shown).not.to.have.any.keys('aiTemperature', 'aiMaxTokens', 'aiTopP');
282
+ (0, chai_1.expect)(yield typedi_1.Container.get(web_config_service_1.WebConfigService).getConfig()).to.deep.equal({});
283
+ }));
284
+ });
285
+ describe('at boot', () => {
286
+ it('a database that cannot be read leaves the started-with settings in charge, said once', () => __awaiter(void 0, void 0, void 0, function* () {
287
+ startup({ aiProvider: 'anthropic' });
288
+ sinon_1.default.stub(web_config_service_1.WebConfigService.prototype, 'getConfig').rejects(new Error('disk I/O error'));
289
+ const warn = sinon_1.default.stub(logger_1.default, 'warn');
290
+ yield restart();
291
+ (0, chai_1.expect)(config_1.config.aiProvider).to.equal('anthropic');
292
+ (0, chai_1.expect)(warn.callCount).to.equal(1);
293
+ (0, chai_1.expect)(String(warn.firstCall.args[0])).to.include('disk I/O error');
294
+ }));
295
+ it('a saved value that could not work is skipped, not applied', () => __awaiter(void 0, void 0, void 0, function* () {
296
+ startup({ aiProvider: 'openai' });
297
+ // Written by hand: POST /config refuses it.
298
+ yield scratch.db.webConfig.create({
299
+ data: { id: 'aiProvider', name: 'aiProvider', value: 'gpt' },
300
+ });
301
+ yield restart();
302
+ (0, chai_1.expect)(config_1.config.aiProvider).to.equal('openai');
303
+ }));
304
+ });
305
+ describe('effectiveAiEngine', () => {
306
+ it('takes a saved value over the startup one, field by field', () => {
307
+ (0, chai_1.expect)((0, aiEngineSettings_1.effectiveAiEngine)({ aiProvider: 'gemini', ollamaModel: 'llama3', aiBaseUrl: 'http://a:1' }, { aiProvider: 'ollama', aiBaseUrl: 'http://b:2' })).to.deep.equal({
308
+ aiProvider: 'ollama',
309
+ aiModel: undefined,
310
+ aiBaseUrl: 'http://b:2',
311
+ geminiModel: undefined,
312
+ openaiModel: undefined,
313
+ anthropicModel: undefined,
314
+ ollamaModel: 'llama3',
315
+ });
316
+ });
317
+ it('falls back to gemini when neither names a provider', () => {
318
+ (0, chai_1.expect)((0, aiEngineSettings_1.effectiveAiEngine)({}, {}).aiProvider).to.equal('gemini');
319
+ (0, chai_1.expect)((0, aiEngineSettings_1.effectiveAiEngine)({}, { aiProvider: '' }).aiProvider).to.equal('gemini');
320
+ });
321
+ it("keeps a started-with provider it doesn't know, rather than pick another for it", () => {
322
+ // AIService then runs with no provider, and says so; a fallback to gemini
323
+ // would send screenshots somewhere the server's owner didn't choose.
324
+ (0, chai_1.expect)((0, aiEngineSettings_1.effectiveAiEngine)({ aiProvider: 'claude' }, {}).aiProvider).to.equal('claude');
325
+ });
326
+ it('names every setting it decides, and no key', () => {
327
+ (0, chai_1.expect)([...aiEngineSettings_1.AI_ENGINE_SETTINGS].sort()).to.deep.equal([
328
+ 'aiBaseUrl',
329
+ 'aiModel',
330
+ 'aiProvider',
331
+ 'anthropicModel',
332
+ 'geminiModel',
333
+ 'ollamaModel',
334
+ 'openaiModel',
335
+ ].sort());
336
+ });
337
+ });
338
+ });
@@ -0,0 +1,130 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ require("reflect-metadata");
16
+ const chai_1 = require("chai");
17
+ const sinon_1 = __importDefault(require("sinon"));
18
+ const config_1 = require("../../src/config");
19
+ const AIService_1 = require("../../src/services/AIService");
20
+ const LlmHealingProvider_1 = require("../../src/services/healing/LlmHealingProvider");
21
+ const VisualAiHealingProvider_1 = require("../../src/services/healing/VisualAiHealingProvider");
22
+ const fake_ai_provider_1 = require("../helpers/fake-ai-provider");
23
+ /**
24
+ * The AI engine page changes the provider while the server runs (`POST
25
+ * /config` writes `config.aiProvider` and the models). The AI service set its
26
+ * provider up when it was created, and again only inside its own calls, while
27
+ * failure analysis and the LLM and visual healing tiers first asked
28
+ * `isEnabled()`, which read what was set up last. A server started with a
29
+ * provider that had no key never turned those on when a configured one was
30
+ * chosen; one started with a configured provider kept using it after a
31
+ * provider with no key was chosen.
32
+ */
33
+ describe('AI provider chosen while the server runs', () => {
34
+ const ai = (0, fake_ai_provider_1.useFakeAiProviders)();
35
+ const SCREEN = 'c2NyZWVuc2hvdA==';
36
+ const FAILURE = {
37
+ sessionId: 'sess-switch',
38
+ failureReason: 'An element could not be located on the page',
39
+ commandLogs: [],
40
+ deviceLogs: [],
41
+ };
42
+ afterEach(() => sinon_1.default.restore());
43
+ it('turns on when a provider with a key is chosen after one without', () => {
44
+ ai.use('gemini', { key: false });
45
+ const service = new AIService_1.AIService();
46
+ (0, chai_1.expect)(service.isEnabled()).to.equal(false);
47
+ ai.use('anthropic');
48
+ (0, chai_1.expect)(service.isEnabled()).to.equal(true);
49
+ });
50
+ it('turns off when a provider without a key is chosen, and stops asking the old one', () => __awaiter(void 0, void 0, void 0, function* () {
51
+ ai.use('openai');
52
+ ai.answer({ text: 'Root Cause: from OpenAI' });
53
+ const service = new AIService_1.AIService();
54
+ (0, chai_1.expect)(service.isEnabled()).to.equal(true);
55
+ ai.use('anthropic', { key: false });
56
+ (0, chai_1.expect)(service.isEnabled()).to.equal(false);
57
+ (0, chai_1.expect)(yield service.analyzeFailure(FAILURE)).to.equal(null);
58
+ (0, chai_1.expect)(ai.calls).to.have.length(0);
59
+ }));
60
+ it('asks the model chosen last', () => __awaiter(void 0, void 0, void 0, function* () {
61
+ ai.use('openai');
62
+ ai.answer({ text: 'Root Cause: x' });
63
+ const service = new AIService_1.AIService();
64
+ yield service.analyzeFailure(FAILURE);
65
+ config_1.config.openaiModel = 'test-openai-chosen-later';
66
+ yield service.analyzeFailure(FAILURE);
67
+ (0, chai_1.expect)(JSON.parse(ai.calls[1].body).model).to.equal('test-openai-chosen-later');
68
+ }));
69
+ it('a provider set only by the plugin options is on at the first heal', () => __awaiter(void 0, void 0, void 0, function* () {
70
+ // AI_SERVICE is made when the plugin loads, before ServerManager applies
71
+ // the plugin's AI options (syncDatabaseAndAIConfig, through updateConfig).
72
+ (0, chai_1.expect)(AIService_1.AI_SERVICE.isEnabled()).to.equal(false);
73
+ (0, config_1.updateConfig)({
74
+ aiProvider: 'ollama',
75
+ aiBaseUrl: fake_ai_provider_1.OLLAMA_URL,
76
+ ollamaModel: `test-ollama-${Math.random().toString(36).slice(2)}`,
77
+ });
78
+ ai.answer({ text: '{"x": 120, "y": 340}' });
79
+ const healed = yield new VisualAiHealingProvider_1.VisualAiHealingProvider().heal({
80
+ sessionId: 'sess-switch',
81
+ driver: {},
82
+ strategy: 'id',
83
+ selector: 'login',
84
+ screenshotBase64: SCREEN,
85
+ });
86
+ (0, chai_1.expect)(ai.calls).to.have.length(1);
87
+ (0, chai_1.expect)(healed === null || healed === void 0 ? void 0 : healed.rect).to.deep.equal({ x: 100, y: 320, width: 40, height: 40 });
88
+ }));
89
+ describe('the healing tiers', () => {
90
+ beforeEach(() => {
91
+ // The server started with a provider that has no key.
92
+ ai.use('gemini', { key: false });
93
+ (0, chai_1.expect)(AIService_1.AI_SERVICE.isEnabled()).to.equal(false);
94
+ // Then a configured one is chosen on the AI engine page.
95
+ ai.use('anthropic');
96
+ });
97
+ it('the LLM tier asks the provider chosen on the AI engine page', () => __awaiter(void 0, void 0, void 0, function* () {
98
+ ai.answer({
99
+ text: '{"recommendedXpath": "//button[@text=\'Log in\']", "reason": "same text"}',
100
+ });
101
+ const driver = { findElement: sinon_1.default.stub().resolves({ ELEMENT: 'el-1' }) };
102
+ const healed = yield new LlmHealingProvider_1.LlmHealingProvider().heal({
103
+ sessionId: 'sess-switch',
104
+ driver,
105
+ strategy: 'id',
106
+ selector: 'login',
107
+ pageSource: '<hierarchy/>',
108
+ screenshotBase64: SCREEN,
109
+ });
110
+ (0, chai_1.expect)(ai.calls).to.have.length(1);
111
+ (0, chai_1.expect)(healed === null || healed === void 0 ? void 0 : healed.id).to.equal('el-1');
112
+ (0, chai_1.expect)(driver.findElement.firstCall.args).to.deep.equal([
113
+ 'xpath',
114
+ "//button[@text='Log in']",
115
+ ]);
116
+ }));
117
+ it('the visual tier asks the provider chosen on the AI engine page', () => __awaiter(void 0, void 0, void 0, function* () {
118
+ ai.answer({ text: '{"x": 120, "y": 340}' });
119
+ const healed = yield new VisualAiHealingProvider_1.VisualAiHealingProvider().heal({
120
+ sessionId: 'sess-switch',
121
+ driver: {},
122
+ strategy: 'id',
123
+ selector: 'login',
124
+ screenshotBase64: SCREEN,
125
+ });
126
+ (0, chai_1.expect)(ai.calls).to.have.length(1);
127
+ (0, chai_1.expect)(healed === null || healed === void 0 ? void 0 : healed.rect).to.deep.equal({ x: 100, y: 320, width: 40, height: 40 });
128
+ }));
129
+ });
130
+ });