@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,301 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
36
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
37
+ return new (P || (P = Promise))(function (resolve, reject) {
38
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
39
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
40
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
41
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
42
+ });
43
+ };
44
+ var __importDefault = (this && this.__importDefault) || function (mod) {
45
+ return (mod && mod.__esModule) ? mod : { "default": mod };
46
+ };
47
+ Object.defineProperty(exports, "__esModule", { value: true });
48
+ require("reflect-metadata");
49
+ const chai_1 = require("chai");
50
+ const sinon_1 = __importDefault(require("sinon"));
51
+ const typedi_1 = require("typedi");
52
+ const plugin_1 = require("../../src/plugin");
53
+ const NodeDevices_1 = __importDefault(require("../../src/device-managers/NodeDevices"));
54
+ const DeviceService = __importStar(require("../../src/data-service/device-service"));
55
+ const ActiveLeases = __importStar(require("../../src/services/lease/activeLeases"));
56
+ const device_utils_1 = require("../../src/device-utils");
57
+ const CommandInterceptor_1 = require("../../src/interceptors/CommandInterceptor");
58
+ const HealingOrchestrator_1 = require("../../src/services/healing/HealingOrchestrator");
59
+ const types_1 = require("../../src/services/healing/types");
60
+ const OmniVisionService_1 = require("../../src/services/omni-vision/OmniVisionService");
61
+ const AutowaitService_1 = require("../../src/services/autowait/AutowaitService");
62
+ const SessionLifecycleService_1 = require("../../src/services/SessionLifecycleService");
63
+ const SessionMetricsService_1 = require("../../src/services/metrics/SessionMetricsService");
64
+ const OrphanSweeper_1 = require("../../src/services/OrphanSweeper");
65
+ const PhoneNetworkRestore_1 = require("../../src/services/network/PhoneNetworkRestore");
66
+ const SelfHealingSwitch_1 = require("../../src/services/settings/SelfHealingSwitch");
67
+ const AIService_1 = require("../../src/services/AIService");
68
+ const event_manager_1 = require("../../src/dashboard/event-manager");
69
+ const IPluginArgs_1 = require("../../src/interfaces/IPluginArgs");
70
+ const container_registration_1 = require("../helpers/container-registration");
71
+ const scratch_database_1 = require("../helpers/scratch-database");
72
+ /**
73
+ * An element Xenon finds in a screenshot (a `-custom:ai-*` find, or an OCR or
74
+ * Visual AI heal) is a box on the screen it remembers by id. Through 2.14 it
75
+ * remembered every one, from every session, until the server restarted:
76
+ * device control's locator test and an icon `smartTap` added one per call
77
+ * that nothing ever read, and any session could act on another's. Now an
78
+ * element belongs to the session that found it and goes when that session
79
+ * ends, and the store has a size limit.
80
+ *
81
+ * OCR, the AI provider and the drivers are stubbed: no Tesseract, no AI call,
82
+ * no phone.
83
+ */
84
+ const W3C = 'element-6066-11e4-a52e-4f735466cecf';
85
+ const WORDS = [
86
+ { text: 'Sign', confidence: 90, bbox: { x0: 100, y0: 200, x1: 150, y1: 224 } },
87
+ { text: 'in', confidence: 90, bbox: { x0: 158, y0: 200, x1: 180, y1: 224 } },
88
+ ];
89
+ const box = (id) => ({
90
+ id,
91
+ rect: { x: 0, y: 0, width: 10, height: 10 },
92
+ confidence: 0.9,
93
+ });
94
+ describe('Elements found in a screenshot belong to their session', () => {
95
+ (0, scratch_database_1.useScratchDatabase)();
96
+ let restoreContainer;
97
+ let omni;
98
+ const driverFor = (sessionId) => ({
99
+ sessionId,
100
+ caps: { platformName: 'Android' },
101
+ getScreenshot: sinon_1.default.stub().resolves('aGVsbG8='),
102
+ performActions: sinon_1.default.stub().resolves(),
103
+ releaseActions: sinon_1.default.stub().resolves(),
104
+ });
105
+ beforeEach(() => {
106
+ restoreContainer = (0, container_registration_1.saveRegistrations)(OmniVisionService_1.OmniVisionService, HealingOrchestrator_1.HealingOrchestrator, SelfHealingSwitch_1.SelfHealingSwitch);
107
+ typedi_1.Container.set(OmniVisionService_1.OmniVisionService, new OmniVisionService_1.OmniVisionService());
108
+ typedi_1.Container.set(SelfHealingSwitch_1.SelfHealingSwitch, new SelfHealingSwitch_1.SelfHealingSwitch());
109
+ omni = typedi_1.Container.get(OmniVisionService_1.OmniVisionService);
110
+ sinon_1.default.stub(omni, 'performOcr').resolves({ text: '', words: WORDS });
111
+ sinon_1.default.stub(event_manager_1.DASHBORD_EVENT_MANAGER, 'afterSessionCommand').resolves();
112
+ });
113
+ afterEach(() => {
114
+ restoreContainer();
115
+ sinon_1.default.restore();
116
+ });
117
+ const run = (driver, command, args, next = () => null) => typedi_1.Container.get(CommandInterceptor_1.CommandInterceptor).handle(() => __awaiter(void 0, void 0, void 0, function* () { return next(); }), driver, command, [...args, driver.sessionId], Object.assign({}, IPluginArgs_1.DefaultPluginArgs), false);
118
+ const failure = (promise) => __awaiter(void 0, void 0, void 0, function* () {
119
+ try {
120
+ yield promise;
121
+ }
122
+ catch (err) {
123
+ return err;
124
+ }
125
+ throw new Error('expected the command to fail');
126
+ });
127
+ const held = () => omni.elements.size;
128
+ it("answers another session's command on it with no such element", () => __awaiter(void 0, void 0, void 0, function* () {
129
+ const a = driverFor('sess-a');
130
+ const b = driverFor('sess-b');
131
+ const found = yield run(a, 'findElement', ['-custom:ai-text', 'Sign in']);
132
+ const err = yield failure(run(b, 'click', [found[W3C]]));
133
+ (0, chai_1.expect)(err.error).to.equal('no such element');
134
+ (0, chai_1.expect)(b.performActions.called, "tapped on the other session's phone").to.equal(false);
135
+ yield run(a, 'click', [found[W3C]]);
136
+ (0, chai_1.expect)(a.performActions.calledOnce).to.equal(true);
137
+ }));
138
+ it('keeps a healed element for the session it healed in', () => __awaiter(void 0, void 0, void 0, function* () {
139
+ const attemptHealing = sinon_1.default.stub().resolves({
140
+ id: 'healed_ocr_1_abc',
141
+ tier: types_1.HealingTier.TIER_3_LOCAL_OCR,
142
+ confidence: 0.9,
143
+ originalSelector: "//*[@text='Sign in']",
144
+ originalStrategy: 'xpath',
145
+ recommendedSelector: 'ocr:text="Sign in"',
146
+ recommendedStrategy: 'xenon:visual',
147
+ text: 'Sign in',
148
+ rect: { x: 100, y: 200, width: 80, height: 24 },
149
+ });
150
+ typedi_1.Container.set(HealingOrchestrator_1.HealingOrchestrator, { attemptHealing });
151
+ const a = driverFor('sess-a');
152
+ yield run(a, 'findElement', ['xpath', "//*[@text='Sign in']"], () => {
153
+ throw Object.assign(new Error('NoSuchElement'), { name: 'NoSuchElementError' });
154
+ });
155
+ (0, chai_1.expect)(yield run(a, 'getText', ['healed_ocr_1_abc'])).to.equal('Sign in');
156
+ const err = yield failure(run(driverFor('sess-b'), 'getText', ['healed_ocr_1_abc']));
157
+ (0, chai_1.expect)(err.error).to.equal('no such element');
158
+ }));
159
+ it("keeps nothing for device control's locator test", () => __awaiter(void 0, void 0, void 0, function* () {
160
+ // As POST /control/<udid>/test-locator calls it: the ids are only shown.
161
+ const manual = { sessionId: 'manual_R5CT', getScreenshot: sinon_1.default.stub().resolves('aGVsbG8=') };
162
+ const found = yield omni.findByText(manual, 'Sign in', { throwOnError: true });
163
+ (0, chai_1.expect)(found).to.have.length(1);
164
+ (0, chai_1.expect)(held()).to.equal(0);
165
+ }));
166
+ it('keeps nothing for an icon smartTap, which taps at once', () => __awaiter(void 0, void 0, void 0, function* () {
167
+ sinon_1.default.stub(AIService_1.AI_SERVICE, 'visualFind').resolves({ x: 50, y: 50 });
168
+ const result = yield omni.omniClickByIcon(driverFor('sess-a'), { icon: 'the cart' });
169
+ (0, chai_1.expect)(result.clicked).to.equal(true);
170
+ (0, chai_1.expect)(held()).to.equal(0);
171
+ }));
172
+ it('keeps the newest 1,000 for a session', () => {
173
+ for (let i = 0; i < 1001; i++)
174
+ omni.remember('sess-a', box(`omni_ocr_${i}`));
175
+ (0, chai_1.expect)(omni.getVirtualElement('omni_ocr_0', 'sess-a')).to.equal(undefined);
176
+ (0, chai_1.expect)(omni.getVirtualElement('omni_ocr_1', 'sess-a')).to.not.equal(undefined);
177
+ (0, chai_1.expect)(omni.getVirtualElement('omni_ocr_1000', 'sess-a')).to.not.equal(undefined);
178
+ });
179
+ it('keeps the newest 10,000 in all', () => {
180
+ for (let s = 0; s < 11; s++) {
181
+ for (let i = 0; i < 1000; i++)
182
+ omni.remember(`sess-${s}`, box(`omni_ocr_${s}_${i}`));
183
+ }
184
+ (0, chai_1.expect)(held()).to.equal(10000);
185
+ (0, chai_1.expect)(omni.getVirtualElement('omni_ocr_0_999', 'sess-0')).to.equal(undefined);
186
+ (0, chai_1.expect)(omni.getVirtualElement('omni_ocr_1_0', 'sess-1')).to.not.equal(undefined);
187
+ });
188
+ it('gives icon matches ids of their own, as text matches have', () => __awaiter(void 0, void 0, void 0, function* () {
189
+ sinon_1.default.stub(Date, 'now').returns(1700000000000);
190
+ sinon_1.default.stub(AIService_1.AI_SERVICE, 'visualFind').resolves({ x: 50, y: 50 });
191
+ const a = driverFor('sess-a');
192
+ const first = yield omni.findByIcon(a, 'the cart');
193
+ const second = yield omni.findByIcon(a, 'the cart');
194
+ (0, chai_1.expect)(first === null || first === void 0 ? void 0 : first.id).to.not.equal(second === null || second === void 0 ? void 0 : second.id);
195
+ }));
196
+ });
197
+ /**
198
+ * Everything a session leaves in this server's memory for its commands goes
199
+ * when the session ends, however it ends: its virtual elements, and its
200
+ * autowait settings (`xenon: setAutowaitProperties`). Those were meant to be
201
+ * cleared by the interceptor on `deleteSession`, but XenonPlugin answers
202
+ * `deleteSession` itself, so Appium never hands that command to the
203
+ * interceptor: they were kept for good.
204
+ */
205
+ describe("A session's virtual elements and autowait settings go when it ends", function () {
206
+ this.timeout(30000);
207
+ const scratch = (0, scratch_database_1.useScratchDatabase)();
208
+ let restoreRegs;
209
+ let omni;
210
+ const seed = (sessionId) => {
211
+ omni.remember(sessionId, box(`omni_ocr_${sessionId}`));
212
+ typedi_1.Container.get(AutowaitService_1.AutowaitService).setProps(sessionId, { timeoutMs: 1234 });
213
+ };
214
+ const expectForgotten = (sessionId) => {
215
+ (0, chai_1.expect)(omni.getVirtualElement(`omni_ocr_${sessionId}`, sessionId), 'element').to.equal(undefined);
216
+ (0, chai_1.expect)(typedi_1.Container.get(AutowaitService_1.AutowaitService).getProps(sessionId, Object.assign({}, IPluginArgs_1.DefaultPluginArgs)).timeoutMs, 'autowait timeout').to.equal(IPluginArgs_1.DefaultPluginArgs.autowait.timeoutMs);
217
+ };
218
+ beforeEach(() => {
219
+ restoreRegs = (0, container_registration_1.saveRegistrations)(OmniVisionService_1.OmniVisionService, AutowaitService_1.AutowaitService, PhoneNetworkRestore_1.PhoneNetworkRestore, SessionMetricsService_1.SessionMetricsService);
220
+ typedi_1.Container.set(OmniVisionService_1.OmniVisionService, new OmniVisionService_1.OmniVisionService());
221
+ typedi_1.Container.set(AutowaitService_1.AutowaitService, new AutowaitService_1.AutowaitService());
222
+ omni = typedi_1.Container.get(OmniVisionService_1.OmniVisionService);
223
+ typedi_1.Container.set(PhoneNetworkRestore_1.PhoneNetworkRestore, {
224
+ restoreSession: sinon_1.default.stub().resolves(),
225
+ restoreAll: sinon_1.default.stub().resolves(),
226
+ });
227
+ typedi_1.Container.set(SessionMetricsService_1.SessionMetricsService, { stop: sinon_1.default.stub().resolves() });
228
+ sinon_1.default.stub(DeviceService, 'releaseSessionDevices').resolves();
229
+ sinon_1.default.stub(event_manager_1.DASHBORD_EVENT_MANAGER, 'onSessionStopped').resolves();
230
+ });
231
+ afterEach(() => {
232
+ sinon_1.default.restore();
233
+ restoreRegs();
234
+ });
235
+ it('when the test deletes the session', () => __awaiter(this, void 0, void 0, function* () {
236
+ seed('s-deleted');
237
+ yield typedi_1.Container.get(SessionLifecycleService_1.SessionLifecycleService).deleteSession(() => __awaiter(this, void 0, void 0, function* () { return null; }), 's-deleted');
238
+ expectForgotten('s-deleted');
239
+ }));
240
+ it('when Appium ends the session itself (new-command timeout)', () => __awaiter(this, void 0, void 0, function* () {
241
+ seed('s-timeout');
242
+ const plugin = { pluginArgs: {}, xenonLog: { withSession: () => ({ info() { } }) } };
243
+ yield plugin_1.XenonPlugin.prototype.onUnexpectedShutdown.call(plugin, { sessionId: 's-timeout', caps: { udid: 'R5CT' } }, new Error('New command timeout'));
244
+ expectForgotten('s-timeout');
245
+ }));
246
+ it('when Appium ends the session on a node', () => __awaiter(this, void 0, void 0, function* () {
247
+ seed('s-node');
248
+ sinon_1.default.stub(NodeDevices_1.default.prototype, 'unblockDevice').resolves(undefined);
249
+ const plugin = { pluginArgs: { hub: 'http://hub:4723' } };
250
+ yield plugin_1.XenonPlugin.prototype.onUnexpectedShutdown.call(plugin, { sessionId: 's-node', caps: {} }, null);
251
+ expectForgotten('s-node');
252
+ }));
253
+ it("when Xenon's idle check releases the phone", () => __awaiter(this, void 0, void 0, function* () {
254
+ seed('s-idle');
255
+ const idle = {
256
+ udid: 'R5CT',
257
+ host: 'http://127.0.0.1:4723',
258
+ busy: true,
259
+ userBlocked: false,
260
+ session_id: 's-idle',
261
+ claimSessionId: 's-idle',
262
+ lastCmdExecutedAt: Date.now() - 10 * 60 * 1000,
263
+ };
264
+ sinon_1.default.stub(DeviceService, 'getAllDevices').resolves([idle]);
265
+ sinon_1.default.stub(ActiveLeases, 'activeLeasesByDevice').resolves(new Map());
266
+ sinon_1.default.stub(DeviceService, 'releaseSessionDevice').resolves(true);
267
+ yield (0, device_utils_1.releaseBlockedDevices)(60);
268
+ expectForgotten('s-idle');
269
+ }));
270
+ it('when a graceful shutdown drains the session', () => __awaiter(this, void 0, void 0, function* () {
271
+ seed('s-down');
272
+ yield typedi_1.Container.get(SessionLifecycleService_1.SessionLifecycleService).stopSessionForShutdown('s-down', 'shutdown');
273
+ expectForgotten('s-down');
274
+ }));
275
+ it("when the session's heartbeat goes stale", () => __awaiter(this, void 0, void 0, function* () {
276
+ seed('s-stale');
277
+ yield scratch.db.session.deleteMany({});
278
+ yield scratch.db.session.create({
279
+ data: {
280
+ id: 's-stale',
281
+ status: 'running',
282
+ desired_capabilities: '{}',
283
+ session_capabilities: '{}',
284
+ node_id: 'n',
285
+ has_live_video: false,
286
+ device_udid: 'R5CT',
287
+ device_platform: 'android',
288
+ device_version: '14',
289
+ last_heartbeat_at: new Date(Date.now() - 60 * 60 * 1000),
290
+ },
291
+ });
292
+ yield new OrphanSweeper_1.OrphanSweeper().sweep({ heartbeatIntervalMs: 1000 });
293
+ expectForgotten('s-stale');
294
+ }));
295
+ it("leaves other sessions' elements alone", () => __awaiter(this, void 0, void 0, function* () {
296
+ seed('s-ends');
297
+ seed('s-stays');
298
+ yield typedi_1.Container.get(SessionLifecycleService_1.SessionLifecycleService).deleteSession(() => __awaiter(this, void 0, void 0, function* () { return null; }), 's-ends');
299
+ (0, chai_1.expect)(omni.getVirtualElement('omni_ocr_s-stays', 's-stays')).to.not.equal(undefined);
300
+ }));
301
+ });
@@ -0,0 +1,72 @@
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 OmniVisionService_1 = require("../../src/services/omni-vision/OmniVisionService");
19
+ const AIService_1 = require("../../src/services/AIService");
20
+ /**
21
+ * `xenon: analyzeScreen` (and device control's Omni-Scan) sent the screenshot's
22
+ * base64 where a file path was expected. The AI step found no such file and
23
+ * asked the provider without an image, under a "why did this test fail"
24
+ * prompt, so `ai_insights` was generic text about no screen at all.
25
+ *
26
+ * Now the screenshot goes to the provider with a prompt about the screen, and
27
+ * when there is no AI answer `ai_insights_error` says why.
28
+ */
29
+ describe('xenon: analyzeScreen', () => {
30
+ const SCREEN = 'c2NyZWVuc2hvdA==';
31
+ let service;
32
+ const driver = { sessionId: 'session-1', getScreenshot: () => __awaiter(void 0, void 0, void 0, function* () { return SCREEN; }) };
33
+ beforeEach(() => {
34
+ service = new OmniVisionService_1.OmniVisionService();
35
+ sinon_1.default.stub(service, 'performOcr').resolves({
36
+ text: 'Sign in',
37
+ words: [{ text: 'Sign', confidence: 95, bbox: { x0: 10, y0: 10, x1: 50, y1: 30 } }],
38
+ });
39
+ sinon_1.default.stub(AIService_1.AI_SERVICE, 'initializeProvider');
40
+ });
41
+ afterEach(() => sinon_1.default.restore());
42
+ it('sends the screenshot to the AI provider and asks about the screen', () => __awaiter(void 0, void 0, void 0, function* () {
43
+ sinon_1.default.stub(AIService_1.AI_SERVICE, 'provider').value({ analyze: () => __awaiter(void 0, void 0, void 0, function* () { return ''; }) });
44
+ const call = sinon_1.default
45
+ .stub(AIService_1.AI_SERVICE, 'callProvider')
46
+ .resolves('A sign-in screen with an email field.');
47
+ const analysis = yield service.analyzeScreen(driver);
48
+ (0, chai_1.expect)(call.calledOnce).to.equal(true);
49
+ const [prompt, image] = call.firstCall.args;
50
+ (0, chai_1.expect)(image).to.equal(SCREEN);
51
+ (0, chai_1.expect)(prompt).to.match(/screen/i);
52
+ (0, chai_1.expect)(prompt).to.not.match(/root cause|failed/i);
53
+ (0, chai_1.expect)(analysis.ai_insights).to.equal('A sign-in screen with an email field.');
54
+ (0, chai_1.expect)(analysis).to.not.have.property('ai_insights_error');
55
+ (0, chai_1.expect)(analysis.ocr.text).to.equal('Sign in');
56
+ }));
57
+ it('says why there are no AI insights when no provider is configured', () => __awaiter(void 0, void 0, void 0, function* () {
58
+ sinon_1.default.stub(AIService_1.AI_SERVICE, 'provider').value(null);
59
+ const analysis = yield service.analyzeScreen(driver);
60
+ (0, chai_1.expect)(analysis.ai_insights).to.equal(null);
61
+ (0, chai_1.expect)(analysis.ai_insights_error).to.match(/No AI provider is configured/);
62
+ (0, chai_1.expect)(analysis.ocr.text).to.equal('Sign in');
63
+ }));
64
+ it('says why there are no AI insights when the provider fails', () => __awaiter(void 0, void 0, void 0, function* () {
65
+ sinon_1.default.stub(AIService_1.AI_SERVICE, 'provider').value({ analyze: () => __awaiter(void 0, void 0, void 0, function* () { return ''; }) });
66
+ sinon_1.default.stub(AIService_1.AI_SERVICE, 'callProvider').rejects(new Error('429 quota exceeded'));
67
+ const analysis = yield service.analyzeScreen(driver);
68
+ (0, chai_1.expect)(analysis.ai_insights).to.equal(null);
69
+ (0, chai_1.expect)(analysis.ai_insights_error).to.match(/429 quota exceeded/);
70
+ (0, chai_1.expect)(analysis.ocr.text).to.equal('Sign in');
71
+ }));
72
+ });
@@ -0,0 +1,159 @@
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 sharp_1 = __importDefault(require("sharp"));
19
+ const OmniVisionService_1 = require("../../src/services/omni-vision/OmniVisionService");
20
+ const AIService_1 = require("../../src/services/AIService");
21
+ const HealingOrchestrator_1 = require("../../src/services/healing/HealingOrchestrator");
22
+ const types_1 = require("../../src/services/healing/types");
23
+ const screenScale_1 = require("../../src/services/omni-vision/screenScale");
24
+ /**
25
+ * OCR and the AI find a target in screenshot pixels. An iPhone's screenshot is
26
+ * 2x or 3x its screen in points, and XCUITest's W3C actions and element rects
27
+ * are in points, so a tap at the pixel position landed 2-3 times too far right
28
+ * and down. Nothing converted: not smartTap/visualTap, not the virtual
29
+ * elements the -custom:ai-* locators return, not the healing tiers' tap.
30
+ *
31
+ * Positions found in a screenshot are now converted to the driver's
32
+ * coordinates on iOS (screenshot size over window size). Android's W3C
33
+ * coordinates are the screenshot's pixels, so nothing changes there.
34
+ */
35
+ describe('Omni-Vision taps in points on iOS', () => {
36
+ // A 3x screen: 30 x 60 pixels for a 10 x 20 point window.
37
+ let png3x;
38
+ before(() => __awaiter(void 0, void 0, void 0, function* () {
39
+ const image = yield (0, sharp_1.default)({
40
+ create: { width: 30, height: 60, channels: 3, background: { r: 255, g: 255, b: 255 } },
41
+ })
42
+ .png()
43
+ .toBuffer();
44
+ png3x = image.toString('base64');
45
+ }));
46
+ // "OK" at pixels 12..18 x 30..36: its middle is (15, 33) px, (5, 11) pt.
47
+ const OK_WORD = { text: 'OK', confidence: 90, bbox: { x0: 12, y0: 30, x1: 18, y1: 36 } };
48
+ let service;
49
+ let ios;
50
+ let android;
51
+ function driverFor(platformName) {
52
+ return {
53
+ sessionId: `session-${platformName}`,
54
+ caps: { platformName },
55
+ getScreenshot: sinon_1.default.stub().resolves(png3x),
56
+ getWindowRect: sinon_1.default.stub().resolves({ x: 0, y: 0, width: 10, height: 20 }),
57
+ performActions: sinon_1.default.stub().resolves(),
58
+ releaseActions: sinon_1.default.stub().resolves(),
59
+ };
60
+ }
61
+ beforeEach(() => {
62
+ service = new OmniVisionService_1.OmniVisionService();
63
+ sinon_1.default.stub(service, 'performOcr').resolves({ text: 'OK', words: [OK_WORD] });
64
+ ios = driverFor('iOS');
65
+ android = driverFor('Android');
66
+ });
67
+ afterEach(() => sinon_1.default.restore());
68
+ const tappedAt = (driver) => {
69
+ const move = driver.performActions.firstCall.args[0][0].actions[0];
70
+ return { x: move.x, y: move.y };
71
+ };
72
+ it('smartTap by text taps in points on iOS', () => __awaiter(void 0, void 0, void 0, function* () {
73
+ const result = yield service.omniClickByText(ios, { text: 'OK' });
74
+ (0, chai_1.expect)(result.clicked, result.message).to.equal(true);
75
+ (0, chai_1.expect)(tappedAt(ios)).to.deep.equal({ x: 5, y: 11 });
76
+ (0, chai_1.expect)(result.target).to.deep.include({
77
+ x: 5,
78
+ y: 11,
79
+ rect: { x: 4, y: 10, width: 2, height: 2 },
80
+ });
81
+ }));
82
+ it('smartTap by text taps in pixels on Android, without asking for the window', () => __awaiter(void 0, void 0, void 0, function* () {
83
+ const result = yield service.omniClickByText(android, { text: 'OK' });
84
+ (0, chai_1.expect)(result.clicked, result.message).to.equal(true);
85
+ (0, chai_1.expect)(tappedAt(android)).to.deep.equal({ x: 15, y: 33 });
86
+ (0, chai_1.expect)(android.getWindowRect.called).to.equal(false);
87
+ }));
88
+ it('visualTap taps in points on iOS', () => __awaiter(void 0, void 0, void 0, function* () {
89
+ sinon_1.default.stub(AIService_1.AI_SERVICE, 'visualFind').resolves({ x: 15, y: 33 });
90
+ const result = yield service.omniClickByIcon(ios, { icon: 'the gear icon' });
91
+ (0, chai_1.expect)(result.clicked, result.message).to.equal(true);
92
+ (0, chai_1.expect)(tappedAt(ios)).to.deep.equal({ x: 5, y: 11 });
93
+ }));
94
+ it('gives the -custom:ai-text locator an element in points on iOS', () => __awaiter(void 0, void 0, void 0, function* () {
95
+ const [found] = yield service.findByText(ios, 'OK');
96
+ (0, chai_1.expect)(found.rect).to.deep.equal({ x: 4, y: 10, width: 2, height: 2 });
97
+ }));
98
+ it("doesn't tap on iOS when the window size can't be read", () => __awaiter(void 0, void 0, void 0, function* () {
99
+ ios.getWindowRect.rejects(new Error('WDA is not running'));
100
+ const err = yield service.omniClickByText(ios, { text: 'OK' }).then(() => null, (e) => e);
101
+ (0, chai_1.expect)(err === null || err === void 0 ? void 0 : err.message).to.match(/WDA is not running/);
102
+ (0, chai_1.expect)(ios.performActions.called).to.equal(false);
103
+ }));
104
+ it('reads the window from getWindowSize when there is no getWindowRect', () => __awaiter(void 0, void 0, void 0, function* () {
105
+ const driver = {
106
+ caps: { platformName: 'iOS' },
107
+ getWindowSize: () => __awaiter(void 0, void 0, void 0, function* () { return ({ width: 15, height: 30 }); }),
108
+ };
109
+ (0, chai_1.expect)(yield (0, screenScale_1.screenScaleOf)(driver, png3x)).to.deep.equal({ x: 2, y: 2 });
110
+ }));
111
+ it("keeps pixels for a driver that isn't iOS, or can't be placed", () => __awaiter(void 0, void 0, void 0, function* () {
112
+ (0, chai_1.expect)(yield (0, screenScale_1.screenScaleOf)({}, 'not an image')).to.deep.equal({ x: 1, y: 1 });
113
+ (0, chai_1.expect)(yield (0, screenScale_1.screenScaleOf)({ caps: { platformName: 'Android' } }, 'x')).to.deep.equal({
114
+ x: 1,
115
+ y: 1,
116
+ });
117
+ }));
118
+ it('converts the OCR and Visual AI healing tiers’ rect to points on iOS', () => __awaiter(void 0, void 0, void 0, function* () {
119
+ const orchestrator = new HealingOrchestrator_1.HealingOrchestrator({
120
+ getSignature: () => __awaiter(void 0, void 0, void 0, function* () { return null; }),
121
+ saveSignature: () => __awaiter(void 0, void 0, void 0, function* () { return undefined; }),
122
+ });
123
+ const ocr = orchestrator.providers.find((p) => p.tier === types_1.HealingTier.TIER_3_LOCAL_OCR);
124
+ sinon_1.default.stub(ocr, 'heal').resolves({
125
+ id: 'healed_ocr_1',
126
+ tier: types_1.HealingTier.TIER_3_LOCAL_OCR,
127
+ confidence: 0.9,
128
+ originalSelector: '//*[@label="OK"]',
129
+ recommendedSelector: 'ocr:text="OK"',
130
+ rect: { x: 12, y: 30, width: 6, height: 6 },
131
+ });
132
+ ios.getPageSource = sinon_1.default.stub().resolves('<AppiumAUT/>');
133
+ const healed = yield orchestrator.attemptHealing('s1', ios, 'xpath', '//*[@label="OK"]', [3]);
134
+ (0, chai_1.expect)(healed === null || healed === void 0 ? void 0 : healed.rect).to.deep.equal({ x: 4, y: 10, width: 2, height: 2 });
135
+ }));
136
+ it("drops a healed virtual element whose position on an iPhone can't be worked out", () => __awaiter(void 0, void 0, void 0, function* () {
137
+ const orchestrator = new HealingOrchestrator_1.HealingOrchestrator({
138
+ getSignature: () => __awaiter(void 0, void 0, void 0, function* () { return null; }),
139
+ saveSignature: () => __awaiter(void 0, void 0, void 0, function* () { return undefined; }),
140
+ });
141
+ const ocr = orchestrator.providers.find((p) => p.tier === types_1.HealingTier.TIER_3_LOCAL_OCR);
142
+ const found = {
143
+ tier: types_1.HealingTier.TIER_3_LOCAL_OCR,
144
+ confidence: 0.9,
145
+ originalSelector: '//*[@label="OK"]',
146
+ recommendedSelector: 'ocr:text="OK"',
147
+ rect: { x: 12, y: 30, width: 6, height: 6 },
148
+ };
149
+ const heal = sinon_1.default.stub(ocr, 'heal').resolves(Object.assign(Object.assign({}, found), { id: 'healed_ocr_1' }));
150
+ ios.getPageSource = sinon_1.default.stub().resolves('<AppiumAUT/>');
151
+ ios.getWindowRect.rejects(new Error('WDA is not running'));
152
+ (0, chai_1.expect)(yield orchestrator.attemptHealing('s1', ios, 'xpath', '//x', [3])).to.equal(null);
153
+ // A real element the tier resolved is kept, without the rect.
154
+ heal.resolves(Object.assign(Object.assign({}, found), { id: 'real-element-1' }));
155
+ const healed = yield orchestrator.attemptHealing('s1', ios, 'xpath', '//x', [3]);
156
+ (0, chai_1.expect)(healed === null || healed === void 0 ? void 0 : healed.id).to.equal('real-element-1');
157
+ (0, chai_1.expect)(healed === null || healed === void 0 ? void 0 : healed.rect).to.equal(undefined);
158
+ }));
159
+ });