@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,71 @@
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
+ const chai_1 = require("chai");
16
+ const http_1 = __importDefault(require("http"));
17
+ const healReport_1 = require("../../src/gateway/healReport");
18
+ /**
19
+ * How a node hands a heal back to its hub on the answer to the hub's command
20
+ * (healReport.ts): what goes on the answer, and what the hub accepts off it.
21
+ */
22
+ describe('A heal reported to the hub', () => {
23
+ const answer = () => new http_1.default.ServerResponse(new http_1.default.IncomingMessage(null));
24
+ const heal = (selector) => ({
25
+ originalSelector: selector,
26
+ originalStrategy: 'xpath',
27
+ healedSelector: "//*[@text='Anmelden']",
28
+ healedStrategy: 'xpath',
29
+ confidence: 0.82,
30
+ tier: 2,
31
+ });
32
+ it('goes on the answer and comes off it whole, whatever its characters', () => {
33
+ const res = answer();
34
+ const sent = heal("//*[@text='Ünïcödé “quotes”']");
35
+ (0, chai_1.expect)((0, healReport_1.runReportingHeals)(res, () => (0, healReport_1.reportHeal)(sent))).to.equal('reported');
36
+ const headers = { [healReport_1.HEAL_REPORT_HEADER]: String(res.getHeader(healReport_1.HEAL_REPORT_HEADER)) };
37
+ (0, chai_1.expect)((0, healReport_1.takeHealReport)(headers)).to.deep.equal(sent);
38
+ (0, chai_1.expect)(headers, 'taken off the headers').to.not.have.property(healReport_1.HEAL_REPORT_HEADER);
39
+ });
40
+ it('is not sent for a command that did not come from a hub', () => {
41
+ (0, chai_1.expect)((0, healReport_1.reportHeal)(heal('//a'))).to.equal('not-from-hub');
42
+ });
43
+ it('is not sent when too long for the hub to read, so the command still answers', () => {
44
+ const res = answer();
45
+ const reported = (0, healReport_1.runReportingHeals)(res, () => (0, healReport_1.reportHeal)(heal(`//${'x'.repeat(10000)}`)));
46
+ (0, chai_1.expect)(reported).to.equal('too-long');
47
+ (0, chai_1.expect)(res.getHeader(healReport_1.HEAL_REPORT_HEADER)).to.equal(undefined);
48
+ });
49
+ it('is not put on an answer that has closed, by work the command left running', () => __awaiter(void 0, void 0, void 0, function* () {
50
+ const res = answer();
51
+ let release;
52
+ const gate = new Promise((resolve) => (release = resolve));
53
+ // Async work started during the command keeps its context after it.
54
+ const lingering = (0, healReport_1.runReportingHeals)(res, () => __awaiter(void 0, void 0, void 0, function* () {
55
+ yield gate;
56
+ return (0, healReport_1.reportHeal)(heal('//late'));
57
+ }));
58
+ res.emit('close');
59
+ release();
60
+ (0, chai_1.expect)(yield lingering).to.equal('not-from-hub');
61
+ }));
62
+ it('is ignored when it is not one', () => {
63
+ const notJson = { [healReport_1.HEAL_REPORT_HEADER]: 'not base64 json' };
64
+ const notAHeal = {
65
+ [healReport_1.HEAL_REPORT_HEADER]: Buffer.from('{"originalSelector":1}').toString('base64url'),
66
+ };
67
+ (0, chai_1.expect)((0, healReport_1.takeHealReport)(notJson)).to.equal(undefined);
68
+ (0, chai_1.expect)((0, healReport_1.takeHealReport)(notAHeal)).to.equal(undefined);
69
+ (0, chai_1.expect)((0, healReport_1.takeHealReport)({})).to.equal(undefined);
70
+ });
71
+ });
@@ -187,7 +187,11 @@ describe('HealingOrchestrator instrumentation', () => {
187
187
  (0, chai_1.expect)(mockSpan.end.calledOnce).to.equal(true);
188
188
  }));
