@xenon-device-management/xenon 2.14.0 → 2.16.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 (335) hide show
  1. package/README.md +44 -29
  2. package/lib/package.json +6 -5
  3. package/lib/public/assets/{AnnotationOverlay-B_VNtKBH.js → AnnotationOverlay-2W4JvEyZ.js} +1 -1
  4. package/lib/public/assets/{ApiKeyGate-BwuaMjkX.js → ApiKeyGate-GmckX_7r.js} +1 -1
  5. package/lib/public/assets/DeviceMosaicView-DqODzkyo.js +2 -0
  6. package/lib/public/assets/{EmptyState-BSjhk_IV.js → EmptyState-7hETKEWb.js} +1 -1
  7. package/lib/public/assets/{FieldGroup-Cb5QZsE1.js → FieldGroup-GOgssFEP.js} +1 -1
  8. package/lib/public/assets/{FilterMenu-BAaSlWbW.js → FilterMenu-Yvf6RGMd.js} +1 -1
  9. package/lib/public/assets/{Menu-JGGYrSQQ.js → Menu-KrPnfdsf.js} +1 -1
  10. package/lib/public/assets/{Modal-CFCp3n9d.js → Modal-C6WjZFeI.js} +1 -1
  11. package/lib/public/assets/{RecordingPage-CbJj_EBT.js → RecordingPage-CSRIhZEh.js} +1 -1
  12. package/lib/public/assets/{RecordingsPage-BJeYnygR.js → RecordingsPage-CB8ST3nE.js} +1 -1
  13. package/lib/public/assets/{SegmentedControl-Bg42Atjy.js → SegmentedControl-C9o90KId.js} +1 -1
  14. package/lib/public/assets/{SettingCard-Njz1pUIJ.js → SettingCard-CPm3w9l2.js} +1 -1
  15. package/lib/public/assets/{Table-BcTm5xV2.js → Table-DwIgERRt.js} +1 -1
  16. package/lib/public/assets/{activity-DOVdsfMQ.js → activity-DrLbHifn.js} +1 -1
  17. package/lib/public/assets/ai-settings-DU_Ls06P.js +16 -0
  18. package/lib/public/assets/{api-keys-BKnNSTw9.js → api-keys-CWYdEzJ_.js} +1 -1
  19. package/lib/public/assets/{apps-D0SC6uw6.js → apps-BqNVdln7.js} +1 -1
  20. package/lib/public/assets/{arrow-left-MMbTmgoE.js → arrow-left-BtB0_Jg_.js} +1 -1
  21. package/lib/public/assets/{arrow-right-BMfnGmxW.js → arrow-right-C8NhdtyN.js} +1 -1
  22. package/lib/public/assets/{arrow-up-right-DuAftMWB.js → arrow-up-right-gMg0ffvv.js} +1 -1
  23. package/lib/public/assets/{auth-shell-nZC2JNDO.js → auth-shell-CiYZ3N4M.js} +2 -2
  24. package/lib/public/assets/{builds-page-hDCgTwkS.js → builds-page-EaaqDmLN.js} +1 -1
  25. package/lib/public/assets/{button-Dda4irn_.js → button-DDau4Axv.js} +1 -1
  26. package/lib/public/assets/{calendar-BMr0Q62v.js → calendar-DQ83B1ed.js} +1 -1
  27. package/lib/public/assets/{check-CZ9iw9-e.js → check-K1ovTQam.js} +1 -1
  28. package/lib/public/assets/{chevron-right-BRYJJVC9.js → chevron-right-Biq00pMt.js} +1 -1
  29. package/lib/public/assets/{circle-check-CqAJjQ5O.js → circle-check-CBv88cH7.js} +1 -1
  30. package/lib/public/assets/{circle-x-DlDJtnVz.js → circle-x-Bmltailn.js} +1 -1
  31. package/lib/public/assets/{clock-CpuDKysU.js → clock-BPrld-3h.js} +1 -1
  32. package/lib/public/assets/{copy-CAQydNFe.js → copy-t0QB14JI.js} +1 -1
  33. package/lib/public/assets/derive-UnLRp4r3.js +1 -0
  34. package/lib/public/assets/device-explorer-BgGZ8AvV.js +282 -0
  35. package/lib/public/assets/{download-C9XG6lLv.js → download-BroHxNOh.js} +1 -1
  36. package/lib/public/assets/forgot-password-Bjmf49i6.js +1 -0
  37. package/lib/public/assets/{h264Stream-VQBphYhg.js → h264Stream-DeD__6wa.js} +6 -6
  38. package/lib/public/assets/{index-DJGc1M2f.js → index-72oF89LJ.js} +1 -1
  39. package/lib/public/assets/index-B0JeuZbg.js +15 -0
  40. package/lib/public/assets/index-B0gQkEg0.js +219 -0
  41. package/lib/public/assets/{input-Cm0tmSoc.js → input-DIc4MSC7.js} +1 -1
  42. package/lib/public/assets/{line-chart-D2DyJCml.js → line-chart-BcWm2vW8.js} +1 -1
  43. package/lib/public/assets/{list-checks-UWf1GY99.js → list-checks-B06IJJad.js} +1 -1
  44. package/lib/public/assets/{lock-CCJv9m_I.js → lock-pVLvnN30.js} +1 -1
  45. package/lib/public/assets/{login-f8vOVW-U.js → login-CN77vy--.js} +1 -1
  46. package/lib/public/assets/{maintenance-settings-C1PrkuAH.js → maintenance-settings-CSdzQSkY.js} +1 -1
  47. package/lib/public/assets/{monitor-B2jDgwm7.js → monitor-BtrN5IkI.js} +1 -1
  48. package/lib/public/assets/{mouse-pointer-2-DLI1RtzU.js → mouse-pointer-2-CNsQfo6w.js} +1 -1
  49. package/lib/public/assets/{network-ebT86qoi.js → network-BSfA8ZxG.js} +1 -1
  50. package/lib/public/assets/{overview-C0aykVrd.js → overview-BfTebCK5.js} +1 -1
  51. package/lib/public/assets/{page-header-BjzgWz-T.js → page-header-D8KUX6Qg.js} +1 -1
  52. package/lib/public/assets/{play-DLG9t8FU.js → play-Cvc8xzic.js} +1 -1
  53. package/lib/public/assets/{plus-nBFSMu_X.js → plus-C9Zj84UF.js} +1 -1
  54. package/lib/public/assets/{profile-page-CSl0UYlv.js → profile-page-Bi27STgs.js} +1 -1
  55. package/lib/public/assets/{reset-password-LeOuHqmB.js → reset-password-CPsOuwSu.js} +1 -1
  56. package/lib/public/assets/runbook-page-DEQ-DBOX.js +304 -0
  57. package/lib/public/assets/{select-88jHcl-Z.js → select-BWpnhWXj.js} +1 -1
  58. package/lib/public/assets/{selector-detail-redirect-BbXKJjZM.js → selector-detail-redirect-BefSCSpe.js} +1 -1
  59. package/lib/public/assets/{selector-health-page-B4rA1Xkv.js → selector-health-page-MSk96Zz7.js} +1 -1
  60. package/lib/public/assets/session-detail-page-DPyBNef_.js +4 -0
  61. package/lib/public/assets/{settings-Dep_3kbA.css → settings-K8UHWMdI.css} +1 -1
  62. package/lib/public/assets/{settings-GF6ZMSQ6.js → settings-V6x-cXwy.js} +1 -1
  63. package/lib/public/assets/{stat-tile-BV98e_Sv.js → stat-tile-Duex-GZD.js} +1 -1
  64. package/lib/public/assets/{tablet-BiAsnFDj.js → tablet-BBcoqBmL.js} +1 -1
  65. package/lib/public/assets/{teams-B4-jrr-1.js → teams-ts8p5rEh.js} +1 -1
  66. package/lib/public/assets/{trash-2-C5RSuX0e.js → trash-2-CZ2ew6AZ.js} +1 -1
  67. package/lib/public/assets/{upload-BG7_rMXK.js → upload-Dv99y_le.js} +1 -1
  68. package/lib/public/assets/{use-builds-data-J-riQYyC.js → use-builds-data-DrYC8JCF.js} +1 -1
  69. package/lib/public/assets/{use-password-reset-mode-C8-eH-UG.js → use-password-reset-mode-C7QkBNkP.js} +1 -1
  70. package/lib/public/assets/{users-CFba5TSy.js → users-D7tKMESH.js} +1 -1
  71. package/lib/public/assets/{users-Cp0beJm3.js → users-gJ5Y01yo.js} +1 -1
  72. package/lib/public/assets/{video-off-DDsEISjV.js → video-off-CqZVuZGs.js} +1 -1
  73. package/lib/public/assets/{webhook-settings-BGu32vSB.js → webhook-settings-DI5DSW99.js} +1 -1
  74. package/lib/public/assets/{zap-l4VaXzq8.js → zap-ClcHFxfo.js} +1 -1
  75. package/lib/public/index.html +1 -1
  76. package/lib/schema.json +16 -12
  77. package/lib/src/InternalHttpClient.js +27 -5
  78. package/lib/src/XenonCapabilityManager.js +10 -3
  79. package/lib/src/app/dashboardPluginLink.js +117 -0
  80. package/lib/src/app/index.js +13 -34
  81. package/lib/src/app/openapi/control.yaml +6 -4
  82. package/lib/src/app/openapi/grid.yaml +8 -2
  83. package/lib/src/app/openapi/identity.yaml +103 -31
  84. package/lib/src/app/openapi/platform.yaml +33 -15
  85. package/lib/src/app/openapi/sessions.yaml +12 -3
  86. package/lib/src/app/routers/apikeys.js +14 -2
  87. package/lib/src/app/routers/auth.js +31 -6
  88. package/lib/src/app/routers/control.js +13 -5
  89. package/lib/src/app/routers/dashboard.js +42 -27
  90. package/lib/src/app/routers/grid.js +5 -3
  91. package/lib/src/app/routers/profile.js +21 -0
  92. package/lib/src/app/routers/users.js +12 -6
  93. package/lib/src/app/swagger.js +2 -0
  94. package/lib/src/app/ws/h264StreamWs.js +10 -1
  95. package/lib/src/app/ws/nodeSocketRelay.js +77 -11
  96. package/lib/src/config.js +1 -0
  97. package/lib/src/dashboard/commandLogFields.js +42 -0
  98. package/lib/src/dashboard/commands.js +5 -5
  99. package/lib/src/dashboard/event-manager.js +64 -204
  100. package/lib/src/dashboard/services/failure-analysis-service.js +118 -105
  101. package/lib/src/dashboard/services/failureCategories.js +264 -0
  102. package/lib/src/dashboard/sessionCommandSummary.js +39 -0
  103. package/lib/src/data-service/device-store.js +14 -1
  104. package/lib/src/data-service/prisma-store.js +19 -7
  105. package/lib/src/data-service/web-config-service.js +7 -0
  106. package/lib/src/device-managers/AndroidDeviceManager.js +61 -23
  107. package/lib/src/device-managers/IOSDeviceManager.js +10 -0
  108. package/lib/src/device-managers/android/AndroidH264StreamService.js +69 -20
  109. package/lib/src/device-managers/android/H264Multiplexer.js +38 -4
  110. package/lib/src/device-utils.js +29 -3
  111. package/lib/src/gateway/defaultGateway.js +1 -1
  112. package/lib/src/gateway/forwardToNode.js +6 -11
  113. package/lib/src/gateway/healReport.js +87 -0
  114. package/lib/src/gateway/hubSessionToken.js +27 -4
  115. package/lib/src/gateway/nodeWebDriverUrl.js +3 -5
  116. package/lib/src/gateway/sessionGateway.js +12 -2
  117. package/lib/src/generated/client/edge.js +4 -3
  118. package/lib/src/generated/client/index-browser.js +1 -0
  119. package/lib/src/generated/client/index.d.ts +32 -0
  120. package/lib/src/generated/client/index.js +4 -3
  121. package/lib/src/generated/client/package.json +1 -1
  122. package/lib/src/generated/client/schema.prisma +3 -0
  123. package/lib/src/generated/client/wasm.js +1 -0
  124. package/lib/src/helpers/outboundProxy.js +219 -0
  125. package/lib/src/index.js +21 -0
  126. package/lib/src/interceptors/CommandInterceptor.js +256 -196
  127. package/lib/src/interceptors/xenonScripts.js +28 -7
  128. package/lib/src/logger.js +3 -1
  129. package/lib/src/middleware/authMiddleware.js +15 -3
  130. package/lib/src/middleware/commandCaller.js +3 -3
  131. package/lib/src/middleware/verifyCredential.js +58 -1
  132. package/lib/src/plugin.js +16 -6
  133. package/lib/src/scripts/database-check.js +3 -3
  134. package/lib/src/scripts/initialize-database.js +35 -33
  135. package/lib/src/scripts/run-migrations.js +1079 -27
  136. package/lib/src/services/AICommandService.js +9 -2
  137. package/lib/src/services/AIService.js +192 -42
  138. package/lib/src/services/ApiKeyService.js +16 -5
  139. package/lib/src/services/EmailService.js +29 -0
  140. package/lib/src/services/InterceptorService.js +6 -2
  141. package/lib/src/services/OrphanSweeper.js +8 -0
  142. package/lib/src/services/SelectorStateService.js +11 -7
  143. package/lib/src/services/SelectorVerificationJob.js +21 -6
  144. package/lib/src/services/ServerManager.js +11 -1
  145. package/lib/src/services/SessionLifecycleService.js +118 -44
  146. package/lib/src/services/SocketClient.js +11 -0
  147. package/lib/src/services/SocketServer.js +380 -45
  148. package/lib/src/services/TeamService.js +5 -1
  149. package/lib/src/services/TracingService.js +171 -60
  150. package/lib/src/services/UserService.js +14 -3
  151. package/lib/src/services/UserSessionService.js +16 -4
  152. package/lib/src/services/bug-report/video-slice.js +4 -1
  153. package/lib/src/services/device-access/leaseOverride.js +7 -5
  154. package/lib/src/services/healing/FuzzyXmlHealingProvider.js +51 -0
  155. package/lib/src/services/healing/HealEtalonService.js +3 -2
  156. package/lib/src/services/healing/HealingOrchestrator.js +57 -72
  157. package/lib/src/services/healing/OcrHealingProvider.js +41 -64
  158. package/lib/src/services/healing/ResilioTreeHealingProvider.js +41 -50
  159. package/lib/src/services/healing/VisualAiHealingProvider.js +1 -1
  160. package/lib/src/services/healing/fingerprintIdentity.js +126 -0
  161. package/lib/src/services/healing/resilioPath.js +274 -0
  162. package/lib/src/services/identity/identityChanges.js +41 -0
  163. package/lib/src/services/logcat/SessionDeviceLogs.js +358 -0
  164. package/lib/src/services/logcat/deviceLogBook.js +164 -0
  165. package/lib/src/services/logcat/iosDriverLog.js +178 -0
  166. package/lib/src/services/ocr/ocrData.js +118 -0
  167. package/lib/src/services/ocr/vendor/LICENSE-tessdata.txt +202 -0
  168. package/lib/src/services/ocr/vendor/eng.traineddata.gz +0 -0
  169. package/lib/src/services/omni-vision/OmniVisionService.js +83 -56
  170. package/lib/src/services/passwordResetLink.js +50 -9
  171. package/lib/src/services/passwordResetMode.js +3 -1
  172. package/lib/src/services/publicUrl.js +45 -0
  173. package/lib/src/services/selector-health/SelectorVisibilityResolver.js +121 -0
  174. package/lib/src/services/session/sessionIdentity.js +5 -3
  175. package/lib/src/services/session/shutdownReason.js +26 -0
  176. package/lib/src/services/settings/aiEngineSettings.js +219 -0
  177. package/lib/src/services/token/JwtKeyService.js +8 -2
  178. package/lib/src/services/token/mcpTokenTtl.js +27 -0
  179. package/lib/src/services/token/mintedLifetime.js +51 -0
  180. package/lib/src/sessions/LocalSession.js +29 -1
  181. package/lib/src/sessions/RemoteSession.js +12 -2
  182. package/lib/src/sessions/SessionManager.js +4 -1
  183. package/lib/src/sessions/sessionMemory.js +25 -0
  184. package/lib/test/helpers/driver-log.js +38 -0
  185. package/lib/test/helpers/fake-ai-provider.js +188 -0
  186. package/lib/test/helpers/fake-http-proxy.js +106 -0
  187. package/lib/test/helpers/loki-stores.js +33 -0
  188. package/lib/test/helpers/otlp-probe.js +107 -0
  189. package/lib/test/helpers/page-sources.js +164 -0
  190. package/lib/test/helpers/scratch-database.js +57 -13
  191. package/lib/test/integration/androidDevices.spec.js +1 -1
  192. package/lib/test/integration/audit-ingest.spec.js +10 -10
  193. package/lib/test/integration/auth-flow.spec.js +9 -14
  194. package/lib/test/integration/auth-rate-limit.spec.js +6 -3
  195. package/lib/test/integration/bug-report-route.spec.js +22 -2
  196. package/lib/test/integration/forgot-password-rate-limit.spec.js +6 -3
  197. package/lib/test/integration/forgot-password.spec.js +17 -18
  198. package/lib/test/integration/ios/01iOSSimulator.spec.js +1 -1
  199. package/lib/test/integration/ios/02iOSDevices.spec.js +1 -1
  200. package/lib/test/integration/legacy-deprecation-headers.spec.js +5 -2
  201. package/lib/test/integration/migration.spec.js +6 -1
  202. package/lib/test/integration/node-pair-auth-rest.spec.js +5 -9
  203. package/lib/test/integration/ports-router.spec.js +4 -4
  204. package/lib/test/integration/prisma-device-store-add-devices.spec.js +2 -3
  205. package/lib/test/integration/prisma-device-store-team-filter.spec.js +2 -5
  206. package/lib/test/integration/profile-tokens.spec.js +10 -16
  207. package/lib/test/integration/recordings-router.spec.js +16 -16
  208. package/lib/test/integration/reset-revokes-sessions.spec.js +4 -9
  209. package/lib/test/integration/role-matrix.spec.js +4 -9
  210. package/lib/test/integration/sdk-leases-router.spec.js +9 -9
  211. package/lib/test/integration/sdk-version.spec.js +2 -2
  212. package/lib/test/integration/selector-health-endpoint.spec.js +5 -5
  213. package/lib/test/integration/selector-state-flow.spec.js +5 -5
  214. package/lib/test/integration/team-membership-migration.spec.js +73 -12
  215. package/lib/test/integration/team-visibility-apps.spec.js +15 -22
  216. package/lib/test/integration/team-visibility-control.spec.js +96 -69
  217. package/lib/test/integration/team-visibility-dashboard.spec.js +7 -23
  218. package/lib/test/integration/team-visibility-grid.spec.js +16 -15
  219. package/lib/test/integration/team-visibility-leases.spec.js +8 -11
  220. package/lib/test/integration/team-visibility-recordings.spec.js +6 -19
  221. package/lib/test/integration/team-visibility-sessions.spec.js +6 -19
  222. package/lib/test/integration/users-flow.spec.js +7 -12
  223. package/lib/test/integration/users-lockout.spec.js +5 -3
  224. package/lib/test/unit/EmailService.test.js +66 -0
  225. package/lib/test/unit/ResilioTreeHealingProvider.test.js +46 -19
  226. package/lib/test/unit/XenonCapabilityManager.spec.js +23 -0
  227. package/lib/test/unit/ai-call-time-limit.spec.js +130 -0
  228. package/lib/test/unit/ai-engine-settings.spec.js +338 -0
  229. package/lib/test/unit/ai-provider-switch.spec.js +130 -0
  230. package/lib/test/unit/ai-rate-limit.spec.js +126 -0
  231. package/lib/test/unit/android-adb-retry.spec.js +112 -0
  232. package/lib/test/unit/android-h264-viewers.spec.js +272 -0
  233. package/lib/test/unit/android-preview-hold.spec.js +3 -2
  234. package/lib/test/unit/apikeys-router.spec.js +84 -0
  235. package/lib/test/unit/authMiddleware.bearer.spec.js +9 -0
  236. package/lib/test/unit/authMiddleware.credentialLifetime.spec.js +149 -0
  237. package/lib/test/unit/bug-report/video-slice.spec.js +19 -0
  238. package/lib/test/unit/command-log-network-capture.spec.js +191 -0
  239. package/lib/test/unit/dashboard-events-device-logs.spec.js +255 -0
  240. package/lib/test/unit/dashboard-events-team-scope.spec.js +6 -3
  241. package/lib/test/unit/dashboard-plugin-link.spec.js +213 -0
  242. package/lib/test/unit/database-check.spec.js +1 -0
  243. package/lib/test/unit/device-log-book.spec.js +180 -0
  244. package/lib/test/unit/device-utils.spec.js +13 -4
  245. package/lib/test/unit/enable-dashboard-option.spec.js +57 -0
  246. package/lib/test/unit/event-log-session-data.spec.js +251 -0
  247. package/lib/test/unit/failure-analysis.spec.js +196 -0
  248. package/lib/test/unit/failure-categories.spec.js +130 -0
  249. package/lib/test/unit/failure-categorization.spec.js +393 -0
  250. package/lib/test/unit/fingerprint-identity.spec.js +433 -0
  251. package/lib/test/unit/h264-multiplexer.spec.js +46 -0
  252. package/lib/test/unit/heal-report.spec.js +71 -0
  253. package/lib/test/unit/healing-instrumentation.spec.js +6 -1
  254. package/lib/test/unit/healing-tier-gate.spec.js +74 -29
  255. package/lib/test/unit/healing-tiers-every-session.spec.js +219 -0
  256. package/lib/test/unit/hub-node-gateway-appium-server.spec.js +243 -1
  257. package/lib/test/unit/interceptor-visual-elements.spec.js +192 -0
  258. package/lib/test/unit/ios-driver-log.spec.js +168 -0
  259. package/lib/test/unit/ios-simulator-session-readiness.spec.js +187 -0
  260. package/lib/test/unit/ios-simulator-session-teardown.spec.js +158 -0
  261. package/lib/test/unit/lease/lease-override.spec.js +23 -6
  262. package/lib/test/unit/lease/session-lease-proof.spec.js +16 -4
  263. package/lib/test/unit/logcat-ws.spec.js +78 -37
  264. package/lib/test/unit/logger-json-levels.spec.js +44 -0
  265. package/lib/test/unit/mcp-token-ttl.spec.js +38 -0
  266. package/lib/test/unit/observability-example-loki.spec.js +25 -0
  267. package/lib/test/unit/ocr-healing-tier-real-ocr.spec.js +113 -0
  268. package/lib/test/unit/ocr-healing-tier.spec.js +122 -0
  269. package/lib/test/unit/ocr-offline.spec.js +213 -0
  270. package/lib/test/unit/omni-virtual-element-store.spec.js +301 -0
  271. package/lib/test/unit/omni-vision-ios-points.spec.js +0 -2
  272. package/lib/test/unit/omni-vision.spec.js +5 -7
  273. package/lib/test/unit/openapi-for-site.spec.js +80 -0
  274. package/lib/test/unit/outbound-proxy.spec.js +320 -0
  275. package/lib/test/unit/package-engines.spec.js +27 -0
  276. package/lib/test/unit/passwordResetMode.test.js +10 -2
  277. package/lib/test/unit/port-exhaustion.spec.js +2 -1
  278. package/lib/test/unit/profile-router.test.js +122 -1
  279. package/lib/test/unit/proxy-option.spec.js +358 -0
  280. package/lib/test/unit/queue-requester.spec.js +1 -1
  281. package/lib/test/unit/reset-link-address.spec.js +266 -0
  282. package/lib/test/unit/reset-link.test.js +10 -4
  283. package/lib/test/unit/resilio-path-healing.spec.js +270 -0
  284. package/lib/test/unit/run-migrations-database.spec.js +807 -0
  285. package/lib/test/unit/run-migrations-fail-fast.test.js +41 -27
  286. package/lib/test/unit/run-migrations.test.js +864 -0
  287. package/lib/test/unit/selector-health-actions.spec.js +3 -1
  288. package/lib/test/unit/selector-learning-every-session.spec.js +179 -0
  289. package/lib/test/unit/selector-state-service.spec.js +72 -23
  290. package/lib/test/unit/selector-verification-clean-build.spec.js +130 -0
  291. package/lib/test/unit/selector-verification-job.spec.js +28 -19
  292. package/lib/test/unit/selector-verification-missed-regression.spec.js +2 -2
  293. package/lib/test/unit/selector-visibility-resolver.spec.js +209 -0
  294. package/lib/test/unit/self-healing-boot.spec.js +10 -5
  295. package/lib/test/unit/self-healing-setting.spec.js +7 -6
  296. package/lib/test/unit/session-app-resolution.spec.js +5 -1
  297. package/lib/test/unit/session-attribution-wiring.spec.js +41 -13
  298. package/lib/test/unit/session-create-credential-owner.spec.js +242 -0
  299. package/lib/test/unit/session-credential-strip.spec.js +1 -1
  300. package/lib/test/unit/session-device-logs-ios.spec.js +180 -0
  301. package/lib/test/unit/session-device-logs.spec.js +356 -0
  302. package/lib/test/unit/session-end-failure-analysis.spec.js +133 -0
  303. package/lib/test/unit/session-log-indexes.spec.js +64 -3
  304. package/lib/test/unit/socket-team-scope.integration.spec.js +749 -0
  305. package/lib/test/unit/socketServer.scopedEmit.spec.js +237 -5
  306. package/lib/test/unit/socketServer.sessionCookie.spec.js +2 -1
  307. package/lib/test/unit/stream-start-pipelines.spec.js +72 -18
  308. package/lib/test/unit/token-mint-limits.spec.js +165 -0
  309. package/lib/test/unit/tracing-export-routing.spec.js +137 -0
  310. package/lib/test/unit/tracing-session-spans.spec.js +337 -0
  311. package/lib/test/unit/unexpected-shutdown-reason.spec.js +163 -0
  312. package/lib/test/unit/verify-credential.spec.js +33 -0
  313. package/lib/test/unit/vision-assertion.spec.js +3 -2
  314. package/lib/test/unit/web-config-service.spec.js +19 -0
  315. package/lib/tsconfig.tsbuildinfo +1 -1
  316. package/package.json +6 -5
  317. package/prisma/migrations/20261004174413_locator_etalon_path/migration.sql +2 -0
  318. package/prisma/schema.prisma +3 -0
  319. package/schema.json +16 -12
  320. package/scripts/dev/readme-screenshots.js +276 -23
  321. package/scripts/dev/screenshot-fixtures/logcat.js +157 -0
  322. package/scripts/dev/screenshot-fixtures/phone-screen.html +469 -0
  323. package/scripts/dev/screenshot-fixtures/session.js +242 -0
  324. package/scripts/export-openapi.js +32 -0
  325. package/scripts/lib/openapi-for-site.js +79 -0
  326. package/lib/public/assets/BugReportButton-BAt0_pzq.js +0 -15
  327. package/lib/public/assets/DeviceMosaicView-BYaQHhTf.js +0 -2
  328. package/lib/public/assets/ai-settings-CsuJroRf.js +0 -21
  329. package/lib/public/assets/device-explorer-CdO6_nrn.js +0 -282
  330. package/lib/public/assets/forgot-password-CGnkRgE9.js +0 -1
  331. package/lib/public/assets/index-BbKEXUFH.js +0 -219
  332. package/lib/public/assets/runbook-page-CMMeQml9.js +0 -124
  333. package/lib/public/assets/session-detail-page-COL_RZxw.js +0 -4
  334. package/lib/src/scripts/prepare-prisma.js +0 -89
  335. /package/lib/public/assets/{BugReportButton-CPBjfiv4.css → index-CPBjfiv4.css} +0 -0
