@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
@@ -20,6 +20,9 @@ const xpath_1 = require("xpath");
20
20
  const typedi_1 = require("typedi");
21
21
  const logger_1 = __importDefault(require("../../logger"));
22
22
  const HealedLocatorGenerator_1 = require("./HealedLocatorGenerator");
23
+ const fingerprintIdentity_1 = require("./fingerprintIdentity");
24
+ /** What an element says in words, for telling it apart once its identifier differs. */
25
+ const TEXTS = ['text', 'label', 'content-desc'];
23
26
  class FuzzyXmlHealingProvider {
24
27
  constructor(etalonService) {
25
28
  this.etalonService = etalonService;
@@ -41,6 +44,12 @@ class FuzzyXmlHealingProvider {
41
44
  if (this.etalonService) {
42
45
  etalon = yield this.etalonService.getSignature(context.selector);
43
46
  }
47
+ // A fingerprint with only a rect and a type (every learnt one through
48
+ // 2.14) would heal to whatever element of that type is at that spot.
49
+ if (etalon && !(0, fingerprintIdentity_1.hasIdentity)(etalon.attributes)) {
50
+ this.logger.info('The baseline signature says nothing about which element it is; matching without it.');
51
+ etalon = null;
52
+ }
44
53
  const keywords = this.extractKeywords(context.selector);
45
54
  if (keywords.length === 0 && !etalon)
46
55
  return null;
@@ -135,6 +144,9 @@ class FuzzyXmlHealingProvider {
135
144
  if (etalonTag !== nodeTag && etalonTag !== 'xcuielementtypeany' && etalonTag !== 'unknown') {
136
145
  return 0; // Immediate rejection — wrong element type
137
146
  }
147
+ // Position can't heal to an element whose identifier says it's another one.
148
+ if (this.contradictsIdentifier(node, etalon))
149
+ return 0;
138
150
  // Tag matches perfectly
139
151
  totalWeight += 1.0;
140
152
  totalScore += 1.0;
@@ -260,6 +272,45 @@ class FuzzyXmlHealingProvider {
260
272
  }
261
273
  return finalScore;
262
274
  }
275
+ /**
276
+ * Whether `node` is, by its identifier, another element than the
277
+ * fingerprint's: the fingerprint has an identifier (an Android resource-id,
278
+ * compared without its package, or an iOS accessibility identifier), `node`
279
+ * has another one (sameIdentifier), and no text of it (text, label,
280
+ * content-desc) reads the same as the fingerprint's (sameText). Its
281
+ * position then counts for nothing.
282
+ *
283
+ * A renamed id that kept its text or description still heals, and so does
284
+ * a renamed text that kept its id. An element without an identifier, where
285
+ * the fingerprint has one, is judged by its text alone: an Android element
286
+ * with no resource-id, or an iOS one, whose name is then its label. Only
287
+ * where neither has one does position decide, as through 2.14.
288
+ * What this stops is the button that took the old one's place: "Cancel
289
+ * order" (cancel) where "Pay now" (pay) was, "Log out" where "Log in" was.
290
+ */
291
+ contradictsIdentifier(node, etalon) {
292
+ const identifier = (0, fingerprintIdentity_1.stableIdentifierOf)(etalon.attributes);
293
+ if (!identifier)
294
+ return false;
295
+ // Another identifier, or none where the fingerprint has one, and the
296
+ // text decides. iOS gives an element without one its label as name.
297
+ const theirs = this.getAttrValue(node, identifier.attribute);
298
+ if ((0, fingerprintIdentity_1.meaningful)(theirs)) {
299
+ const [mine, other] = identifier.attribute === 'resource-id'
300
+ ? [(0, fingerprintIdentity_1.resourceIdName)(identifier.value), (0, fingerprintIdentity_1.resourceIdName)(theirs)]
301
+ : [identifier.value, theirs];
302
+ if ((0, fingerprintIdentity_1.sameIdentifier)(mine, other))
303
+ return false;
304
+ }
305
+ const texts = TEXTS.map((name) => etalon.attributes[name]).filter(fingerprintIdentity_1.meaningful);
306
+ if (texts.length === 0)
307
+ return true;
308
+ const nodeTexts = [
309
+ node.textContent,
310
+ ...TEXTS.map((name) => this.getAttrValue(node, name)),
311
+ ].filter(fingerprintIdentity_1.meaningful);
312
+ return !nodeTexts.some((t) => texts.some((w) => (0, fingerprintIdentity_1.sameText)(t, w)));
313
+ }
263
314
  getAttrValue(node, name) {
264
315
  if (typeof node.getAttribute === 'function') {
265
316
  const val = node.getAttribute(name);
@@ -37,7 +37,9 @@ let HealEtalonService = class HealEtalonService {
37
37
  return __awaiter(this, void 0, void 0, function* () {
38
38
  try {
39
39
  const attributes = {};
40
- // Extract identifying attributes
40
+ // Extract identifying attributes. Not `value`: on a text field it is
41
+ // what the test typed, an email or a name, which says nothing about
42
+ // which field it is.
41
43
  const nodeAttrs = node.attributes || [];
42
44
  const anchorNames = [
43
45
  'content-desc',
@@ -48,7 +50,6 @@ let HealEtalonService = class HealEtalonService {
48
50
  'hint',
49
51
  'label',
50
52
  'accessibility-id',
51
- 'value',
52
53
  'x',
53
54
  'y',
54
55
  'width',
@@ -1,43 +1,10 @@
1
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
2
  var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
19
3
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
20
4
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
21
5
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
22
6
  return c > 3 && r && Object.defineProperty(target, key, r), r;
23
7
  };
24
- var __importStar = (this && this.__importStar) || (function () {
25
- var ownKeys = function(o) {
26
- ownKeys = Object.getOwnPropertyNames || function (o) {
27
- var ar = [];
28
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
29
- return ar;
30
- };
31
- return ownKeys(o);
32
- };
33
- return function (mod) {
34
- if (mod && mod.__esModule) return mod;
35
- var result = {};
36
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
37
- __setModuleDefault(result, mod);
38
- return result;
39
- };
40
- })();
41
8
  var __metadata = (this && this.__metadata) || function (k, v) {
42
9
  if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
43
10
  };
@@ -54,7 +21,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
54
21
  return (mod && mod.__esModule) ? mod : { "default": mod };
55
22
  };
56
23
  Object.defineProperty(exports, "__esModule", { value: true });
57
- exports.HealingOrchestrator = void 0;
24
+ exports.HealingOrchestrator = exports.LOCAL_HEALING_TIERS = void 0;
58
25
  exports.filterProvidersByTier = filterProvidersByTier;
59
26
  exports.coerceHealingTiersCap = coerceHealingTiersCap;
60
27
  exports.healingTiersFromCaps = healingTiersFromCaps;
@@ -67,9 +34,11 @@ const VisualAiHealingProvider_1 = require("./VisualAiHealingProvider");
67
34
  const LlmHealingProvider_1 = require("./LlmHealingProvider");
68
35
  const HealEtalonService_1 = require("./HealEtalonService");
69
36
  const ResilioTreeHealingProvider_1 = require("./ResilioTreeHealingProvider");
37
+ const resilioPath_1 = require("./resilioPath");
70
38
  const HealingMetrics_1 = require("./HealingMetrics");
71
39
  const attributes_1 = require("../telemetry/attributes");
72
40
  const xenonOptions_1 = require("../session/xenonOptions");
41
+ const screenScale_1 = require("../omni-vision/screenScale");
73
42
  // §2.7 healing-tier capability gate. The tier numbering here is the
74
43
  // externally-facing capability contract (xe:options.healingTiers),
75
44
  // which is array-position based: providers[i] is tier i+1 (1=Resilio,
@@ -85,24 +54,46 @@ function filterProvidersByTier(providers, allowedTiers) {
85
54
  const allowed = new Set(allowedTiers);
86
55
  return providers.filter((_, index) => allowed.has(index + 1));
87
56
  }
57
+ // The tiers that run on this server: Resilio, Fuzzy XML and OCR. Visual AI (4)
58
+ // and the LLM (5) send the screenshot and the page source to the AI provider.
59
+ exports.LOCAL_HEALING_TIERS = [1, 2, 3];
60
+ const isHealingTierList = (raw) => Array.isArray(raw) && raw.every((t) => Number.isInteger(t) && t >= 1 && t <= 5);
88
61
  // Coerce the raw xe:options.healingTiers capability value into the
89
- // allowedTiers argument for attemptHealing. Fail-open contract (Phase 2a
90
- // review fix): a malformed cap must RUN ALL tiers, never silently disable
91
- // healing. Pre-fix, an all-non-numeric array (e.g. ["1","2"]) filtered to []
92
- // which filterProvidersByTier turned into "no providers" — healing silently
93
- // off. Now any coercion that yields no numeric tiers (non-array, all-non-
94
- // numeric, or an explicitly-empty []) returns undefined → run all tiers.
95
- // The cap is meant to restrict healing, not disable it, so an explicit []
96
- // opting out of healing entirely is deliberately NOT supported.
62
+ // allowedTiers argument for attemptHealing. The value is a privacy control (a
63
+ // session leaves out 4 and 5 to keep its screen away from the AI provider), so
64
+ // one that can't be read fails closed for the AI tiers and open for the rest:
65
+ // - not set (or null): every tier (undefined);
66
+ // - a list of tier numbers 1 to 5: exactly those, and [] none;
67
+ // - anything else ("1,2", ["1","2"], [1, 6], ...): LOCAL_HEALING_TIERS.
68
+ // Through 2.14 anything else, and [], ran every tier, the AI ones included.
97
69
  function coerceHealingTiersCap(raw) {
98
- const nums = Array.isArray(raw) ? raw.filter((t) => typeof t === 'number') : undefined;
99
- return nums && nums.length === 0 ? undefined : nums;
70
+ if (raw === undefined || raw === null)
71
+ return undefined;
72
+ return isHealingTierList(raw) ? [...raw] : [...exports.LOCAL_HEALING_TIERS];
100
73
  }
101
74
  // The allowedTiers a session asked for, read from its capabilities (one flat
102
- // map, e.g. the session response): xe:options.healingTiers, or the
103
- // xenon:options alias's, coerced as above.
75
+ // map: the session's driver's caps): xe:options.healingTiers, or the
76
+ // xenon:options alias's, coerced as above. A namespace that isn't an object
77
+ // can't be read either: xenonOptionsIn skips it, which would run every tier.
78
+ // `unreadable` says what couldn't be read, for the log.
104
79
  function healingTiersFromCaps(caps) {
105
- return coerceHealingTiersCap((0, xenonOptions_1.xenonOptionsIn)(caps).healingTiers);
80
+ for (const namespace of xenonOptions_1.OPTION_NAMESPACES) {
81
+ const bag = (0, xenonOptions_1.isOptionsObject)(caps) ? caps[namespace] : undefined;
82
+ if (bag !== undefined && bag !== null && !(0, xenonOptions_1.isOptionsObject)(bag)) {
83
+ return {
84
+ tiers: [...exports.LOCAL_HEALING_TIERS],
85
+ unreadable: `${namespace} is ${JSON.stringify(bag)}, not an object`,
86
+ };
87
+ }
88
+ }
89
+ const raw = (0, xenonOptions_1.xenonOptionsIn)(caps).healingTiers;
90
+ const tiers = coerceHealingTiersCap(raw);
91
+ return tiers === undefined || isHealingTierList(raw)
92
+ ? { tiers }
93
+ : {
94
+ tiers,
95
+ unreadable: `healingTiers is ${JSON.stringify(raw)}, not a list of tier numbers from 1 to 5`,
96
+ };
106
97
  }
107
98
  let HealingOrchestrator = class HealingOrchestrator {
108
99
  constructor(etalonService) {
@@ -119,7 +110,15 @@ let HealingOrchestrator = class HealingOrchestrator {
119
110
  }
120
111
  attemptHealing(sessionId, driver, strategy, selector, allowedTiers) {
121
112
  return __awaiter(this, void 0, void 0, function* () {
122
- var _a, _b;
113
+ var _a, _b, _c;
114
+ // §2.7: when the session's xe:options.healingTiers capability is set,
115
+ // restrict dispatch to the allowed tier indices; absent -> unchanged.
116
+ const activeProviders = filterProvidersByTier(this.providers, allowedTiers);
117
+ if (activeProviders.length === 0) {
118
+ // Nothing to run, so nothing is collected either: no screenshot taken.
119
+ this.logger.info(`Session ${sessionId} allows no self-healing tier; ${strategy}=${selector} is not healed.`);
120
+ return null;
121
+ }
123
122
  this.logger.info(`🚨 Self-Healing triggered for session ${sessionId}. Broken locator: ${strategy}=${selector}`);
124
123
  // Span wraps the whole attempt. Original selector text is intentionally
125
124
  // kept off the span — high cardinality (long XPath strings) would explode
@@ -150,15 +149,15 @@ let HealingOrchestrator = class HealingOrchestrator {
150
149
  return null;
151
150
  }
152
151
  // Tiered Execution: Try providers in order of cost/complexity.
153
- // §2.7: when the session's xe:options.healingTiers capability is set,
154
- // restrict dispatch to the allowed tier indices; absent -> unchanged.
155
- const activeProviders = filterProvidersByTier(this.providers, allowedTiers);
156
152
  for (const provider of activeProviders) {
157
153
  const tierStart = Date.now();
158
154
  span.addEvent('tier_started', { tier: provider.name });
159
155
  try {
160
156
  this.logger.info(`Attempting Tier ${provider.tier}: ${provider.name}...`);
161
- const result = yield provider.heal(context);
157
+ let result = yield provider.heal(context);
158
+ if (result === null || result === void 0 ? void 0 : result.rect) {
159
+ result = yield this.rectInDriverCoordinates(result, result.rect, context, provider);
160
+ }
162
161
  HealingMetrics_1.HEALING_METRICS.record(provider.tier, provider.name, result ? 'success' : 'failure', Date.now() - tierStart);
163
162
  if (result) {
164
163
  this.logger.info(`✨ Provider ${provider.name} found a match! Confidence: ${(result.confidence * 100).toFixed(0)}%`);
@@ -199,23 +198,11 @@ let HealingOrchestrator = class HealingOrchestrator {
199
198
  else if (result.confidence > 0.7 && result.node) {
200
199
  try {
201
200
  this.logger.info(`🧠 Learning from healing success: updating etalon for ${selector}`);
202
- // Recalculate path for ResilioTree if node is available
203
- let learnedPath = null;
204
- if (result.node) {
205
- try {
206
- const { Path } = yield Promise.resolve().then(() => __importStar(require('resiliotree')));
207
- const pathNodes = [];
208
- let curr = result.node;
209
- while (curr) {
210
- pathNodes.unshift(curr);
211
- curr = curr.parent || curr.parentNode;
212
- }
213
- learnedPath = new Path(pathNodes).toJSON();
214
- }
215
- catch (_c) {
216
- // resiliotree import is best-effort; learnedPath stays null
217
- }
218
- }
201
+ // The healed element's path through the page source, for the
202
+ // Resilio tier. Through 2.14 this built it from the healed node
203
+ // with resiliotree, which an xmldom element (every node a tier
204
+ // returns) can't be, so it was always null.
205
+ const learnedPath = (0, resilioPath_1.resilioPathOf)(result.node, (_a = context.pageSource) !== null && _a !== void 0 ? _a : '');
219
206
  yield this.etalonService.saveSignature(strategy, selector, result.node, learnedPath);
220
207
  }
221
208
  catch (learnErr) {
@@ -231,7 +218,7 @@ let HealingOrchestrator = class HealingOrchestrator {
231
218
  span.setAttributes({
232
219
  [attributes_1.ATTR.HEALING_TIER]: provider.name,
233
220
  [attributes_1.ATTR.HEALING_CONFIDENCE]: result.confidence,
234
- [attributes_1.ATTR.HEALING_RESULT_STRATEGY]: (_a = result.recommendedStrategy) !== null && _a !== void 0 ? _a : 'xpath',
221
+ [attributes_1.ATTR.HEALING_RESULT_STRATEGY]: (_b = result.recommendedStrategy) !== null && _b !== void 0 ? _b : 'xpath',
235
222
  [attributes_1.ATTR.HEALING_DURATION_MS]: Date.now() - attemptStart,
236
223
  });
237
224
  span.setStatus({ code: api_1.SpanStatusCode.OK });
@@ -249,7 +236,7 @@ let HealingOrchestrator = class HealingOrchestrator {
249
236
  // tier can plausibly succeed (e.g. missing prerequisites that all
250
237
  // share). Saves an expensive LLM round-trip when upstream context
251
238
  // collection failed.
252
- if ((_b = provider.shouldSkipRemaining) === null || _b === void 0 ? void 0 : _b.call(provider, context)) {
239
+ if ((_c = provider.shouldSkipRemaining) === null || _c === void 0 ? void 0 : _c.call(provider, context)) {
253
240
  this.logger.warn(`Tier ${provider.tier} (${provider.name}) advised skipping remaining tiers for selector=${selector}`);
254
241
  HealingMetrics_1.HEALING_METRICS.recordSkippedRemaining(provider.tier, provider.name);
255
242
  span.addEvent('tier_skipped_remaining', { tier: provider.name });
@@ -267,6 +254,33 @@ let HealingOrchestrator = class HealingOrchestrator {
267
254
  return null;
268
255
  });
269
256
  }
257
+ /**
258
+ * The OCR and Visual AI tiers find the element in the screenshot, so their
259
+ * `rect` is in its pixels. The interceptor returns a virtual element there,
260
+ * which a click taps in the driver's coordinates (points on iOS), so it is
261
+ * converted here. If that can't be worked out on iOS, a virtual element
262
+ * (only a position) is no use and the tier counts as failed; a real element
263
+ * the tier resolved keeps its id and loses only the rect.
264
+ */
265
+ rectInDriverCoordinates(result, rect, context, provider) {
266
+ return __awaiter(this, void 0, void 0, function* () {
267
+ var _a, _b;
268
+ try {
269
+ const scale = yield (0, screenScale_1.screenScaleOf)(context.driver, (_a = context.screenshotBase64) !== null && _a !== void 0 ? _a : '');
270
+ return Object.assign(Object.assign({}, result), { rect: (0, screenScale_1.toDriverRect)(rect, scale) });
271
+ }
272
+ catch (err) {
273
+ const reason = (_b = err === null || err === void 0 ? void 0 : err.message) !== null && _b !== void 0 ? _b : err;
274
+ if (!result.id.startsWith('healed_')) {
275
+ this.logger.warn(`${provider.name}: the element's position is unknown (${reason}).`);
276
+ return Object.assign(Object.assign({}, result), { rect: undefined });
277
+ }
278
+ this.logger.warn(`${provider.name} found the element in the screenshot, but not where it is on the ` +
279
+ `screen (${reason}); trying the next tier.`);
280
+ return null;
281
+ }
282
+ });
283
+ }
270
284
  };
271
285
  exports.HealingOrchestrator = HealingOrchestrator;
272
286
  exports.HealingOrchestrator = HealingOrchestrator = __decorate([
@@ -14,8 +14,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
15
  exports.OcrHealingProvider = void 0;
16
16
  const types_1 = require("./types");
17
- const tesseract_js_1 = __importDefault(require("tesseract.js"));
17
+ const typedi_1 = require("typedi");
18
18
  const logger_1 = __importDefault(require("../../logger"));
19
+ const OmniVisionService_1 = require("../omni-vision/OmniVisionService");
19
20
  class OcrHealingProvider {
20
21
  constructor() {
21
22
  this.name = 'OCR Text Provider';
@@ -41,71 +42,47 @@ class OcrHealingProvider {
41
42
  if (!soughtText)
42
43
  return null;
43
44
  this.logger.info(`Attempting OCR search for text: "${soughtText}"`);
44
- // Run OCR on the screenshot
45
- const buffer = Buffer.from(context.screenshotBase64, 'base64');
46
- const result = yield tesseract_js_1.default.recognize(buffer, 'eng');
47
- const { words } = result.data;
48
- // Look for the best word match
49
- const bestWord = words
50
- ? words.find((w) => w.text.toLowerCase().includes(soughtText.toLowerCase()) ||
51
- soughtText.toLowerCase().includes(w.text.toLowerCase()))
52
- : null;
53
- if (bestWord) {
54
- this.logger.info(`✅ OCR found text "${bestWord.text}" at ${JSON.stringify(bestWord.bbox)}`);
55
- // Convert bbox to center coordinates
56
- const x = Math.round((bestWord.bbox.x0 + bestWord.bbox.x1) / 2);
57
- const y = Math.round((bestWord.bbox.y0 + bestWord.bbox.y1) / 2);
58
- // Try to get the REAL element at these coordinates by tapping
59
- try {
60
- // Use W3C Actions to tap at the OCR-detected coordinates, then find the element at that position
61
- const element = yield context.driver
62
- .findElement('-ios predicate string', `label CONTAINS[c] "${bestWord.text.replace(/"/g, '\\"')}"`)
63
- .catch(() => null);
64
- if (element) {
65
- const elementId = element.ELEMENT || element['element-6066-11e4-a52e-4f735466cecf'];
66
- if (elementId) {
67
- this.logger.info('🎯 OCR resolved to real element via predicate search');
68
- return {
69
- id: elementId,
70
- tier: this.tier,
71
- confidence: bestWord.confidence / 100,
72
- originalSelector: context.selector,
73
- originalStrategy: context.strategy,
74
- recommendedSelector: `ocr:text="${bestWord.text}"`,
75
- recommendedStrategy: 'xenon:visual',
76
- message: `Found text "${bestWord.text}" via local OCR (${bestWord.confidence.toFixed(0)}% confidence)`,
77
- rect: {
78
- x: bestWord.bbox.x0,
79
- y: bestWord.bbox.y0,
80
- width: bestWord.bbox.x1 - bestWord.bbox.x0,
81
- height: bestWord.bbox.y1 - bestWord.bbox.y0,
82
- },
83
- };
84
- }
85
- }
86
- }
87
- catch (predErr) {
88
- this.logger.debug(`Predicate search failed: ${predErr.message}`);
45
+ // Read the screenshot as Omni-Vision's `-custom:ai-text` does: the text
46
+ // may cover several neighbouring words ("Sign in"), and a word that is
47
+ // only part of it ("Log" for "Login") is no match. The first match in
48
+ // reading order, as `findElement` with that locator returns.
49
+ const [match] = yield typedi_1.Container.get(OmniVisionService_1.OmniVisionService).findTextInScreenshot(context.screenshotBase64, soughtText);
50
+ if (!match)
51
+ return null;
52
+ this.logger.info(`✅ OCR found text "${match.text}" at ${JSON.stringify({ x0: match.x0, y0: match.y0, x1: match.x1, y1: match.y1 })}`);
53
+ const found = {
54
+ tier: this.tier,
55
+ confidence: match.confidence / 100,
56
+ originalSelector: context.selector,
57
+ originalStrategy: context.strategy,
58
+ recommendedSelector: `ocr:text="${match.text}"`,
59
+ recommendedStrategy: 'xenon:visual',
60
+ message: `Found text "${match.text}" via local OCR (${match.confidence.toFixed(0)}% confidence)`,
61
+ text: match.text,
62
+ rect: {
63
+ x: match.x0,
64
+ y: match.y0,
65
+ width: match.x1 - match.x0,
66
+ height: match.y1 - match.y0,
67
+ },
68
+ };
69
+ // On an iPhone, the element whose label holds the text, when there is one.
70
+ try {
71
+ const element = yield context.driver
72
+ .findElement('-ios predicate string', `label CONTAINS[c] "${match.text.replace(/"/g, '\\"')}"`)
73
+ .catch(() => null);
74
+ const elementId = (element === null || element === void 0 ? void 0 : element.ELEMENT) || (element === null || element === void 0 ? void 0 : element['element-6066-11e4-a52e-4f735466cecf']);
75
+ if (elementId) {
76
+ this.logger.info('🎯 OCR resolved to real element via predicate search');
77
+ return Object.assign({ id: elementId }, found);
89
78
  }
90
- // Fallback: Return coordinate-based result with virtual ID
91
- // The CommandInterceptor handles rect-based results via coordinate tap
92
- return {
93
- id: `healed_ocr_${Date.now()}`,
94
- tier: this.tier,
95
- confidence: bestWord.confidence / 100,
96
- originalSelector: context.selector,
97
- originalStrategy: context.strategy,
98
- recommendedSelector: `ocr:text="${bestWord.text}"`,
99
- recommendedStrategy: 'xenon:visual',
100
- message: `Found text "${bestWord.text}" via local OCR (${bestWord.confidence.toFixed(0)}% confidence)`,
101
- rect: {
102
- x: bestWord.bbox.x0,
103
- y: bestWord.bbox.y0,
104
- width: bestWord.bbox.x1 - bestWord.bbox.x0,
105
- height: bestWord.bbox.y1 - bestWord.bbox.y0,
106
- },
107
- };
108
79
  }
80
+ catch (predErr) {
81
+ this.logger.debug(`Predicate search failed: ${predErr.message}`);
82
+ }
83
+ // Otherwise where the text is: the interceptor returns it as a virtual
84
+ // element, which a click taps.
85
+ return Object.assign({ id: `healed_ocr_${Date.now()}_${Math.random().toString(36).slice(2, 8)}` }, found);
109
86
  }
110
87
  catch (err) {
111
88
  this.logger.error(`Error during OCR healing: ${err.message}`);
@@ -14,10 +14,17 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
15
  exports.ResilioTreeHealingProvider = void 0;
16
16
  const types_1 = require("./types");
17
- const { Path, PathFinder, JSDOMParser, LCSPathDistance, HeuristicNodeDistance, } = require('resiliotree');
18
17
  const typedi_1 = require("typedi");
19
18
  const logger_1 = __importDefault(require("../../logger"));
20
19
  const HealedLocatorGenerator_1 = require("./HealedLocatorGenerator");
20
+ const resilioPath_1 = require("./resilioPath");
21
+ /**
22
+ * Tier 0: the element at the end of the path most like the one the selector's
23
+ * element had when the selector last worked (resilioPath.ts). It answers only
24
+ * when it is sure, which is when the element kept its identity (Android's
25
+ * resource-id, iOS's name) and moved in the tree: a positional XPath that
26
+ * broke when the layout changed. Anything less is the next tier's to decide.
27
+ */
21
28
  class ResilioTreeHealingProvider {
22
29
  constructor(etalonService) {
23
30
  this.etalonService = etalonService;
@@ -34,44 +41,45 @@ class ResilioTreeHealingProvider {
34
41
  }
35
42
  try {
36
43
  const signature = yield this.etalonService.getSignature(context.selector);
37
- if (!signature || !signature.path) {
44
+ if (!(0, resilioPath_1.isResilioPath)(signature === null || signature === void 0 ? void 0 : signature.path)) {
38
45
  this.logger.debug(`No ResilioTree path found for selector: ${context.selector}`);
39
46
  return null;
40
47
  }
41
48
  this.logger.info(`Attempting robust ResilioTree recovery for: ${context.selector}`);
42
- // 1. Parse current page source into ResilioTree model
43
- const parser = new JSDOMParser();
44
- const targetRoot = parser.parse(context.pageSource);
45
- // 2. Revive the saved path from JSON
46
- const savedPath = Path.fromJSON(signature.path);
47
- // 3. Find the nearest node using ResilioTree's PathFinder
48
- const pathDistance = new LCSPathDistance();
49
- const nodeDistance = new HeuristicNodeDistance();
50
- const pathFinder = new PathFinder(pathDistance, nodeDistance);
51
- const nearestNode = pathFinder.findNearest(savedPath, targetRoot);
52
- if (nearestNode) {
53
- const candidateLocators = this.generator.generate(nearestNode);
54
- const recommendedXpath = candidateLocators[0] || this.generateXpath(nearestNode);
55
- this.logger.info(`ResilioTree suggested recovery XPath: ${recommendedXpath}`);
49
+ const nearest = (0, resilioPath_1.nearestElement)(signature.path, context.pageSource);
50
+ if (!nearest) {
51
+ this.logger.info(`ResilioTree found no element it is sure of for: ${context.selector}`);
52
+ return null;
53
+ }
54
+ if ((0, resilioPath_1.contradictsSelector)(nearest.element, context.strategy, context.selector)) {
55
+ this.logger.info(`ResilioTree's element doesn't have the text the selector states; leaving it to the next tier: ${context.selector}`);
56
+ return null;
57
+ }
58
+ // The first of the element's locators the driver finds, among those
59
+ // that select that element alone in the page source.
60
+ const candidates = (0, resilioPath_1.locatorsSelectingOnly)(nearest.element, this.generator.generate(nearest.element));
61
+ for (const candidate of candidates) {
56
62
  try {
57
- const healedElement = yield context.driver.findElement('xpath', recommendedXpath);
58
- if (healedElement) {
59
- return {
60
- id: healedElement.ELEMENT || healedElement['element-6066-11e4-a52e-4f735466cecf'],
61
- originalSelector: context.selector,
62
- originalStrategy: context.strategy,
63
- recommendedSelector: recommendedXpath,
64
- recommendedStrategy: 'xpath',
65
- candidateSelectors: candidateLocators,
66
- confidence: 0.9, // ResilioTree path matching is high confidence
67
- tier: this.tier,
68
- node: nearestNode,
69
- message: `Recovered via ResilioTree path matching. New XPath: ${recommendedXpath}`,
70
- };
71
- }
63
+ const found = yield context.driver.findElement('xpath', candidate);
64
+ const id = (found === null || found === void 0 ? void 0 : found.ELEMENT) || (found === null || found === void 0 ? void 0 : found['element-6066-11e4-a52e-4f735466cecf']);
65
+ if (!id)
66
+ continue;
67
+ this.logger.info(`ResilioTree recovered the element (score ${nearest.score.toFixed(2)}): ${candidate}`);
68
+ return {
69
+ id,
70
+ originalSelector: context.selector,
71
+ originalStrategy: context.strategy,
72
+ recommendedSelector: candidate,
73
+ recommendedStrategy: 'xpath',
74
+ candidateSelectors: candidates,
75
+ confidence: nearest.score,
76
+ tier: this.tier,
77
+ node: nearest.element,
78
+ message: `Recovered via ResilioTree path matching. New XPath: ${candidate}`,
79
+ };
72
80
  }
73
- catch (e) {
74
- this.logger.debug(`Driver failed to find element suggested by ResilioTree: ${recommendedXpath}`);
81
+ catch (_a) {
82
+ this.logger.debug(`Driver failed to find element suggested by ResilioTree: ${candidate}`);
75
83
  }
76
84
  }
77
85
  }
@@ -81,22 +89,5 @@ class ResilioTreeHealingProvider {
81
89
  return null;
82
90
  });
83
91
  }
84
- /**
85
- * Generates an absolute XPath for a ResilioTree Node
86
- */
87
- generateXpath(node) {
88
- const parts = [];
89
- let curr = node;
90
- while (curr) {
91
- const tag = curr.tag.toLowerCase();
92
- // Skip JSDOM added wrapper tags for mobile compatibility
93
- if (tag !== 'html' && tag !== 'body') {
94
- const index = curr.index + 1; // XPath is 1-based
95
- parts.unshift(`${curr.tag}[${index}]`);
96
- }
97
- curr = curr.parent;
98
- }
99
- return `/${parts.join('/')}`;
100
- }
101
92
  }
102
93
  exports.ResilioTreeHealingProvider = ResilioTreeHealingProvider;
@@ -42,7 +42,7 @@ class VisualAiHealingProvider {
42
42
  if (coordinates && typeof coordinates.x === 'number' && typeof coordinates.y === 'number') {
43
43
  this.logger.info(`✅ Visual AI found element at (${coordinates.x}, ${coordinates.y})`);
44
44
  return {
45
- id: `healed_visual_${Date.now()}`,
45
+ id: `healed_visual_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
46
46
  tier: this.tier,
47
47
  confidence: 0.8, // Basic vision models don't always give confidence, assuming high if found
48
48
  originalSelector: context.selector,