189
189
  it('marks span ERROR and ends on context-collection failure (driver throws)', () => __awaiter(void 0, void 0, void 0, function* () {
190
- const orch = makeOrchestratorWithProviders([]);
190
+ // A tier to run, or nothing is collected at all; it never runs here.
191
+ const heal = sinon_1.default.stub().resolves(null);
192
+ const orch = makeOrchestratorWithProviders([
193
+ { name: 'p1', tier: types_1.HealingTier.TIER_1_RECOVERY, heal },
194
+ ]);
191
195
  const driver = {
192
196
  getPageSource: () => __awaiter(void 0, void 0, void 0, function* () {
193
197
  throw new Error('disconnected');
@@ -198,6 +202,7 @@ describe('HealingOrchestrator instrumentation', () => {
198
202
  (0, chai_1.expect)(result).to.equal(null);
199
203
  (0, chai_1.expect)(mockSpan.addEvent.calledWith('context_collection_failed')).to.equal(true);
200
204
  (0, chai_1.expect)(mockSpan.end.calledOnce).to.equal(true);
205
+ (0, chai_1.expect)(heal.called).to.equal(false);
201
206
  }));
202
207
  it('does not put the original selector text on span attributes (cardinality discipline)', () => __awaiter(void 0, void 0, void 0, function* () {
203
208
  const provider = {
@@ -51,54 +51,84 @@ describe('filterProvidersByTier (§2.7 healing-tier gate)', () => {
51
51
  const result = (0, HealingOrchestrator_1.filterProvidersByTier)(providers, []);
52
52
  (0, chai_1.expect)(result).to.deep.equal([]);
53
53
  });
54
- // MINOR fix (Phase 2a review): the capability accessor must fail OPEN on a
55
- // malformed healingTiers cap. Pre-fix, an all-non-numeric array (e.g.
56
- // ["1","2"]) filtered to [] and silently DISABLED healing; the contract is
57
- // malformed → run all tiers. The cap is meant to RESTRICT healing, never to
58
- // disable it — so an explicitly-empty [] also coerces to run-all (opting out
59
- // of healing entirely is not a supported use case).
60
- describe('coerceHealingTiersCap (malformed cap → fail open)', () => {
61
- it('passes a numeric array through unchanged', () => {
54
+ // The value is a privacy control: a session that leaves out Visual AI (4)
55
+ // and the LLM (5) keeps its screen away from the AI provider. So a value
56
+ // that can't be read fails closed for those two, and open for the rest:
57
+ // the tiers that stay on the server (1, 2, 3) still run. Through 2.14 any
58
+ // such value, and an empty list, ran every tier, the AI ones included.
59
+ describe('coerceHealingTiersCap', () => {
60
+ it('passes a list of tier numbers through unchanged', () => {
62
61
  (0, chai_1.expect)((0, HealingOrchestrator_1.coerceHealingTiersCap)([1, 3])).to.deep.equal([1, 3]);
62
+ (0, chai_1.expect)((0, HealingOrchestrator_1.coerceHealingTiersCap)([5, 4, 1])).to.deep.equal([5, 4, 1]);
63
63
  });
64
- it('keeps only the numeric entries of a mixed array', () => {
65
- (0, chai_1.expect)((0, HealingOrchestrator_1.coerceHealingTiersCap)([1, '2'])).to.deep.equal([1]);
66
- });
67
- it('non-array (string/undefined/null) → undefined (run all)', () => {
68
- (0, chai_1.expect)((0, HealingOrchestrator_1.coerceHealingTiersCap)('1,2')).to.equal(undefined);
64
+ it('runs every tier when the session sets nothing', () => {
69
65
  (0, chai_1.expect)((0, HealingOrchestrator_1.coerceHealingTiersCap)(undefined)).to.equal(undefined);
70
66
  (0, chai_1.expect)((0, HealingOrchestrator_1.coerceHealingTiersCap)(null)).to.equal(undefined);
71
67
  });
72
- it('all-non-numeric array ["1","2"] → undefined → ALL providers run (fail open)', () => {
73
- const coerced = (0, HealingOrchestrator_1.coerceHealingTiersCap)(['1', '2']);
74
- (0, chai_1.expect)(coerced).to.equal(undefined);
75
- (0, chai_1.expect)((0, HealingOrchestrator_1.filterProvidersByTier)(providers, coerced)).to.deep.equal(providers);
76
- });
77
- it('explicitly-empty [] also → undefined (run all; the cap restricts, it cannot disable)', () => {
78
- (0, chai_1.expect)((0, HealingOrchestrator_1.coerceHealingTiersCap)([])).to.equal(undefined);
68
+ it('runs no tier for an empty list', () => {
69
+ (0, chai_1.expect)((0, HealingOrchestrator_1.coerceHealingTiersCap)([])).to.deep.equal([]);
79
70
  });
71
+ for (const [label, value] of [
72
+ ['a string', '1,2'],
73
+ ['numbers as strings', ['1', '2']],
74
+ ['a list with a string in it', [1, '2']],
75
+ ['a number that is no tier', [1, 6]],
76
+ ['tier 0', [0]],
77
+ ['a fraction', [1.5]],
78
+ ['a lone number', 4],
79
+ ['an object', { llm: false }],
80
+ ['true', true],
81
+ ]) {
82
+ it(`runs only the tiers that stay on the server for ${label}`, () => {
83
+ (0, chai_1.expect)((0, HealingOrchestrator_1.coerceHealingTiersCap)(value)).to.deep.equal([1, 2, 3]);
84
+ (0, chai_1.expect)((0, HealingOrchestrator_1.filterProvidersByTier)(providers, (0, HealingOrchestrator_1.coerceHealingTiersCap)(value))).to.deep.equal(providers.slice(0, 3));
85
+ });
86
+ }
80
87
  });
81
88
  // What CommandInterceptor hands attemptHealing, read from the session's
82
89
  // capabilities.
83
90
  describe('healingTiersFromCaps', () => {
84
91
  it('reads xe:options.healingTiers', () => {
85
- (0, chai_1.expect)((0, HealingOrchestrator_1.healingTiersFromCaps)({ 'xe:options': { healingTiers: [1, 2] } })).to.deep.equal([
86
- 1, 2,
87
- ]);
92
+ (0, chai_1.expect)((0, HealingOrchestrator_1.healingTiersFromCaps)({ 'xe:options': { healingTiers: [1, 2] } })).to.deep.equal({
93
+ tiers: [1, 2],
94
+ });
88
95
  });
89
96
  it('still reads the xenon:options alias', () => {
90
- (0, chai_1.expect)((0, HealingOrchestrator_1.healingTiersFromCaps)({ 'xenon:options': { healingTiers: [3] } })).to.deep.equal([3]);
97
+ (0, chai_1.expect)((0, HealingOrchestrator_1.healingTiersFromCaps)({ 'xenon:options': { healingTiers: [3] } })).to.deep.equal({
98
+ tiers: [3],
99
+ });
91
100
  });
92
101
  it('prefers xe:options when both set it', () => {
93
102
  (0, chai_1.expect)((0, HealingOrchestrator_1.healingTiersFromCaps)({
94
103
  'xenon:options': { healingTiers: [5] },
95
104
  'xe:options': { healingTiers: [1] },
96
- })).to.deep.equal([1]);
105
+ })).to.deep.equal({ tiers: [1] });
106
+ });
107
+ it('runs every tier with no capabilities, or none set', () => {
108
+ (0, chai_1.expect)((0, HealingOrchestrator_1.healingTiersFromCaps)(undefined)).to.deep.equal({ tiers: undefined });
109
+ (0, chai_1.expect)((0, HealingOrchestrator_1.healingTiersFromCaps)({})).to.deep.equal({ tiers: undefined });
110
+ (0, chai_1.expect)((0, HealingOrchestrator_1.healingTiersFromCaps)({ 'xe:options': {} })).to.deep.equal({ tiers: undefined });
97
111
  });
98
- it('fails open with no capabilities or a malformed value', () => {
99
- (0, chai_1.expect)((0, HealingOrchestrator_1.healingTiersFromCaps)(undefined)).to.equal(undefined);
100
- (0, chai_1.expect)((0, HealingOrchestrator_1.healingTiersFromCaps)({})).to.equal(undefined);
101
- (0, chai_1.expect)((0, HealingOrchestrator_1.healingTiersFromCaps)({ 'xe:options': { healingTiers: ['1'] } })).to.equal(undefined);
112
+ it('names a value it could not read, for the log', () => {
113
+ (0, chai_1.expect)((0, HealingOrchestrator_1.healingTiersFromCaps)({ 'xe:options': { healingTiers: ['1'] } })).to.deep.equal({
114
+ tiers: [1, 2, 3],
115
+ unreadable: 'healingTiers is ["1"], not a list of tier numbers from 1 to 5',
116
+ });
117
+ });
118
+ // xenonOptionsIn skips a namespace that isn't an object, so this one used
119
+ // to read as "nothing set", and ran every tier.
120
+ it('runs only the tiers that stay on the server when xe:options is not an object', () => {
121
+ (0, chai_1.expect)((0, HealingOrchestrator_1.healingTiersFromCaps)({ 'xe:options': '{"healingTiers":[1]}' })).to.deep.equal({
122
+ tiers: [1, 2, 3],
123
+ unreadable: 'xe:options is "{\\"healingTiers\\":[1]}", not an object',
124
+ });
125
+ (0, chai_1.expect)((0, HealingOrchestrator_1.healingTiersFromCaps)({
126
+ 'xenon:options': ['healingTiers'],
127
+ 'xe:options': { healingTiers: [1, 2, 3, 4, 5] },
128
+ })).to.deep.equal({
129
+ tiers: [1, 2, 3],
130
+ unreadable: 'xenon:options is ["healingTiers"], not an object',
131
+ });
102
132
  });
103
133
  });
104
134
  });
@@ -136,6 +166,20 @@ describe('HealingOrchestrator.attemptHealing allowedTiers dispatch gate', () =>
136
166
  yield orchestrator.attemptHealing('sess-1', mockDriver, 'xpath', '//broken', [1]);
137
167
  (0, chai_1.expect)(dispatched).to.deep.equal(['ResilioTree Provider']);
138
168
  }));
169
+ it('collects nothing and dispatches nothing when no tier is allowed', () => __awaiter(void 0, void 0, void 0, function* () {
170
+ let collected = 0;
171
+ const driver = Object.assign(Object.assign({}, mockDriver), { getPageSource: () => __awaiter(void 0, void 0, void 0, function* () {
172
+ collected++;
173
+ return '<xml/>';
174
+ }), getScreenshot: () => __awaiter(void 0, void 0, void 0, function* () {
175
+ collected++;
176
+ return 'fake-screenshot';
177
+ }) });
178
+ const healed = yield orchestrator.attemptHealing('sess-3', driver, 'xpath', '//broken', []);
179
+ (0, chai_1.expect)(healed).to.equal(null);
180
+ (0, chai_1.expect)(dispatched).to.deep.equal([]);
181
+ (0, chai_1.expect)(collected).to.equal(0);
182
+ }));
139
183
  it('dispatches to the full provider set when allowedTiers is omitted (regression)', () => __awaiter(void 0, void 0, void 0, function* () {
140
184
  yield orchestrator.attemptHealing('sess-2', mockDriver, 'xpath', '//broken');
141
185
  (0, chai_1.expect)(dispatched).to.deep.equal([
@@ -0,0 +1,219 @@
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 typedi_1 = require("typedi");
19
+ const base_driver_1 = require("@appium/base-driver");
20
+ const CommandInterceptor_1 = require("../../src/interceptors/CommandInterceptor");
21
+ const HealingOrchestrator_1 = require("../../src/services/healing/HealingOrchestrator");
22
+ const AIService_1 = require("../../src/services/AIService");
23
+ const SelfHealingSwitch_1 = require("../../src/services/settings/SelfHealingSwitch");
24
+ const SessionManager_1 = require("../../src/sessions/SessionManager");
25
+ const IPluginArgs_1 = require("../../src/interfaces/IPluginArgs");
26
+ const container_registration_1 = require("../helpers/container-registration");
27
+ const scratch_database_1 = require("../helpers/scratch-database");
28
+ /**
29
+ * A session's `xe:options.healingTiers` keeps its screen away from the AI
30
+ * provider: with `[1, 2, 3]`, Visual AI (4) and the LLM (5) never run, so the
31
+ * screenshot and page source never leave the server.
32
+ *
33
+ * Through 2.14 the interceptor read the tiers from SESSION_MANAGER, which holds
34
+ * a local session only with the dashboard on or a video recorded. A session
35
+ * with `record_video: false` on a server with the dashboard off (the default),
36
+ * a node's for its hub included, wasn't there: the interceptor found no
37
+ * capabilities and ran every tier. The tiers are now read from the session's
38
+ * own driver, which Appium hands the plugin with every command. These sessions
39
+ * are registered nowhere, as such a session isn't.
40
+ */
41
+ describe("a session's healing tiers, on a session the dashboard doesn't track", () => {
42
+ (0, scratch_database_1.useScratchDatabase)();
43
+ let restoreContainer;
44
+ let dispatched;
45
+ let visualFind;
46
+ let healLocator;
47
+ const drivers = [];
48
+ const pluginArgs = Object.assign(Object.assign({}, IPluginArgs_1.DefaultPluginArgs), { enableDashboard: false });
49
+ beforeEach(() => {
50
+ restoreContainer = (0, container_registration_1.saveRegistrations)(HealingOrchestrator_1.HealingOrchestrator, SelfHealingSwitch_1.SelfHealingSwitch);
51
+ // The switch as at boot with nothing saved: healing on.
52
+ typedi_1.Container.set(SelfHealingSwitch_1.SelfHealingSwitch, new SelfHealingSwitch_1.SelfHealingSwitch());
53
+ const etalons = {
54
+ getSignature: () => __awaiter(void 0, void 0, void 0, function* () { return null; }),
55
+ saveSignature: () => __awaiter(void 0, void 0, void 0, function* () { }),
56
+ };
57
+ const orchestrator = new HealingOrchestrator_1.HealingOrchestrator(etalons);
58
+ dispatched = [];
59
+ const providers = orchestrator.providers;
60
+ // The three local tiers are only recorded. Visual AI and the LLM run as
61
+ // they are, up to the AI service, which is stubbed: what reaches it is
62
+ // what would have gone to the provider.
63
+ for (const provider of providers.slice(0, 3)) {
64
+ provider.heal = () => __awaiter(void 0, void 0, void 0, function* () {
65
+ dispatched.push(provider.name);
66
+ return null;
67
+ });
68
+ }
69
+ for (const provider of providers.slice(3)) {
70
+ const heal = provider.heal.bind(provider);
71
+ provider.heal = (context) => __awaiter(void 0, void 0, void 0, function* () {
72
+ dispatched.push(provider.name);
73
+ return heal(context);
74
+ });
75
+ }
76
+ typedi_1.Container.set(HealingOrchestrator_1.HealingOrchestrator, orchestrator);
77
+ sinon_1.default.stub(AIService_1.AI_SERVICE, 'isEnabled').returns(true);
78
+ visualFind = sinon_1.default.stub(AIService_1.AI_SERVICE, 'visualFind').resolves(null);
79
+ healLocator = sinon_1.default.stub(AIService_1.AI_SERVICE, 'healLocator').resolves(null);
80
+ });
81
+ afterEach(() => __awaiter(void 0, void 0, void 0, function* () {
82
+ sinon_1.default.restore();
83
+ restoreContainer();
84
+ for (const driver of drivers.splice(0))
85
+ yield driver.deleteSession();
86
+ }));
87
+ /**
88
+ * The driver Appium hands the plugin for a session's commands: a session
89
+ * created from the capabilities the client sent, as Appium creates it.
90
+ */
91
+ function sessionWith(xenonOptions) {
92
+ return __awaiter(this, void 0, void 0, function* () {
93
+ const driver = new base_driver_1.BaseDriver({});
94
+ // Every driver warns that it doesn't know `xe:options`; not this spec's business.
95
+ sinon_1.default.stub(driver.log, 'warn');
96
+ const alwaysMatch = {
97
+ platformName: 'Android',
98
+ 'appium:automationName': 'UiAutomator2',
99
+ };
100
+ if (xenonOptions)
101
+ Object.assign(alwaysMatch, xenonOptions);
102
+ yield driver.createSession({ alwaysMatch, firstMatch: [{}] });
103
+ drivers.push(driver);
104
+ Object.assign(driver, {
105
+ getPageSource: () => __awaiter(this, void 0, void 0, function* () { return '<hierarchy><node text="Pay now"/></hierarchy>'; }),
106
+ getScreenshot: () => __awaiter(this, void 0, void 0, function* () { return 'iVBORw0KGgo='; }),
107
+ findElements: () => __awaiter(this, void 0, void 0, function* () { return []; }),
108
+ });
109
+ return driver;
110
+ });
111
+ }
112
+ /** A findElement the driver answers with NoSuchElement, as a broken selector does. */
113
+ function findMissing(driver_1, _a) {
114
+ return __awaiter(this, arguments, void 0, function* (driver, { isHub }) {
115
+ const sessionId = driver.sessionId;
116
+ return typedi_1.Container.get(CommandInterceptor_1.CommandInterceptor)
117
+ .handle(() => __awaiter(this, void 0, void 0, function* () {
118
+ throw Object.assign(new Error('NoSuchElement: An element could not be located'), {
119
+ name: 'NoSuchElementError',
120
+ });
121
+ }), driver, 'findElement', ['xpath', '//android.widget.Button[@text="Pay"]', sessionId], pluginArgs, isHub)
122
+ .then(() => undefined, (err) => err);
123
+ });
124
+ }
125
+ for (const [where, isHub] of [
126
+ ['on a standalone server', true],
127
+ ["on a node running its hub's session", false],
128
+ ]) {
129
+ describe(where, () => {
130
+ it('never calls the AI provider for a session that allows tiers 1, 2 and 3', () => __awaiter(void 0, void 0, void 0, function* () {
131
+ const driver = yield sessionWith({ 'xe:options': { healingTiers: [1, 2, 3] } });
132
+ // The case: with the dashboard off and no video, nothing registers it.
133
+ (0, chai_1.expect)(SessionManager_1.SESSION_MANAGER.getSession(driver.sessionId)).to.equal(undefined);
134
+ const thrown = yield findMissing(driver, { isHub });
135
+ (0, chai_1.expect)(visualFind.called, 'Visual AI sent the screenshot').to.equal(false);
136
+ (0, chai_1.expect)(healLocator.called, 'the LLM was sent the page source').to.equal(false);
137
+ (0, chai_1.expect)(dispatched).to.deep.equal([
138
+ 'ResilioTree Provider',
139
+ 'Fuzzy XML Provider',
140
+ 'OCR Text Provider',
141
+ ]);
142
+ // Nothing healed it, so the test sees its own failure.
143
+ (0, chai_1.expect)(thrown === null || thrown === void 0 ? void 0 : thrown.name).to.equal('NoSuchElementError');
144
+ }));
145
+ it('reads the xenon:options alias the same way', () => __awaiter(void 0, void 0, void 0, function* () {
146
+ const driver = yield sessionWith({ 'xenon:options': { healingTiers: [2] } });
147
+ yield findMissing(driver, { isHub });
148
+ (0, chai_1.expect)(visualFind.called).to.equal(false);
149
+ (0, chai_1.expect)(healLocator.called).to.equal(false);
150
+ (0, chai_1.expect)(dispatched).to.deep.equal(['Fuzzy XML Provider']);
151
+ }));
152
+ it('still runs every tier for a session that asks for none in particular', () => __awaiter(void 0, void 0, void 0, function* () {
153
+ const driver = yield sessionWith(null);
154
+ yield findMissing(driver, { isHub });
155
+ (0, chai_1.expect)(dispatched).to.deep.equal([
156
+ 'ResilioTree Provider',
157
+ 'Fuzzy XML Provider',
158
+ 'OCR Text Provider',
159
+ 'Visual AI Provider',
160
+ 'LLM Reasoning Provider',
161
+ ]);
162
+ (0, chai_1.expect)(visualFind.calledOnce).to.equal(true);
163
+ (0, chai_1.expect)(healLocator.calledOnce).to.equal(true);
164
+ }));
165
+ });
166
+ }
167
+ describe('a value that is not a list of tier numbers', () => {
168
+ for (const [label, value] of [
169
+ ['a string', '1,2,3'],
170
+ ['numbers as strings', ['1', '2']],
171
+ ['a list with a string in it', [4, 'llm']],
172
+ ['a number that is no tier', [1, 6]],
173
+ ['a lone number', 4],
174
+ ['an object', { visualAi: false }],
175
+ ]) {
176
+ it(`runs only the tiers that stay on the server: ${label}`, () => __awaiter(void 0, void 0, void 0, function* () {
177
+ const driver = yield sessionWith({ 'xe:options': { healingTiers: value } });
178
+ yield findMissing(driver, { isHub: true });
179
+ (0, chai_1.expect)(visualFind.called).to.equal(false);
180
+ (0, chai_1.expect)(healLocator.called).to.equal(false);
181
+ (0, chai_1.expect)(dispatched).to.deep.equal([
182
+ 'ResilioTree Provider',
183
+ 'Fuzzy XML Provider',
184
+ 'OCR Text Provider',
185
+ ]);
186
+ }));
187
+ }
188
+ it('runs only the tiers that stay on the server when xe:options is not an object', () => __awaiter(void 0, void 0, void 0, function* () {
189
+ const driver = yield sessionWith({ 'xe:options': '{"healingTiers":[1,2,3]}' });
190
+ yield findMissing(driver, { isHub: true });
191
+ (0, chai_1.expect)(visualFind.called).to.equal(false);
192
+ (0, chai_1.expect)(healLocator.called).to.equal(false);
193
+ (0, chai_1.expect)(dispatched).to.deep.equal([
194
+ 'ResilioTree Provider',
195
+ 'Fuzzy XML Provider',
196
+ 'OCR Text Provider',
197
+ ]);
198
+ }));
199
+ it('says so in the log, once per session', () => __awaiter(void 0, void 0, void 0, function* () {
200
+ const warn = sinon_1.default.stub(typedi_1.Container.get(CommandInterceptor_1.CommandInterceptor).log, 'warn');
201
+ const driver = yield sessionWith({ 'xe:options': { healingTiers: ['1', '2'] } });
202
+ yield findMissing(driver, { isHub: true });
203
+ yield findMissing(driver, { isHub: true });
204
+ const said = warn.getCalls().filter((c) => /healingTiers/.test(String(c.args[0])));
205
+ (0, chai_1.expect)(said).to.have.length(1);
206
+ (0, chai_1.expect)(String(said[0].args[0])).to.contain('["1","2"]');
207
+ }));
208
+ });
209
+ it('heals with no tier, and takes no screenshot, for a session that lists none', () => __awaiter(void 0, void 0, void 0, function* () {
210
+ const driver = yield sessionWith({ 'xe:options': { healingTiers: [] } });
211
+ const screenshot = sinon_1.default.spy(driver, 'getScreenshot');
212
+ const source = sinon_1.default.spy(driver, 'getPageSource');
213
+ const thrown = yield findMissing(driver, { isHub: true });
214
+ (0, chai_1.expect)(dispatched).to.deep.equal([]);
215
+ (0, chai_1.expect)(screenshot.called).to.equal(false);
216
+ (0, chai_1.expect)(source.called).to.equal(false);
217
+ (0, chai_1.expect)(thrown === null || thrown === void 0 ? void 0 : thrown.name).to.equal('NoSuchElementError');
218
+ }));
219
+ });