@@ -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,6 +34,7 @@ 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");
@@ -86,24 +54,48 @@ function filterProvidersByTier(providers, allowedTiers) {
86
54
  const allowed = new Set(allowedTiers);
87
55
  return providers.filter((_, index) => allowed.has(index + 1));
88
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);
89
61
  // Coerce the raw xe:options.healingTiers capability value into the
90
- // allowedTiers argument for attemptHealing. Fail-open contract (Phase 2a
91
- // review fix): a malformed cap must RUN ALL tiers, never silently disable
92
- // healing. Pre-fix, an all-non-numeric array (e.g. ["1","2"]) filtered to []
93
- // which filterProvidersByTier turned into "no providers" — healing silently
94
- // off. Now any coercion that yields no numeric tiers (non-array, all-non-
95
- // numeric, or an explicitly-empty []) returns undefined → run all tiers.
96
- // The cap is meant to restrict healing, not disable it, so an explicit []
97
- // 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 [], or a value that wasn't a list or had no numbers in it, ran
69
+ // every tier, the AI ones included; a list that held some numbers ran those
70
+ // and skipped the rest ([1, 6] ran tier 1, [6] none).
98
71
  function coerceHealingTiersCap(raw) {
99
- const nums = Array.isArray(raw) ? raw.filter((t) => typeof t === 'number') : undefined;
100
- return nums && nums.length === 0 ? undefined : nums;
72
+ if (raw === undefined || raw === null)
73
+ return undefined;
74
+ return isHealingTierList(raw) ? [...raw] : [...exports.LOCAL_HEALING_TIERS];
101
75
  }
