@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
@@ -25,7 +25,9 @@ describe('POST /healing/selector/state (real queries)', function () {
25
25
  before(() => __awaiter(this, void 0, void 0, function* () {
26
26
  yield (0, selector_health_fixture_1.seedSelectorHealth)(scratch.db);
27
27
  // The service's transactions use the scratch database's own client.
28
- typedi_1.Container.set(SelectorStateService_1.SelectorStateService, new SelectorStateService_1.SelectorStateService(scratch.db, { emitToDashboard: () => undefined }));
28
+ typedi_1.Container.set(SelectorStateService_1.SelectorStateService, new SelectorStateService_1.SelectorStateService(scratch.db, {
29
+ emitToDashboardForSelector: () => __awaiter(this, void 0, void 0, function* () { return undefined; }),
30
+ }));
29
31
  }));
30
32
  after(() => {
31
33
  typedi_1.Container.remove(SelectorStateService_1.SelectorStateService);
@@ -0,0 +1,179 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ require("reflect-metadata");
16
+ const chai_1 = require("chai");
17
+ const sinon_1 = __importDefault(require("sinon"));
18
+ const typedi_1 = require("typedi");
19
+ const CommandInterceptor_1 = require("../../src/interceptors/CommandInterceptor");
20
+ const HealEtalonService_1 = require("../../src/services/healing/HealEtalonService");
21
+ const SelfHealingSwitch_1 = require("../../src/services/settings/SelfHealingSwitch");
22
+ const SessionManager_1 = require("../../src/sessions/SessionManager");
23
+ const event_manager_1 = require("../../src/dashboard/event-manager");
24
+ const IPluginArgs_1 = require("../../src/interfaces/IPluginArgs");
25
+ const waitFor_1 = require("../../src/services/autowait/waitFor");
26
+ const container_registration_1 = require("../helpers/container-registration");
27
+ const scratch_database_1 = require("../helpers/scratch-database");
28
+ /**
29
+ * After a findElement finds its element, Xenon learns the selector's
30
+ * fingerprint (the element's attributes, position and path), which the
31
+ * Resilio and Fuzzy XML tiers heal it with later, on the server, without
32
+ * calling the AI provider.
33
+ *
34
+ * Through 2.14 it learnt only behind the dashboard's per-command record
35
+ * (`isHub && enableDashboard && SESSION_MANAGER.isValidSession`): never with
36
+ * `enableDashboard` off (the default), and never on a node. Those servers
37
+ * healed without fingerprints, so a broken selector went on to OCR, Visual AI
38
+ * and the LLM. Learning now follows the self-healing switch alone, on every
39
+ * session this server drives. The record itself is unchanged.
40
+ */
41
+ describe('selector learning, on every session this server drives', () => {
42
+ (0, scratch_database_1.useScratchDatabase)();
43
+ let restoreContainer;
44
+ let etalons;
45
+ let learn;
46
+ let record;
47
+ beforeEach(() => {
48
+ restoreContainer = (0, container_registration_1.saveRegistrations)(HealEtalonService_1.HealEtalonService, SelfHealingSwitch_1.SelfHealingSwitch);
49
+ // The switch as at boot with nothing saved: it follows the options.
50
+ typedi_1.Container.set(SelfHealingSwitch_1.SelfHealingSwitch, new SelfHealingSwitch_1.SelfHealingSwitch());
51
+ etalons = {
52
+ getSignature: sinon_1.default.stub().resolves(null),
53
+ saveSignature: sinon_1.default.stub().resolves(),
54
+ };
55
+ typedi_1.Container.set(HealEtalonService_1.HealEtalonService, etalons);
56
+ learn = sinon_1.default.spy(CommandInterceptor_1.CommandInterceptor.prototype, 'triggerLearning');
57
+ record = sinon_1.default.stub(event_manager_1.DASHBORD_EVENT_MANAGER, 'afterSessionCommand').resolves();
58
+ });
59
+ afterEach(() => {
60
+ sinon_1.default.restore();
61
+ restoreContainer();
62
+ });
63
+ const options = (over = {}) => (Object.assign(Object.assign(Object.assign({}, IPluginArgs_1.DefaultPluginArgs), { enableDashboard: false }), over));
64
+ let sessions = 0;
65
+ /** A session's driver, with what learning reads from a found element. */
66
+ function sessionDriver(caps = {}) {
67
+ sessions += 1;
68
+ return {
69
+ sessionId: `learn-${sessions}-${Date.now()}`,
70
+ caps: Object.assign({ platformName: 'Android' }, caps),
71
+ // As UiAutomator2 answers: String(value), so "null" for an unset one.
72
+ getAttribute: (name, _id) => __awaiter(this, void 0, void 0, function* () { var _a; return String((_a = { 'resource-id': 'com.shop:id/pay', text: 'Pay now' }[name]) !== null && _a !== void 0 ? _a : null); }),
73
+ getElementRect: () => __awaiter(this, void 0, void 0, function* () { return ({ x: 40, y: 900, width: 300, height: 96 }); }),
74
+ getName: () => __awaiter(this, void 0, void 0, function* () { return 'android.widget.Button'; }),
75
+ getPageSource: () => __awaiter(this, void 0, void 0, function* () { return '<hierarchy><android.widget.Button text="Pay now" resource-id="com.shop:id/pay"/></hierarchy>'; }),
76
+ };
77
+ }
78
+ const PAY = ['id', 'com.shop:id/pay'];
79
+ const FOUND = { ELEMENT: 'el-1', 'element-6066-11e4-a52e-4f735466cecf': 'el-1' };
80
+ /** A findElement the driver answers with its element. */
81
+ function find(driver_1, pluginArgs_1, _a) {
82
+ return __awaiter(this, arguments, void 0, function* (driver, pluginArgs, { isHub, command = 'findElement' }) {
83
+ const response = command === 'findElements' ? [FOUND] : FOUND;
84
+ return typedi_1.Container.get(CommandInterceptor_1.CommandInterceptor).handle(() => __awaiter(this, void 0, void 0, function* () { return response; }), driver, command, [...PAY, driver.sessionId], pluginArgs, isHub);
85
+ });
86
+ }
87
+ /** Learning runs in the background, after the find has answered. */
88
+ function learnt() {
89
+ return __awaiter(this, void 0, void 0, function* () {
90
+ const result = yield (0, waitFor_1.waitFor)(() => __awaiter(this, void 0, void 0, function* () { return (etalons.saveSignature.called ? true : null); }), {
91
+ timeoutMs: 2000,
92
+ intervalMs: 5,
93
+ });
94
+ return 'value' in result;
95
+ });
96
+ }
97
+ for (const [where, isHub] of [
98
+ ['a standalone server', true],
99
+ ["a node running its hub's session", false],
100
+ ]) {
101
+ it(`learns a found selector on ${where} with enableDashboard off`, () => __awaiter(void 0, void 0, void 0, function* () {
102
+ const driver = sessionDriver();
103
+ // The case: nothing registers it, as nothing registered it before.
104
+ (0, chai_1.expect)(SessionManager_1.SESSION_MANAGER.isValidSession(driver.sessionId)).to.equal(false);
105
+ const answer = yield find(driver, options(), { isHub });
106
+ (0, chai_1.expect)(answer).to.deep.equal(FOUND);
107
+ (0, chai_1.expect)(learn.calledOnce, 'learning started').to.equal(true);
108
+ (0, chai_1.expect)(yield learnt(), 'a fingerprint was saved').to.equal(true);
109
+ (0, chai_1.expect)(etalons.saveSignature.firstCall.args.slice(0, 2)).to.deep.equal(PAY);
110
+ // A fingerprint that says which element it is, which Fuzzy XML can use.
111
+ (0, chai_1.expect)(etalons.saveSignature.firstCall.args[2].attributes).to.deep.include({
112
+ name: 'resource-id',
113
+ value: 'com.shop:id/pay',
114
+ });
115
+ // The dashboard's record is still the dashboard's: nothing was recorded.
116
+ (0, chai_1.expect)(record.called).to.equal(false);
117
+ }));
118
+ }
119
+ it('learns a selector autowait found', () => __awaiter(void 0, void 0, void 0, function* () {
120
+ const driver = sessionDriver();
121
+ const pluginArgs = options({
122
+ autowait: {
123
+ enabled: true,
124
+ timeoutMs: 500,
125
+ intervalBetweenAttemptsMs: 10,
126
+ excludeEnabledCheck: [],
127
+ },
128
+ });
129
+ yield find(driver, pluginArgs, { isHub: true });
130
+ (0, chai_1.expect)(learn.calledOnce).to.equal(true);
131
+ (0, chai_1.expect)(yield learnt()).to.equal(true);
132
+ }));
133
+ it('learns once, and still records, where the dashboard records the session', () => __awaiter(void 0, void 0, void 0, function* () {
134
+ const driver = sessionDriver();
135
+ sinon_1.default.stub(SessionManager_1.SESSION_MANAGER, 'isValidSession').returns(true);
136
+ yield find(driver, options({ enableDashboard: true }), { isHub: true });
137
+ (0, chai_1.expect)(record.calledOnce).to.equal(true);
138
+ (0, chai_1.expect)(learn.calledOnce).to.equal(true);
139
+ (0, chai_1.expect)(yield learnt()).to.equal(true);
140
+ }));
141
+ it('learns nothing with self-healing off', () => __awaiter(void 0, void 0, void 0, function* () {
142
+ yield find(sessionDriver(), options({ enableSelfHealing: false }), { isHub: true });
143
+ (0, chai_1.expect)(learn.called).to.equal(false);
144
+ }));
145
+ it('learns nothing from a session that turned its own healing off', () => __awaiter(void 0, void 0, void 0, function* () {
146
+ const driver = sessionDriver({ 'xe:options': { healingTiers: [] } });
147
+ yield find(driver, options(), { isHub: true });
148
+ (0, chai_1.expect)(learn.called).to.equal(false);
149
+ }));
150
+ it('still learns from a session that limits its tiers', () => __awaiter(void 0, void 0, void 0, function* () {
151
+ const driver = sessionDriver({ 'xe:options': { healingTiers: [3] } });
152
+ yield find(driver, options(), { isHub: true });
153
+ (0, chai_1.expect)(learn.calledOnce).to.equal(true);
154
+ }));
155
+ it('learns nothing from findElements, as before', () => __awaiter(void 0, void 0, void 0, function* () {
156
+ yield find(sessionDriver(), options(), { isHub: true, command: 'findElements' });
157
+ (0, chai_1.expect)(learn.called).to.equal(false);
158
+ }));
159
+ it('answers the find even when deciding whether to learn throws', () => __awaiter(void 0, void 0, void 0, function* () {
160
+ sinon_1.default.stub(SelfHealingSwitch_1.SelfHealingSwitch.prototype, 'isEnabled').throws(new Error('switch unreadable'));
161
+ const answer = yield find(sessionDriver(), options(), { isHub: true });
162
+ (0, chai_1.expect)(answer).to.deep.equal(FOUND);
163
+ (0, chai_1.expect)(learn.called).to.equal(false);
164
+ }));
165
+ it('answers the find even when learning fails', () => __awaiter(void 0, void 0, void 0, function* () {
166
+ etalons.getSignature.rejects(new Error('database is locked'));
167
+ const driver = sessionDriver();
168
+ const answer = yield find(driver, options(), { isHub: true });
169
+ (0, chai_1.expect)(answer).to.deep.equal(FOUND);
170
+ // The background pass ends, and the session may learn again.
171
+ const settled = yield (0, waitFor_1.waitFor)(() => __awaiter(void 0, void 0, void 0, function* () {
172
+ return typedi_1.Container.get(CommandInterceptor_1.CommandInterceptor).learningSessions.has(driver.sessionId)
173
+ ? null
174
+ : true;
175
+ }), { timeoutMs: 2000, intervalMs: 5 });
176
+ (0, chai_1.expect)('value' in settled).to.equal(true);
177
+ (0, chai_1.expect)(etalons.saveSignature.called).to.equal(false);
178
+ }));
179
+ });
@@ -38,7 +38,7 @@ function buildPrismaStub() {
38
38
  }
39
39
  function buildSocketStub() {
40
40
  return {
41
- emitToDashboard: sinon_1.default.stub(),
41
+ emitToDashboardForSelector: sinon_1.default.stub().resolves(),
42
42
  };
43
43
  }
44
44
  const STRATEGY = 'accessibility id';
@@ -105,8 +105,8 @@ describe('SelectorStateService', () => {
105
105
  selector: SELECTOR,
106
106
  apiKeyId: API_KEY_ID,
107
107
  });
108
- (0, chai_1.expect)(socketStub.emitToDashboard.calledOnce).to.equal(true);
109
- const [event, payload] = socketStub.emitToDashboard.firstCall.args;
108
+ (0, chai_1.expect)(socketStub.emitToDashboardForSelector.calledOnce).to.equal(true);
109
+ const [event, payload] = socketStub.emitToDashboardForSelector.firstCall.args;
110
110
  (0, chai_1.expect)(event).to.equal(SocketEvents_1.SocketEvents.SELECTOR_FIXED);
111
111
  (0, chai_1.expect)(payload).to.include({
112
112
  original_strategy: STRATEGY,
@@ -133,7 +133,7 @@ describe('SelectorStateService', () => {
133
133
  (0, chai_1.expect)(err).to.be.instanceOf(SelectorStateService_1.SelectorStateConflictError);
134
134
  (0, chai_1.expect)(err.currentStatus).to.equal('muted');
135
135
  (0, chai_1.expect)(prismaStub.selectorState.upsert.called).to.equal(false);
136
- (0, chai_1.expect)(socketStub.emitToDashboard.called).to.equal(false);
136
+ (0, chai_1.expect)(socketStub.emitToDashboardForSelector.called).to.equal(false);
137
137
  }));
138
138
  });
139
139
  describe('mute', () => {
@@ -170,8 +170,8 @@ describe('SelectorStateService', () => {
170
170
  selector: SELECTOR,
171
171
  apiKeyId: API_KEY_ID,
172
172
  });
173
- (0, chai_1.expect)(socketStub.emitToDashboard.calledOnce).to.equal(true);
174
- const [event, payload] = socketStub.emitToDashboard.firstCall.args;
173
+ (0, chai_1.expect)(socketStub.emitToDashboardForSelector.calledOnce).to.equal(true);
174
+ const [event, payload] = socketStub.emitToDashboardForSelector.firstCall.args;
175
175
  (0, chai_1.expect)(event).to.equal(SocketEvents_1.SocketEvents.SELECTOR_MUTED);
176
176
  (0, chai_1.expect)(payload.status).to.equal('muted');
177
177
  (0, chai_1.expect)(payload.muted_at).to.equal('2026-04-25T02:00:00.000Z');
@@ -195,8 +195,8 @@ describe('SelectorStateService', () => {
195
195
  });
196
196
  (0, chai_1.expect)(prismaStub.selectorState.delete.calledOnce).to.equal(true);
197
197
  (0, chai_1.expect)(prismaStub.selectorState.update.called).to.equal(false);
198
- (0, chai_1.expect)(socketStub.emitToDashboard.calledOnce).to.equal(true);
199
- (0, chai_1.expect)(socketStub.emitToDashboard.firstCall.args[0]).to.equal(SocketEvents_1.SocketEvents.SELECTOR_UNMUTED);
198
+ (0, chai_1.expect)(socketStub.emitToDashboardForSelector.calledOnce).to.equal(true);
199
+ (0, chai_1.expect)(socketStub.emitToDashboardForSelector.firstCall.args[0]).to.equal(SocketEvents_1.SocketEvents.SELECTOR_UNMUTED);
200
200
  }));
201
201
  it('keeps the row and sets status=active when history exists', () => __awaiter(void 0, void 0, void 0, function* () {
202
202
  const muted = makeRow({
@@ -217,8 +217,8 @@ describe('SelectorStateService', () => {
217
217
  const args = prismaStub.selectorState.update.firstCall.args[0];
218
218
  (0, chai_1.expect)(args.data.status).to.equal('active');
219
219
  (0, chai_1.expect)(args.data.muted_at).to.equal(null);
220
- (0, chai_1.expect)(socketStub.emitToDashboard.calledOnce).to.equal(true);
221
- (0, chai_1.expect)(socketStub.emitToDashboard.firstCall.args[0]).to.equal(SocketEvents_1.SocketEvents.SELECTOR_UNMUTED);
220
+ (0, chai_1.expect)(socketStub.emitToDashboardForSelector.calledOnce).to.equal(true);
221
+ (0, chai_1.expect)(socketStub.emitToDashboardForSelector.firstCall.args[0]).to.equal(SocketEvents_1.SocketEvents.SELECTOR_UNMUTED);
222
222
  }));
223
223
  it('is a no-op when the row exists but is not muted', () => __awaiter(void 0, void 0, void 0, function* () {
224
224
  prismaStub.selectorState.findUnique.resolves(makeRow({ status: 'active' }));
@@ -229,7 +229,7 @@ describe('SelectorStateService', () => {
229
229
  });
230
230
  (0, chai_1.expect)(prismaStub.selectorState.delete.called).to.equal(false);
231
231
  (0, chai_1.expect)(prismaStub.selectorState.update.called).to.equal(false);
232
- (0, chai_1.expect)(socketStub.emitToDashboard.called).to.equal(false);
232
+ (0, chai_1.expect)(socketStub.emitToDashboardForSelector.called).to.equal(false);
233
233
  }));
234
234
  it('is a no-op when no row exists', () => __awaiter(void 0, void 0, void 0, function* () {
235
235
  prismaStub.selectorState.findUnique.resolves(null);
@@ -240,7 +240,7 @@ describe('SelectorStateService', () => {
240
240
  });
241
241
  (0, chai_1.expect)(prismaStub.selectorState.delete.called).to.equal(false);
242
242
  (0, chai_1.expect)(prismaStub.selectorState.update.called).to.equal(false);
243
- (0, chai_1.expect)(socketStub.emitToDashboard.called).to.equal(false);
243
+ (0, chai_1.expect)(socketStub.emitToDashboardForSelector.called).to.equal(false);
244
244
  }));
245
245
  });
246
246
  describe('cancelVerification', () => {
@@ -261,7 +261,7 @@ describe('SelectorStateService', () => {
261
261
  (0, chai_1.expect)(err.currentStatus).to.equal('active');
262
262
  (0, chai_1.expect)(prismaStub.selectorState.delete.called).to.equal(false);
263
263
  (0, chai_1.expect)(prismaStub.selectorState.update.called).to.equal(false);
264
- (0, chai_1.expect)(socketStub.emitToDashboard.called).to.equal(false);
264
+ (0, chai_1.expect)(socketStub.emitToDashboardForSelector.called).to.equal(false);
265
265
  }));
266
266
  it('throws SelectorStateConflictError when no row exists', () => __awaiter(void 0, void 0, void 0, function* () {
267
267
  prismaStub.selectorState.findUnique.resolves(null);
@@ -294,8 +294,8 @@ describe('SelectorStateService', () => {
294
294
  });
295
295
  (0, chai_1.expect)(prismaStub.selectorState.delete.calledOnce).to.equal(true);
296
296
  (0, chai_1.expect)(prismaStub.selectorState.update.called).to.equal(false);
297
- (0, chai_1.expect)(socketStub.emitToDashboard.calledOnce).to.equal(true);
298
- (0, chai_1.expect)(socketStub.emitToDashboard.firstCall.args[0]).to.equal(SocketEvents_1.SocketEvents.SELECTOR_CANCELLED);
297
+ (0, chai_1.expect)(socketStub.emitToDashboardForSelector.calledOnce).to.equal(true);
298
+ (0, chai_1.expect)(socketStub.emitToDashboardForSelector.firstCall.args[0]).to.equal(SocketEvents_1.SocketEvents.SELECTOR_CANCELLED);
299
299
  }));
300
300
  it('keeps row and clears fixed state when history exists (resolved_at not null)', () => __awaiter(void 0, void 0, void 0, function* () {
301
301
  const pending = makeRow({
@@ -318,7 +318,7 @@ describe('SelectorStateService', () => {
318
318
  (0, chai_1.expect)(args.data.status).to.equal('active');
319
319
  (0, chai_1.expect)(args.data.fixed_at).to.equal(null);
320
320
  (0, chai_1.expect)(args.data.clean_builds_count).to.equal(0);
321
- (0, chai_1.expect)(socketStub.emitToDashboard.firstCall.args[0]).to.equal(SocketEvents_1.SocketEvents.SELECTOR_CANCELLED);
321
+ (0, chai_1.expect)(socketStub.emitToDashboardForSelector.firstCall.args[0]).to.equal(SocketEvents_1.SocketEvents.SELECTOR_CANCELLED);
322
322
  }));
323
323
  });
324
324
  describe('onHealRecorded', () => {
@@ -344,8 +344,8 @@ describe('SelectorStateService', () => {
344
344
  (0, chai_1.expect)(args.data.resolved_at).to.equal(null);
345
345
  (0, chai_1.expect)(args.data.regression_count).to.deep.equal({ increment: 1 });
346
346
  (0, chai_1.expect)(args.data.clean_builds_count).to.equal(0);
347
- (0, chai_1.expect)(socketStub.emitToDashboard.calledOnce).to.equal(true);
348
- (0, chai_1.expect)(socketStub.emitToDashboard.firstCall.args[0]).to.equal(SocketEvents_1.SocketEvents.SELECTOR_REGRESSED);
347
+ (0, chai_1.expect)(socketStub.emitToDashboardForSelector.calledOnce).to.equal(true);
348
+ (0, chai_1.expect)(socketStub.emitToDashboardForSelector.firstCall.args[0]).to.equal(SocketEvents_1.SocketEvents.SELECTOR_REGRESSED);
349
349
  }));
350
350
  it('flips resolved → active, clears resolved_at, and increments regression_count', () => __awaiter(void 0, void 0, void 0, function* () {
351
351
  const resolved = makeRow({
@@ -368,7 +368,7 @@ describe('SelectorStateService', () => {
368
368
  (0, chai_1.expect)(args.data.resolved_at).to.equal(null);
369
369
  (0, chai_1.expect)(args.data.fixed_at).to.equal(null);
370
370
  (0, chai_1.expect)(args.data.regression_count).to.deep.equal({ increment: 1 });
371
- (0, chai_1.expect)(socketStub.emitToDashboard.firstCall.args[0]).to.equal(SocketEvents_1.SocketEvents.SELECTOR_REGRESSED);
371
+ (0, chai_1.expect)(socketStub.emitToDashboardForSelector.firstCall.args[0]).to.equal(SocketEvents_1.SocketEvents.SELECTOR_REGRESSED);
372
372
  }));
373
373
  it('is a no-op when the row is muted', () => __awaiter(void 0, void 0, void 0, function* () {
374
374
  prismaStub.selectorState.findUnique.resolves(makeRow({ status: 'muted' }));
@@ -378,7 +378,7 @@ describe('SelectorStateService', () => {
378
378
  sessionId: SESSION_ID,
379
379
  });
380
380
  (0, chai_1.expect)(prismaStub.selectorState.update.called).to.equal(false);
381
- (0, chai_1.expect)(socketStub.emitToDashboard.called).to.equal(false);
381
+ (0, chai_1.expect)(socketStub.emitToDashboardForSelector.called).to.equal(false);
382
382
  }));
383
383
  it('is a no-op when no row exists', () => __awaiter(void 0, void 0, void 0, function* () {
384
384
  prismaStub.selectorState.findUnique.resolves(null);
@@ -388,7 +388,7 @@ describe('SelectorStateService', () => {
388
388
  sessionId: SESSION_ID,
389
389
  });
390
390
  (0, chai_1.expect)(prismaStub.selectorState.update.called).to.equal(false);
391
- (0, chai_1.expect)(socketStub.emitToDashboard.called).to.equal(false);
391
+ (0, chai_1.expect)(socketStub.emitToDashboardForSelector.called).to.equal(false);
392
392
  }));
393
393
  it('is a no-op when row is already active', () => __awaiter(void 0, void 0, void 0, function* () {
394
394
  prismaStub.selectorState.findUnique.resolves(makeRow({ status: 'active' }));
@@ -398,7 +398,7 @@ describe('SelectorStateService', () => {
398
398
  sessionId: SESSION_ID,
399
399
  });
400
400
  (0, chai_1.expect)(prismaStub.selectorState.update.called).to.equal(false);
401
- (0, chai_1.expect)(socketStub.emitToDashboard.called).to.equal(false);
401
+ (0, chai_1.expect)(socketStub.emitToDashboardForSelector.called).to.equal(false);
402
402
  }));
403
403
  });
404
404
  describe('getState', () => {
@@ -443,7 +443,7 @@ describe('SelectorStateService', () => {
443
443
  thrown = e;
444
444
  }
445
445
  (0, chai_1.expect)(thrown.message).to.equal('disk full');
446
- (0, chai_1.expect)(socketStub.emitToDashboard.called).to.equal(false);
446
+ (0, chai_1.expect)(socketStub.emitToDashboardForSelector.called).to.equal(false);
447
447
  }));
448
448
  it('records the mute with its reason', () => __awaiter(void 0, void 0, void 0, function* () {
449
449
  prismaStub.selectorState.upsert.resolves(makeRow({ status: 'muted', muted_at: new Date() }));
@@ -483,4 +483,53 @@ describe('SelectorStateService', () => {
483
483
  (0, chai_1.expect)(prismaStub.selectorEvent.create.called).to.equal(false);
484
484
  }));
485
485
  });
486
+ // The socket server sends a selector's event only to the dashboards whose
487
+ // caller may see the selector, so every event names it.
488
+ describe('each event names its selector, for the socket server to scope it', () => {
489
+ const KEY = { strategy: STRATEGY, selector: SELECTOR };
490
+ const ctx = { strategy: STRATEGY, selector: SELECTOR, apiKeyId: API_KEY_ID };
491
+ const sent = () => socketStub.emitToDashboardForSelector.getCalls().map((c) => [c.args[0], c.args[2]]);
492
+ it('markFixed and mute', () => __awaiter(void 0, void 0, void 0, function* () {
493
+ prismaStub.selectorState.findUnique.resolves(null);
494
+ prismaStub.selectorState.upsert.resolves(makeRow({ status: 'pending' }));
495
+ yield svc.markFixed(ctx);
496
+ prismaStub.selectorState.upsert.resolves(makeRow({ status: 'muted' }));
497
+ yield svc.mute(ctx);
498
+ (0, chai_1.expect)(sent()).to.deep.equal([
499
+ [SocketEvents_1.SocketEvents.SELECTOR_FIXED, KEY],
500
+ [SocketEvents_1.SocketEvents.SELECTOR_MUTED, KEY],
501
+ ]);
502
+ }));
503
+ it('unmute and cancelVerification, whether the row is kept or deleted', () => __awaiter(void 0, void 0, void 0, function* () {
504
+ prismaStub.selectorState.findUnique.resolves(makeRow({ status: 'muted' }));
505
+ yield svc.unmute(ctx);
506
+ prismaStub.selectorState.findUnique.resolves(makeRow({ status: 'muted', regression_count: 1 }));
507
+ prismaStub.selectorState.update.resolves(makeRow({ status: 'active', regression_count: 1 }));
508
+ yield svc.unmute(ctx);
509
+ prismaStub.selectorState.findUnique.resolves(makeRow({ status: 'pending' }));
510
+ yield svc.cancelVerification(ctx);
511
+ prismaStub.selectorState.findUnique.resolves(makeRow({ status: 'pending', regression_count: 1 }));
512
+ yield svc.cancelVerification(ctx);
513
+ (0, chai_1.expect)(sent()).to.deep.equal([
514
+ [SocketEvents_1.SocketEvents.SELECTOR_UNMUTED, KEY],
515
+ [SocketEvents_1.SocketEvents.SELECTOR_UNMUTED, KEY],
516
+ [SocketEvents_1.SocketEvents.SELECTOR_CANCELLED, KEY],
517
+ [SocketEvents_1.SocketEvents.SELECTOR_CANCELLED, KEY],
518
+ ]);
519
+ }));
520
+ it('onHealRecorded', () => __awaiter(void 0, void 0, void 0, function* () {
521
+ prismaStub.selectorState.findUnique.resolves(makeRow({ status: 'pending' }));
522
+ prismaStub.selectorState.update.resolves(makeRow({ status: 'active', regression_count: 1 }));
523
+ yield svc.onHealRecorded({ strategy: STRATEGY, selector: SELECTOR, sessionId: SESSION_ID });
524
+ (0, chai_1.expect)(sent()).to.deep.equal([[SocketEvents_1.SocketEvents.SELECTOR_REGRESSED, KEY]]);
525
+ }));
526
+ it('a heal recorded with no strategy names strategy ""', () => __awaiter(void 0, void 0, void 0, function* () {
527
+ prismaStub.selectorState.findUnique.resolves(null);
528
+ prismaStub.selectorState.upsert.resolves(makeRow({ status: 'muted', original_strategy: '' }));
529
+ yield svc.mute({ strategy: '', selector: SELECTOR, apiKeyId: API_KEY_ID });
530
+ (0, chai_1.expect)(sent()).to.deep.equal([
531
+ [SocketEvents_1.SocketEvents.SELECTOR_MUTED, { strategy: '', selector: SELECTOR }],
532
+ ]);
533
+ }));
534
+ });
486
535
  });
@@ -0,0 +1,130 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ require("reflect-metadata");
16
+ const chai_1 = require("chai");
17
+ const sinon_1 = __importDefault(require("sinon"));
18
+ const scratch_database_1 = require("../helpers/scratch-database");
19
+ const SelectorVerificationJob_1 = require("../../src/services/SelectorVerificationJob");
20
+ const HOUR = 60 * 60 * 1000;
21
+ const ELEMENT = '{"value":{"element-6066-11e4-a52e-4f735466cecf":"e-1"},"sessionId":"s"}';
22
+ const ELEMENTS = '{"value":[{"element-6066-11e4-a52e-4f735466cecf":"e-1"}],"sessionId":"s"}';
23
+ const EMPTY_LIST = '{"value":[],"sessionId":"s"}';
24
+ const NOT_FOUND = '{"value":{"error":"An element could not be located on the page"},"sessionId":"s"}';
25
+ /**
26
+ * A clean build is one where the selector was found without healing. Through
27
+ * 2.14 it was any build with a find of the selector that didn't heal, so a
28
+ * selector whose find failed outright in three builds was verified as fixed.
29
+ * The rows are the ones the dashboard writes (event-manager.afterSessionCommand):
30
+ * a failed find has `is_error` and an error body, an empty findElements has
31
+ * `{"value":[]}`, and the real SQL runs against a scratch database.
32
+ */
33
+ describe('SelectorVerificationJob counts only builds where the selector was found', function () {
34
+ this.timeout(90000);
35
+ const scratch = (0, scratch_database_1.useScratchDatabase)();
36
+ let socket;
37
+ const now = Date.now();
38
+ const fixedAt = new Date(now - 48 * HOUR);
39
+ const builds = ['cb-b1', 'cb-b2', 'cb-b3'];
40
+ before(() => __awaiter(this, void 0, void 0, function* () {
41
+ for (const b of builds) {
42
+ yield scratch.db.build.create({ data: { id: b, name: b } });
43
+ yield scratch.db.session.create({
44
+ data: {
45
+ id: `cb-s-${b}`,
46
+ build_id: b,
47
+ device_udid: 'cb-phone',
48
+ device_platform: 'android',
49
+ device_version: '14',
50
+ desired_capabilities: '{}',
51
+ session_capabilities: '{}',
52
+ node_id: 'localhost',
53
+ has_live_video: false,
54
+ },
55
+ });
56
+ }
57
+ }));
58
+ beforeEach(() => __awaiter(this, void 0, void 0, function* () {
59
+ yield scratch.db.selectorEvent.deleteMany();
60
+ yield scratch.db.selectorState.deleteMany();
61
+ yield scratch.db.sessionLog.deleteMany();
62
+ socket = { emitToDashboardForSelector: sinon_1.default.stub().resolves() };
63
+ yield scratch.db.selectorState.create({
64
+ data: {
65
+ original_strategy: 'xpath',
66
+ original_selector: '//login',
67
+ status: 'pending',
68
+ fixed_at: fixedAt,
69
+ clean_builds_count: 0,
70
+ },
71
+ });
72
+ }));
73
+ const find = (build, command, response, isError = false) => scratch.db.sessionLog.create({
74
+ data: {
75
+ session_id: `cb-s-${build}`,
76
+ command_name: command,
77
+ url: command === 'findElement' ? '/findElement' : '/findElements',
78
+ method: 'POST',
79
+ title: 'Find element',
80
+ response,
81
+ is_success: !isError,
82
+ is_error: isError,
83
+ is_healed: false,
84
+ original_strategy: 'xpath',
85
+ original_selector: '//login',
86
+ createdAt: new Date(now - HOUR),
87
+ },
88
+ });
89
+ const run = () => new SelectorVerificationJob_1.SelectorVerificationJob(scratch.db, socket).run();
90
+ const row = () => scratch.db.selectorState.findUniqueOrThrow({
91
+ where: {
92
+ original_strategy_original_selector: {
93
+ original_strategy: 'xpath',
94
+ original_selector: '//login',
95
+ },
96
+ },
97
+ });
98
+ it('does not count builds where the find failed outright', () => __awaiter(this, void 0, void 0, function* () {
99
+ for (const b of builds)
100
+ yield find(b, 'findElement', NOT_FOUND, true);
101
+ yield run();
102
+ const state = yield row();
103
+ (0, chai_1.expect)(state.status).to.equal('pending');
104
+ (0, chai_1.expect)(state.clean_builds_count).to.equal(0);
105
+ }));
106
+ it('does not count builds where findElements found nothing', () => __awaiter(this, void 0, void 0, function* () {
107
+ for (const b of builds)
108
+ yield find(b, 'findElements', EMPTY_LIST);
109
+ yield run();
110
+ const state = yield row();
111
+ (0, chai_1.expect)(state.status).to.equal('pending');
112
+ (0, chai_1.expect)(state.clean_builds_count).to.equal(0);
113
+ }));
114
+ it('verifies the selector after three builds that found it', () => __awaiter(this, void 0, void 0, function* () {
115
+ yield find('cb-b1', 'findElement', ELEMENT);
116
+ yield find('cb-b2', 'findElements', ELEMENTS);
117
+ yield find('cb-b3', 'findElement', ELEMENT);
118
+ yield run();
119
+ (0, chai_1.expect)((yield row()).status).to.equal('resolved');
120
+ }));
121
+ it('counts a build that found it once, even if another find of it failed', () => __awaiter(this, void 0, void 0, function* () {
122
+ yield find('cb-b1', 'findElement', NOT_FOUND, true);
123
+ yield find('cb-b1', 'findElement', ELEMENT);
124
+ yield find('cb-b2', 'findElement', NOT_FOUND, true);
125
+ yield run();
126
+ const state = yield row();
127
+ (0, chai_1.expect)(state.status).to.equal('pending');
128
+ (0, chai_1.expect)(state.clean_builds_count).to.equal(1);
129
+ }));
130
+ });
@@ -49,7 +49,7 @@ describe('SelectorVerificationJob.run', () => {
49
49
  prismaStub.$transaction = sinon_1.default
50
50
  .stub()
51
51
  .callsFake((fn) => fn(prismaStub));
52
- socketStub = { emitToDashboard: sinon_1.default.stub() };
52
+ socketStub = { emitToDashboardForSelector: sinon_1.default.stub().resolves() };
53
53
  job = new SelectorVerificationJob_1.SelectorVerificationJob(prismaStub, socketStub);
54
54
  });
55
55
  afterEach(() => sinon_1.default.restore());
@@ -65,9 +65,9 @@ describe('SelectorVerificationJob.run', () => {
65
65
  },
66
66
  ]);
67
67
  prismaStub.$queryRaw.resolves([
68
- { build_id: 'b-1', healed: 0 },
69
- { build_id: 'b-2', healed: 0 },
70
- { build_id: 'b-3', healed: 0 },
68
+ { build_id: 'b-1', healed: 0, found: 1 },
69
+ { build_id: 'b-2', healed: 0, found: 1 },
70
+ { build_id: 'b-3', healed: 0, found: 1 },
71
71
  ]);
72
72
  prismaStub.selectorState.update.resolves({
73
73
  id: 'r-1',
@@ -84,8 +84,13 @@ describe('SelectorVerificationJob.run', () => {
84
84
  (0, chai_1.expect)(updateArg.data.status).to.equal('resolved');
85
85
  (0, chai_1.expect)(updateArg.data.resolved_at).to.be.instanceOf(Date);
86
86
  (0, chai_1.expect)(updateArg.data.clean_builds_count).to.equal(3);
87
- (0, chai_1.expect)(socketStub.emitToDashboard.calledOnce).to.be.true;
88
- (0, chai_1.expect)(socketStub.emitToDashboard.firstCall.args[0]).to.equal('selector_resolved');
87
+ (0, chai_1.expect)(socketStub.emitToDashboardForSelector.calledOnce).to.be.true;
88
+ (0, chai_1.expect)(socketStub.emitToDashboardForSelector.firstCall.args[0]).to.equal('selector_resolved');
89
+ // Named, so only the dashboards that may see the selector get it.
90
+ (0, chai_1.expect)(socketStub.emitToDashboardForSelector.firstCall.args[2]).to.deep.equal({
91
+ strategy: 'xpath',
92
+ selector: '//x',
93
+ });
89
94
  }));
90
95
  it('updates clean_builds_count and emits PROGRESS when below 3 and value changed', () => __awaiter(void 0, void 0, void 0, function* () {
91
96
  prismaStub.selectorState.findMany.resolves([
@@ -99,8 +104,8 @@ describe('SelectorVerificationJob.run', () => {
99
104
  },
100
105
  ]);
101
106
  prismaStub.$queryRaw.resolves([
102
- { build_id: 'b-1', healed: 0 },
103
- { build_id: 'b-2', healed: 1 }, // not clean
107
+ { build_id: 'b-1', healed: 0, found: 1 },
108
+ { build_id: 'b-2', healed: 1, found: 1 }, // not clean
104
109
  ]);
105
110
  prismaStub.selectorState.update.resolves({
106
111
  id: 'r-1',
@@ -111,7 +116,11 @@ describe('SelectorVerificationJob.run', () => {
111
116
  const updateArg = prismaStub.selectorState.update.firstCall.args[0];
112
117
  (0, chai_1.expect)(updateArg.data.status).to.be.undefined;
113
118
  (0, chai_1.expect)(updateArg.data.clean_builds_count).to.equal(1);
114
- (0, chai_1.expect)(socketStub.emitToDashboard.firstCall.args[0]).to.equal('selector_progress');
119
+ (0, chai_1.expect)(socketStub.emitToDashboardForSelector.firstCall.args[0]).to.equal('selector_progress');
120
+ (0, chai_1.expect)(socketStub.emitToDashboardForSelector.firstCall.args[2]).to.deep.equal({
121
+ strategy: 'xpath',
122
+ selector: '//x',
123
+ });
115
124
  }));
116
125
  it('does not write or emit when clean_builds_count is unchanged and not promoting', () => __awaiter(void 0, void 0, void 0, function* () {
117
126
  prismaStub.selectorState.findMany.resolves([
@@ -125,12 +134,12 @@ describe('SelectorVerificationJob.run', () => {
125
134
  },
126
135
  ]);
127
136
  prismaStub.$queryRaw.resolves([
128
- { build_id: 'b-1', healed: 0 },
129
- { build_id: 'b-2', healed: 1 },
137
+ { build_id: 'b-1', healed: 0, found: 1 },
138
+ { build_id: 'b-2', healed: 1, found: 1 },
130
139
  ]);
131
140
  yield job.run();
132
141
  (0, chai_1.expect)(prismaStub.selectorState.update.called).to.be.false;
133
- (0, chai_1.expect)(socketStub.emitToDashboard.called).to.be.false;
142
+ (0, chai_1.expect)(socketStub.emitToDashboardForSelector.called).to.be.false;
134
143
  }));
135
144
  it('SQL filters out builds with NULL build_id and only counts findElement(s) commands', () => __awaiter(void 0, void 0, void 0, function* () {
136
145
  prismaStub.selectorState.findMany.resolves([
@@ -160,7 +169,7 @@ describe('SelectorVerificationJob.run', () => {
160
169
  yield job.run();
161
170
  (0, chai_1.expect)(prismaStub.$queryRaw.called).to.be.false;
162
171
  (0, chai_1.expect)(prismaStub.selectorState.update.called).to.be.false;
163
- (0, chai_1.expect)(socketStub.emitToDashboard.called).to.be.false;
172
+ (0, chai_1.expect)(socketStub.emitToDashboardForSelector.called).to.be.false;
164
173
  }));
165
174
  it('continues processing remaining rows if one fails', () => __awaiter(void 0, void 0, void 0, function* () {
166
175
  prismaStub.selectorState.findMany.resolves([
@@ -183,9 +192,9 @@ describe('SelectorVerificationJob.run', () => {
183
192
  ]);
184
193
  prismaStub.$queryRaw.onFirstCall().rejects(new Error('boom'));
185
194
  prismaStub.$queryRaw.onSecondCall().resolves([
186
- { build_id: 'b-1', healed: 0 },
187
- { build_id: 'b-2', healed: 0 },
188
- { build_id: 'b-3', healed: 0 },
195
+ { build_id: 'b-1', healed: 0, found: 1 },
196
+ { build_id: 'b-2', healed: 0, found: 1 },
197
+ { build_id: 'b-3', healed: 0, found: 1 },
189
198
  ]);
190
199
  prismaStub.selectorState.update.resolves({
191
200
  id: 'r-2',
@@ -222,9 +231,9 @@ describe('SelectorVerificationJob.run', () => {
222
231
  },
223
232
  ]);
224
233
  prismaStub.$queryRaw.resolves([
225
- { build_id: 'b-1', healed: 0 },
226
- { build_id: 'b-2', healed: 0 },
227
- { build_id: 'b-3', healed: 0 },
234
+ { build_id: 'b-1', healed: 0, found: 1 },
235
+ { build_id: 'b-2', healed: 0, found: 1 },
236
+ { build_id: 'b-3', healed: 0, found: 1 },
228
237
  ]);
229
238
  prismaStub.selectorState.update.resolves({
230
239
  id: 'r-1',