102
76
  // The allowedTiers a session asked for, read from its capabilities (one flat
103
- // map, e.g. the session response): xe:options.healingTiers, or the
104
- // xenon:options alias's, coerced as above.
77
+ // map: the session's driver's caps): xe:options.healingTiers, or the
78
+ // xenon:options alias's, coerced as above. A namespace that isn't an object
79
+ // can't be read either: xenonOptionsIn skips it, which would run every tier.
80
+ // `unreadable` says what couldn't be read, for the log.
105
81
  function healingTiersFromCaps(caps) {
106
- return coerceHealingTiersCap((0, xenonOptions_1.xenonOptionsIn)(caps).healingTiers);
82
+ for (const namespace of xenonOptions_1.OPTION_NAMESPACES) {
83
+ const bag = (0, xenonOptions_1.isOptionsObject)(caps) ? caps[namespace] : undefined;
84
+ if (bag !== undefined && bag !== null && !(0, xenonOptions_1.isOptionsObject)(bag)) {
85
+ return {
86
+ tiers: [...exports.LOCAL_HEALING_TIERS],
87
+ unreadable: `${namespace} is ${JSON.stringify(bag)}, not an object`,
88
+ };
89
+ }
90
+ }
91
+ const raw = (0, xenonOptions_1.xenonOptionsIn)(caps).healingTiers;
92
+ const tiers = coerceHealingTiersCap(raw);
93
+ return tiers === undefined || isHealingTierList(raw)
94
+ ? { tiers }
95
+ : {
96
+ tiers,
97
+ unreadable: `healingTiers is ${JSON.stringify(raw)}, not a list of tier numbers from 1 to 5`,
98
+ };
107
99
  }
108
100
  let HealingOrchestrator = class HealingOrchestrator {
109
101
  constructor(etalonService) {
@@ -120,7 +112,15 @@ let HealingOrchestrator = class HealingOrchestrator {
120
112
  }
121
113
  attemptHealing(sessionId, driver, strategy, selector, allowedTiers) {
122
114
  return __awaiter(this, void 0, void 0, function* () {
123
- var _a, _b;
115
+ var _a, _b, _c;
116
+ // §2.7: when the session's xe:options.healingTiers capability is set,
117
+ // restrict dispatch to the allowed tier indices; absent -> unchanged.
118
+ const activeProviders = filterProvidersByTier(this.providers, allowedTiers);
119
+ if (activeProviders.length === 0) {
120
+ // Nothing to run, so nothing is collected either: no screenshot taken.
121
+ this.logger.info(`Session ${sessionId} allows no self-healing tier; ${strategy}=${selector} is not healed.`);
122
+ return null;
123
+ }
124
124
  this.logger.info(`🚨 Self-Healing triggered for session ${sessionId}. Broken locator: ${strategy}=${selector}`);
125
125
  // Span wraps the whole attempt. Original selector text is intentionally
126
126
  // kept off the span — high cardinality (long XPath strings) would explode
@@ -151,9 +151,6 @@ let HealingOrchestrator = class HealingOrchestrator {
151
151
  return null;
152
152
  }
153
153
  // Tiered Execution: Try providers in order of cost/complexity.
154
- // §2.7: when the session's xe:options.healingTiers capability is set,
155
- // restrict dispatch to the allowed tier indices; absent -> unchanged.
156
- const activeProviders = filterProvidersByTier(this.providers, allowedTiers);
157
154
  for (const provider of activeProviders) {
158
155
  const tierStart = Date.now();
159
156
  span.addEvent('tier_started', { tier: provider.name });
@@ -203,23 +200,11 @@ let HealingOrchestrator = class HealingOrchestrator {
203
200
  else if (result.confidence > 0.7 && result.node) {
204
201
  try {
205
202
  this.logger.info(`🧠 Learning from healing success: updating etalon for ${selector}`);
206
- // Recalculate path for ResilioTree if node is available
207
- let learnedPath = null;
208
- if (result.node) {
209
- try {
210
- const { Path } = yield Promise.resolve().then(() => __importStar(require('resiliotree')));
211
- const pathNodes = [];
212
- let curr = result.node;
213
- while (curr) {
214
- pathNodes.unshift(curr);
215
- curr = curr.parent || curr.parentNode;
216
- }
217
- learnedPath = new Path(pathNodes).toJSON();
218
- }
219
- catch (_c) {
220
- // resiliotree import is best-effort; learnedPath stays null
221
- }
222
- }
203
+ // The healed element's path through the page source, for the
204
+ // Resilio tier. Through 2.14 this built it from the healed node
205
+ // with resiliotree, which an xmldom element (every node a tier
206
+ // returns) can't be, so it was always null.
207
+ const learnedPath = (0, resilioPath_1.resilioPathOf)(result.node, (_a = context.pageSource) !== null && _a !== void 0 ? _a : '');
223
208
  yield this.etalonService.saveSignature(strategy, selector, result.node, learnedPath);
224
209
  }
225
210
  catch (learnErr) {
@@ -235,7 +220,7 @@ let HealingOrchestrator = class HealingOrchestrator {
235
220
  span.setAttributes({
236
221
  [attributes_1.ATTR.HEALING_TIER]: provider.name,
237
222
  [attributes_1.ATTR.HEALING_CONFIDENCE]: result.confidence,
238
- [attributes_1.ATTR.HEALING_RESULT_STRATEGY]: (_a = result.recommendedStrategy) !== null && _a !== void 0 ? _a : 'xpath',
223
+ [attributes_1.ATTR.HEALING_RESULT_STRATEGY]: (_b = result.recommendedStrategy) !== null && _b !== void 0 ? _b : 'xpath',
239
224
  [attributes_1.ATTR.HEALING_DURATION_MS]: Date.now() - attemptStart,
240
225
  });
241
226
  span.setStatus({ code: api_1.SpanStatusCode.OK });
@@ -253,7 +238,7 @@ let HealingOrchestrator = class HealingOrchestrator {
253
238
  // tier can plausibly succeed (e.g. missing prerequisites that all
254
239
  // share). Saves an expensive LLM round-trip when upstream context
255
240
  // collection failed.
256
- if ((_b = provider.shouldSkipRemaining) === null || _b === void 0 ? void 0 : _b.call(provider, context)) {
241
+ if ((_c = provider.shouldSkipRemaining) === null || _c === void 0 ? void 0 : _c.call(provider, context)) {
257
242
  this.logger.warn(`Tier ${provider.tier} (${provider.name}) advised skipping remaining tiers for selector=${selector}`);
258
243
  HealingMetrics_1.HEALING_METRICS.recordSkippedRemaining(provider.tier, provider.name);
259
244
  span.addEvent('tier_skipped_remaining', { tier: provider.name });
@@ -273,8 +258,8 @@ let HealingOrchestrator = class HealingOrchestrator {
273
258
  }
274
259
  /**
275
260
  * The OCR and Visual AI tiers find the element in the screenshot, so their
276
- * `rect` is in its pixels. The interceptor taps it, and asks iOS for the
277
- * element there, in the driver's coordinates (points on iOS), so it is
261
+ * `rect` is in its pixels. The interceptor returns a virtual element there,
262
+ * which a click taps in the driver's coordinates (points on iOS), so it is
278
263
  * converted here. If that can't be worked out on iOS, a virtual element
279
264
  * (only a position) is no use and the tier counts as failed; a real element
280
265
  * the tier resolved keeps its id and loses only the rect.
@@ -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,
@@ -0,0 +1,126 @@
1
+ "use strict";
2
+ /**
3
+ * What a stored fingerprint (`LocatorSignature.attributes`) says about which
4
+ * element it is, as opposed to where the element was (x, y, width, height).
5
+ *
6
+ * Through 2.14 every learnt fingerprint held only the rect and the type: the
7
+ * attribute reads failed (CommandInterceptor.triggerLearning). Fuzzy XML, which
8
+ * weighs position above everything else, then healed a selector to whatever
9
+ * element of that type sat at that spot. A fingerprint with no identity isn't
10
+ * used for position, and is learnt again.
11
+ */
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.FINGERPRINT_IDENTITY_ATTRIBUTES = void 0;
14
+ exports.meaningful = meaningful;
15
+ exports.hasIdentity = hasIdentity;
16
+ exports.attributesToLearn = attributesToLearn;
17
+ exports.resourceIdName = resourceIdName;
18
+ exports.stableIdentifierOf = stableIdentifierOf;
19
+ exports.sameIdentifier = sameIdentifier;
20
+ exports.sameText = sameText;
21
+ /**
22
+ * The attributes that say which element a fingerprint is. Not `value`: on a
23
+ * text field it is what the test typed, which fingerprints no longer keep.
24
+ */
25
+ exports.FINGERPRINT_IDENTITY_ATTRIBUTES = [
26
+ 'resource-id',
27
+ 'content-desc',
28
+ 'accessibility-id',
29
+ 'text',
30
+ 'name',
31
+ 'label',
32
+ 'id',
33
+ 'hint',
34
+ ];
35
+ /**
36
+ * A value that says something: a string with more than spaces in it, and not
37
+ * "null", which is what UiAutomator2 answers for an attribute the element
38
+ * hasn't set (its getAttribute is String(value)).
39
+ */
40
+ function meaningful(value) {
41
+ return typeof value === 'string' && value.trim() !== '' && value.trim() !== 'null';
42
+ }
43
+ /** Whether a fingerprint says anything about which element it is. */
44
+ function hasIdentity(attributes) {
45
+ return exports.FINGERPRINT_IDENTITY_ATTRIBUTES.some((name) => meaningful(attributes === null || attributes === void 0 ? void 0 : attributes[name]));
46
+ }
47
+ /**
48
+ * The attributes learning asks a driver for: those it has. UiAutomator2
49
+ * throws for `id` and `label`, and its `name` is the text again; WebDriverAgent
50
+ * throws for anything but its own, so five of the old list were five failed
51
+ * round trips per selector learnt.
52
+ */
53
+ function attributesToLearn(platformName) {
54
+ const platform = String(platformName !== null && platformName !== void 0 ? platformName : '').toLowerCase();
55
+ if (platform === 'android')
56
+ return ['resource-id', 'content-desc', 'text', 'hint'];
57
+ if (platform === 'ios' || platform === 'tvos')
58
+ return ['name', 'label'];
59
+ return ['resource-id', 'content-desc', 'text', 'hint', 'name', 'label'];
60
+ }
61
+ /**
62
+ * An Android resource-id without its package: `com.shop:id/pay` is `pay`.
63
+ * Fuzzy XML's veto asks whether two ids are the same this way: ids of one
64
+ * app share the package, and of a debug build another one. Its score still
65
+ * compares whole ids, where the shared package earns a share of the weight.
66
+ */
67
+ function resourceIdName(resourceId) {
68
+ const at = resourceId.lastIndexOf(':id/');
69
+ if (at >= 0)
70
+ return resourceId.slice(at + 4);
71
+ return resourceId.slice(resourceId.lastIndexOf('/') + 1);
72
+ }
73
+ /**
74
+ * The identifier a developer gave the element, which stays when its text
75
+ * changes: an Android resource-id, or an iOS accessibility identifier (`name`
76
+ * when it isn't just the label; an element without one has its label as name).
77
+ */
78
+ function stableIdentifierOf(attributes) {
79
+ const resourceId = attributes === null || attributes === void 0 ? void 0 : attributes['resource-id'];
80
+ if (meaningful(resourceId))
81
+ return { attribute: 'resource-id', value: resourceId };
82
+ const name = attributes === null || attributes === void 0 ? void 0 : attributes.name;
83
+ if (meaningful(name) && name !== (attributes === null || attributes === void 0 ? void 0 : attributes.label))
84
+ return { attribute: 'name', value: name };
85
+ return null;
86
+ }
87
+ // Built at run time: the compiler's target predates Unicode property escapes
88
+ // in regular expression literals. Letters, their combining marks and digits,
89
+ // in any script.
90
+ const NOT_A_WORD = new RegExp('[^\\p{L}\\p{M}\\p{N}]+', 'gu');
91
+ const CAMEL_TURN = new RegExp('([\\p{Ll}\\p{N}])(\\p{Lu})', 'gu');
92
+ /** An identifier's words: split where camelCase turns and at anything but a letter or digit. */
93
+ function identifierWords(identifier) {
94
+ return identifier
95
+ .normalize('NFC')
96
+ .replace(CAMEL_TURN, '$1 $2')
97
+ .toLowerCase()
98
+ .split(NOT_A_WORD)
99
+ .filter(Boolean);
100
+ }
101
+ /**
102
+ * Whether two identifiers name the same element: they have the same words,
103
+ * however written (btn-pay, pay_btn, payBtn). Any other pair names another
104
+ * element, and the text decides: an id that gains words is as often another
105
+ * element (pay_later, undo_delete) as a rename (checkout_pay), and letters
106
+ * alone can't tell follow from unfollow, or checkout.cancel from
107
+ * checkout.pay (64% of their letter pairs are shared). Two with no letter or
108
+ * digit between them can't be told apart, and count as the same.
109
+ */
110
+ function sameIdentifier(a, b) {
111
+ const [wordsA, wordsB] = [new Set(identifierWords(a)), new Set(identifierWords(b))];
112
+ if (wordsA.size !== wordsB.size)
113
+ return false;
114
+ return [...wordsA].every((w) => wordsB.has(w));
115
+ }
116
+ /**
117
+ * Whether two texts read the same: case, punctuation and spacing aside, with
118
+ * accents composed (NFC). Combining marks count: Hindi दें and दो differ only
119
+ * by theirs. Nothing looser: "Log in" and "Log out", "Enable" and "Disable"
120
+ * are as alike by their letters as a reworded label is.
121
+ */
122
+ function sameText(a, b) {
123
+ const read = (t) => t.normalize('NFC').toLowerCase().replace(NOT_A_WORD, ' ').trim();
124
+ const [textA, textB] = [read(a), read(b)];
125
+ return textA !== '' && textA === textB;
126
+ }