@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
@@ -0,0 +1,356 @@
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 supertest_1 = __importDefault(require("supertest"));
18
+ const stream_1 = require("stream");
19
+ const LogcatStreamService_1 = require("../../src/device-managers/android/LogcatStreamService");
20
+ const PackageResolver_1 = require("../../src/services/logcat/PackageResolver");
21
+ const SessionDeviceLogs_1 = require("../../src/services/logcat/SessionDeviceLogs");
22
+ const deviceLogBook_1 = require("../../src/services/logcat/deviceLogBook");
23
+ const scratch_database_1 = require("../helpers/scratch-database");
24
+ const selector_health_fixture_1 = require("../helpers/selector-health-fixture");
25
+ const pad = (n, w = 2) => String(n).padStart(w, '0');
26
+ // How the Device logs tab reads a line's level for its "Errors only" filter
27
+ // (web log-derive.ts `logLineLevel`, whose own tests hold it to lines in the
28
+ // shape these rows have; the web package can't be imported here).
29
+ const DASHBOARD_THREADTIME = /^\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}\.\d+\s+\d+\s+\d+\s+([VDIWEF])\s/;
30
+ const levelOf = (message) => { var _a, _b; return (_b = (_a = DASHBOARD_THREADTIME.exec(message)) === null || _a === void 0 ? void 0 : _a[1]) !== null && _b !== void 0 ? _b : null; };
31
+ /** A threadtime line logged at `at` by this machine's local clock. */
32
+ function lineAt(at, msg, level = 'I', tag = 'Tag', pid = 4127) {
33
+ const t = new Date(at);
34
+ return (`${pad(t.getMonth() + 1)}-${pad(t.getDate())} ${pad(t.getHours())}:${pad(t.getMinutes())}:` +
35
+ `${pad(t.getSeconds())}.${pad(t.getMilliseconds(), 3)} ${pid} ${pid} ${level} ${tag}: ${msg}\n`);
36
+ }
37
+ /** An `adb logcat` child whose output the test writes. */
38
+ function fakeProc() {
39
+ const stdout = new stream_1.PassThrough();
40
+ const handlers = {};
41
+ return {
42
+ stdout,
43
+ killed: false,
44
+ on(ev, cb) {
45
+ (handlers[ev] || (handlers[ev] = [])).push(cb);
46
+ },
47
+ /** The process ends, as when the phone restarts or is unplugged. */
48
+ exit() {
49
+ stdout.end();
50
+ (handlers.close || []).forEach((h) => h());
51
+ },
52
+ kill() {
53
+ this.killed = true;
54
+ },
55
+ };
56
+ }
57
+ /** The real stream service, its `adb logcat` children fake. */
58
+ class FakeLogcatStreams extends LogcatStreamService_1.LogcatStreamService {
59
+ constructor() {
60
+ super(...arguments);
61
+ this.procs = [];
62
+ }
63
+ spawnLogcat() {
64
+ return __awaiter(this, void 0, void 0, function* () {
65
+ const proc = fakeProc();
66
+ this.procs.push(proc);
67
+ return proc;
68
+ });
69
+ }
70
+ makeResolver() {
71
+ return new PackageResolver_1.PackageResolver(() => __awaiter(this, void 0, void 0, function* () { return ' PID NAME\n 4127 com.example.app\n'; }));
72
+ }
73
+ get proc() {
74
+ return this.procs[this.procs.length - 1];
75
+ }
76
+ }
77
+ class TestDeviceLogs extends SessionDeviceLogs_1.SessionDeviceLogs {
78
+ constructor(logcat) {
79
+ super();
80
+ this.logcat = logcat;
81
+ this.clock = deviceLogBook_1.UNKNOWN_CLOCK;
82
+ /** The size of each write, in order. */
83
+ this.writes = [];
84
+ }
85
+ context() {
86
+ return { pluginArgs: { bindHostOrIp: '127.0.0.1' }, port: 4723, nodeId: 'this-server' };
87
+ }
88
+ streams() {
89
+ return this.logcat;
90
+ }
91
+ readClock() {
92
+ return __awaiter(this, void 0, void 0, function* () {
93
+ return this.clock;
94
+ });
95
+ }
96
+ flushIntervalMs() {
97
+ return 60000;
98
+ }
99
+ retryFirstMs() {
100
+ return 1;
101
+ }
102
+ writeLines(sessionId, lines, first) {
103
+ const _super = Object.create(null, {
104
+ writeLines: { get: () => super.writeLines }
105
+ });
106
+ return __awaiter(this, void 0, void 0, function* () {
107
+ this.writes.push(lines.length);
108
+ yield _super.writeLines.call(this, sessionId, lines, first);
109
+ });
110
+ }
111
+ }
112
+ const PHONE = {
113
+ udid: 'phone-1',
114
+ platform: 'android',
115
+ host: 'http://127.0.0.1:4723',
116
+ nodeId: 'this-server',
117
+ };
118
+ /** Lets readline, the stream's package lookups and the mux deliver what was written. */
119
+ const settle = () => __awaiter(void 0, void 0, void 0, function* () {
120
+ for (let i = 0; i < 5; i++)
121
+ yield new Promise((r) => setImmediate(r));
122
+ });
123
+ const until = (check_1, ...args_1) => __awaiter(void 0, [check_1, ...args_1], void 0, function* (check, ms = 2000) {
124
+ const end = Date.now() + ms;
125
+ while (!check()) {
126
+ if (Date.now() > end)
127
+ throw new Error('timed out');
128
+ yield new Promise((r) => setTimeout(r, 5));
129
+ }
130
+ });
131
+ /** The session's Device logs as the session page gets them. */
132
+ function deviceLogs(sessionId) {
133
+ return __awaiter(this, void 0, void 0, function* () {
134
+ const res = yield (0, supertest_1.default)((0, selector_health_fixture_1.selectorHealthApp)(selector_health_fixture_1.ADMIN)).get(`/session/${sessionId}/logs/device`);
135
+ (0, chai_1.expect)(res.status, res.text).to.equal(200);
136
+ return res.body;
137
+ });
138
+ }
139
+ const textOf = (rows) => rows.map((r) => r.message.replace(/^.*?Tag {5}: /, ''));
140
+ describe("An Android session's Device logs", function () {
141
+ this.timeout(60000);
142
+ const scratch = (0, scratch_database_1.useScratchDatabase)();
143
+ let logcat;
144
+ let logs;
145
+ let sessionId;
146
+ let count = 0;
147
+ beforeEach(() => __awaiter(this, void 0, void 0, function* () {
148
+ count += 1;
149
+ sessionId = `dl-session-${count}`;
150
+ yield scratch.db.session.create({
151
+ data: {
152
+ id: sessionId,
153
+ device_udid: PHONE.udid,
154
+ device_platform: 'android',
155
+ device_version: '10',
156
+ desired_capabilities: '{}',
157
+ session_capabilities: '{}',
158
+ node_id: 'this-server',
159
+ has_live_video: false,
160
+ },
161
+ });
162
+ logcat = new FakeLogcatStreams();
163
+ logs = new TestDeviceLogs(logcat);
164
+ }));
165
+ afterEach(() => __awaiter(this, void 0, void 0, function* () {
166
+ yield logs.stop(sessionId);
167
+ yield logcat.cleanup();
168
+ }));
169
+ it('keeps the lines after the first 100, each once, in order, however many commands ran', () => __awaiter(this, void 0, void 0, function* () {
170
+ const since = Date.now() - 60000;
171
+ yield logs.start({ sessionId, device: PHONE, since });
172
+ // History from before the session, as `logcat -T` sends it first.
173
+ for (let i = 0; i < 20; i++)
174
+ logcat.proc.stdout.write(lineAt(since - 600000 + i, `old ${i}`));
175
+ // 250 lines over the run, a batch between each "command".
176
+ for (let batch = 0; batch < 5; batch++) {
177
+ for (let i = 0; i < 50; i++) {
178
+ const n = batch * 50 + i;
179
+ logcat.proc.stdout.write(lineAt(since + 1000 + n, `line ${n}`));
180
+ }
181
+ yield settle();
182
+ }
183
+ yield logs.stop(sessionId);
184
+ const rows = yield deviceLogs(sessionId);
185
+ (0, chai_1.expect)(textOf(rows)).to.deep.equal(Array.from({ length: 250 }, (_, n) => `line ${n}`));
186
+ }));
187
+ it("keeps each line's level and time, so Errors only finds a crash", () => __awaiter(this, void 0, void 0, function* () {
188
+ const since = Date.now() - 60000;
189
+ yield logs.start({ sessionId, device: PHONE, since });
190
+ const crashAt = since + 30123;
191
+ logcat.proc.stdout.write(lineAt(crashAt - 5, 'Start proc com.example.app', 'I', 'ActivityManager'));
192
+ logcat.proc.stdout.write(lineAt(crashAt, 'FATAL EXCEPTION: main', 'E', 'AndroidRuntime'));
193
+ logcat.proc.stdout.write(lineAt(crashAt, 'Process: com.example.app, PID: 4127', 'E', 'AndroidRuntime'));
194
+ logcat.proc.stdout.write(lineAt(crashAt + 1, 'Sending signal. PID: 4127 SIG: 9', 'I', 'Process'));
195
+ yield settle();
196
+ yield logs.stop(sessionId);
197
+ const rows = yield deviceLogs(sessionId);
198
+ (0, chai_1.expect)(rows.map((r) => levelOf(r.message))).to.deep.equal(['I', 'E', 'E', 'I']);
199
+ (0, chai_1.expect)(rows.map((r) => new Date(r.timestamp).getTime())).to.deep.equal([
200
+ crashAt - 5,
201
+ crashAt,
202
+ crashAt,
203
+ crashAt + 1,
204
+ ]);
205
+ (0, chai_1.expect)(rows[1].message).to.match(/ E AndroidRuntime: FATAL EXCEPTION: main$/);
206
+ }));
207
+ it("keeps a stack trace's lines in order, though they share a millisecond and a write", () => __awaiter(this, void 0, void 0, function* () {
208
+ const since = Date.now() - 60000;
209
+ yield logs.start({ sessionId, device: PHONE, since });
210
+ const at = since + 10000;
211
+ const frames = Array.from({ length: 40 }, (_, i) => `\tat com.example.Frame${i}.run(Frame.java:${i})`);
212
+ for (const frame of frames)
213
+ logcat.proc.stdout.write(lineAt(at, frame, 'E', 'AndroidRuntime'));
214
+ yield settle();
215
+ yield logs.stop(sessionId);
216
+ const rows = yield deviceLogs(sessionId);
217
+ (0, chai_1.expect)(rows.map((r) => r.message.replace(/^.*AndroidRuntime: /, ''))).to.deep.equal(frames);
218
+ // SQLite returns rows of one `createdAt` in the order they were written,
219
+ // and Postgres in any order: each row's `createdAt` is its own.
220
+ const created = rows.map((r) => new Date(r.createdAt).getTime());
221
+ (0, chai_1.expect)(created.every((t, i) => i === 0 || t > created[i - 1]), String(created)).to.equal(true);
222
+ }));
223
+ it('writes as the session runs, in batches, not a row at a time', () => __awaiter(this, void 0, void 0, function* () {
224
+ const since = Date.now() - 60000;
225
+ yield logs.start({ sessionId, device: PHONE, since });
226
+ for (let i = 0; i < SessionDeviceLogs_1.WRITE_BATCH + 10; i++)
227
+ logcat.proc.stdout.write(lineAt(since + i, `line ${i}`));
228
+ yield settle();
229
+ // A full batch is written while the session runs, without waiting for
230
+ // its end or the interval (a minute here).
231
+ const written = () => scratch.db.log.count({ where: { session_id: sessionId } });
232
+ const end = Date.now() + 2000;
233
+ while ((yield written()) < SessionDeviceLogs_1.WRITE_BATCH && Date.now() < end)
234
+ yield settle();
235
+ (0, chai_1.expect)(yield written()).to.be.at.least(SessionDeviceLogs_1.WRITE_BATCH);
236
+ (0, chai_1.expect)(logs.isRecording(sessionId)).to.equal(true);
237
+ yield logs.stop(sessionId);
238
+ (0, chai_1.expect)(yield written()).to.equal(SessionDeviceLogs_1.WRITE_BATCH + 10);
239
+ (0, chai_1.expect)(logs.writes).to.have.length(2);
240
+ (0, chai_1.expect)(Math.max(...logs.writes)).to.equal(SessionDeviceLogs_1.WRITE_BATCH);
241
+ }));
242
+ it('picks up again after the phone restarts, without saving its history twice', () => __awaiter(this, void 0, void 0, function* () {
243
+ const since = Date.now() - 60000;
244
+ yield logs.start({ sessionId, device: PHONE, since });
245
+ const first = logcat.proc;
246
+ first.stdout.write(lineAt(since + 1000, 'before 1'));
247
+ first.stdout.write(lineAt(since + 2000, 'before 2'));
248
+ yield settle();
249
+ first.exit();
250
+ yield until(() => logcat.procs.length === 2);
251
+ // The new stream starts with history the session already has, then a
252
+ // line of the same millisecond it hadn't seen, then new lines.
253
+ logcat.proc.stdout.write(lineAt(since + 1000, 'before 1'));
254
+ logcat.proc.stdout.write(lineAt(since + 2000, 'before 2'));
255
+ logcat.proc.stdout.write(lineAt(since + 2000, 'also at 2'));
256
+ logcat.proc.stdout.write(lineAt(since + 9000, 'after'));
257
+ yield settle();
258
+ yield logs.stop(sessionId);
259
+ (0, chai_1.expect)(textOf(yield deviceLogs(sessionId))).to.deep.equal([
260
+ 'before 1',
261
+ 'before 2',
262
+ "Xenon: The phone's log was interrupted. Lines may be missing from here.",
263
+ 'also at 2',
264
+ 'after',
265
+ ]);
266
+ }));
267
+ it("shares the phone's stream with the Logs viewer, and never stops it", () => __awaiter(this, void 0, void 0, function* () {
268
+ const since = Date.now() - 60000;
269
+ const mux = yield logcat.start(PHONE.udid);
270
+ const viewer = [];
271
+ mux.addClient((r) => viewer.push(r.message), () => true);
272
+ yield logs.start({ sessionId, device: PHONE, since });
273
+ (0, chai_1.expect)(logcat.procs).to.have.length(1);
274
+ logcat.proc.stdout.write(lineAt(since + 1, 'shared'));
275
+ yield settle();
276
+ yield logs.stop(sessionId);
277
+ (0, chai_1.expect)(viewer).to.deep.equal(['shared']);
278
+ (0, chai_1.expect)(logcat.proc.killed).to.equal(false);
279
+ (0, chai_1.expect)(logcat.getMultiplexer(PHONE.udid)).to.equal(mux);
280
+ (0, chai_1.expect)(textOf(yield deviceLogs(sessionId))).to.deep.equal(['shared']);
281
+ }));
282
+ it('keeps the stream from its idle stop while the session runs, and lets it go after', () => __awaiter(this, void 0, void 0, function* () {
283
+ yield logs.start({ sessionId, device: PHONE, since: Date.now() });
284
+ const mux = logcat.getMultiplexer(PHONE.udid);
285
+ (0, chai_1.expect)(mux.emptySince).to.equal(undefined);
286
+ yield logs.stop(sessionId);
287
+ (0, chai_1.expect)(mux.emptySince).to.be.a('number');
288
+ }));
289
+ it(`holds to ${deviceLogBook_1.DEVICE_LOG_LINE_LIMIT} lines, then ${deviceLogBook_1.DEVICE_LOG_ERROR_LIMIT} errors`, () => __awaiter(this, void 0, void 0, function* () {
290
+ const since = Date.now() - 600000;
291
+ yield logs.start({ sessionId, device: PHONE, since });
292
+ const total = deviceLogBook_1.DEVICE_LOG_LINE_LIMIT + 3000;
293
+ let chunk = '';
294
+ for (let i = 0; i < total; i++) {
295
+ // Past the limit, five lines in six are errors: 2,500 errors, 500 others.
296
+ const level = i >= deviceLogBook_1.DEVICE_LOG_LINE_LIMIT && i % 6 !== 0 ? 'E' : 'D';
297
+ chunk += lineAt(since + 1000 + i, `line ${i}`, level);
298
+ if (i % 1000 === 999) {
299
+ logcat.proc.stdout.write(chunk);
300
+ chunk = '';
301
+ yield settle();
302
+ }
303
+ }
304
+ yield settle();
305
+ yield logs.stop(sessionId);
306
+ const rows = yield deviceLogs(sessionId);
307
+ (0, chai_1.expect)(rows).to.have.length(deviceLogBook_1.DEVICE_LOG_LINE_LIMIT + 1 + deviceLogBook_1.DEVICE_LOG_ERROR_LIMIT + 1);
308
+ (0, chai_1.expect)(textOf(rows.slice(0, deviceLogBook_1.DEVICE_LOG_LINE_LIMIT))).to.deep.equal(Array.from({ length: deviceLogBook_1.DEVICE_LOG_LINE_LIMIT }, (_, i) => `line ${i}`));
309
+ (0, chai_1.expect)(rows[deviceLogBook_1.DEVICE_LOG_LINE_LIMIT].message).to.equal('Xenon: This session reached 10,000 device log lines. From here on, only errors are kept.');
310
+ const errors = rows.slice(deviceLogBook_1.DEVICE_LOG_LINE_LIMIT + 1, -1);
311
+ (0, chai_1.expect)(errors.every((r) => levelOf(r.message) === 'E')).to.equal(true);
312
+ (0, chai_1.expect)(rows[rows.length - 1].message).to.equal("Xenon: 1,000 device log lines were left out to keep this session's log within its limit.");
313
+ }));
314
+ it("only records this server's own phones", () => {
315
+ (0, chai_1.expect)(logs.appliesTo(PHONE)).to.equal(true);
316
+ (0, chai_1.expect)(logs.appliesTo(Object.assign(Object.assign({}, PHONE), { nodeId: 'a-node' }))).to.equal(false);
317
+ // An iPhone is recorded from its driver's log (session-device-logs-ios.spec.ts).
318
+ (0, chai_1.expect)(logs.appliesTo(Object.assign(Object.assign({}, PHONE), { platform: 'ios' }))).to.equal(true);
319
+ (0, chai_1.expect)(logs.appliesTo(Object.assign(Object.assign({}, PHONE), { platform: 'windows' }))).to.equal(false);
320
+ (0, chai_1.expect)(logs.appliesTo(Object.assign(Object.assign({}, PHONE), { cloud: true }))).to.equal(false);
321
+ (0, chai_1.expect)(logs.appliesTo(undefined)).to.equal(false);
322
+ });
323
+ it("reads lines as this server's time when the phone's clock can't be read", () => __awaiter(this, void 0, void 0, function* () {
324
+ logs.clock = null;
325
+ const since = Date.now() - 60000;
326
+ yield logs.start({ sessionId, device: PHONE, since });
327
+ logcat.proc.stdout.write(lineAt(since - 600000, 'before'));
328
+ logcat.proc.stdout.write(lineAt(since + 10, 'during'));
329
+ yield settle();
330
+ yield logs.stop(sessionId);
331
+ (0, chai_1.expect)(textOf(yield deviceLogs(sessionId))).to.deep.equal(['during']);
332
+ }));
333
+ it('says in the log when the session turned it off', () => __awaiter(this, void 0, void 0, function* () {
334
+ yield logs.noteOff(sessionId);
335
+ (0, chai_1.expect)((yield deviceLogs(sessionId)).map((r) => r.message)).to.deep.equal([
336
+ "Xenon: This session's device log wasn't kept: the test turned it off.",
337
+ ]);
338
+ (0, chai_1.expect)(logcat.procs).to.have.length(0);
339
+ }));
340
+ it('stops however often it is told to', () => __awaiter(this, void 0, void 0, function* () {
341
+ yield logs.start({ sessionId, device: PHONE, since: Date.now() });
342
+ yield logs.stop(sessionId);
343
+ yield logs.stop(sessionId);
344
+ (0, chai_1.expect)(logs.isRecording(sessionId)).to.equal(false);
345
+ }));
346
+ it('starts nothing when stopped before the phone answered', () => __awaiter(this, void 0, void 0, function* () {
347
+ let answer;
348
+ logs.clock = null;
349
+ logs.readClock = () => new Promise((r) => (answer = r));
350
+ const started = logs.start({ sessionId, device: PHONE, since: Date.now() });
351
+ yield logs.stop(sessionId);
352
+ answer(deviceLogBook_1.UNKNOWN_CLOCK);
353
+ yield started;
354
+ (0, chai_1.expect)(logcat.procs).to.have.length(0);
355
+ }));
356
+ });
@@ -0,0 +1,133 @@
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 event_manager_1 = require("../../src/dashboard/event-manager");
20
+ const SessionManager_1 = require("../../src/sessions/SessionManager");
21
+ const SessionMetricsService_1 = require("../../src/services/metrics/SessionMetricsService");
22
+ const SocketServer_1 = require("../../src/services/SocketServer");
23
+ const MetricsService_1 = require("../../src/services/MetricsService");
24
+ const device_store_1 = require("../../src/data-service/device-store");
25
+ const AIService_1 = require("../../src/services/AIService");
26
+ const failure_analysis_service_1 = require("../../src/dashboard/services/failure-analysis-service");
27
+ const SessionStatus_1 = require("../../src/types/SessionStatus");
28
+ const container_registration_1 = require("../helpers/container-registration");
29
+ const scratch_database_1 = require("../helpers/scratch-database");
30
+ /**
31
+ * Ending a failed session doesn't wait for the AI to explain the failure.
32
+ *
33
+ * `onSessionStopped` awaited the AI call, and every way a session ends awaits
34
+ * `onSessionStopped`: the client's `driver.quit()`, a hub's forwarded DELETE,
35
+ * an inactivity timeout, a shutdown. Only Ollama's calls had a time limit; the
36
+ * OpenAI and Anthropic SDKs wait minutes and retry. So a quit could outlast
37
+ * the client's own timeout. The category (rules, no AI) is still saved before
38
+ * the session's end returns.
39
+ */
40
+ describe('ending a failed session', () => {
41
+ const scratch = (0, scratch_database_1.useScratchDatabase)();
42
+ const ID = 'sess-end-analysis';
43
+ let restore;
44
+ let explain;
45
+ beforeEach(() => __awaiter(void 0, void 0, void 0, function* () {
46
+ restore = (0, container_registration_1.saveRegistrations)(SessionMetricsService_1.SessionMetricsService, SocketServer_1.SocketServer, MetricsService_1.MetricsService);
47
+ typedi_1.Container.set(SessionMetricsService_1.SessionMetricsService, { stop: sinon_1.default.stub().resolves() });
48
+ typedi_1.Container.set(SocketServer_1.SocketServer, {
49
+ emitToDashboard: sinon_1.default.stub(),
50
+ emitToDashboardForDevices: sinon_1.default.stub().resolves(),
51
+ hasScopedDashboard: sinon_1.default.stub().returns(false),
52
+ });
53
+ typedi_1.Container.set(MetricsService_1.MetricsService, {
54
+ incrementSessionSuccess: sinon_1.default.stub(),
55
+ incrementSessionFailure: sinon_1.default.stub(),
56
+ });
57
+ sinon_1.default.stub(SessionManager_1.SESSION_MANAGER, 'getSession').returns(undefined);
58
+ sinon_1.default
59
+ .stub(device_store_1.DeviceStoreFactory, 'getStore')
60
+ .returns({ getDevices: () => __awaiter(void 0, void 0, void 0, function* () { return []; }), findDevices: () => __awaiter(void 0, void 0, void 0, function* () { return []; }) });
61
+ yield scratch.db.log.deleteMany();
62
+ yield scratch.db.sessionLog.deleteMany();
63
+ yield scratch.db.session.deleteMany();
64
+ yield scratch.db.session.create({
65
+ data: {
66
+ id: ID,
67
+ status: 'running',
68
+ desired_capabilities: '{}',
69
+ session_capabilities: '{}',
70
+ node_id: 'node',
71
+ has_live_video: false,
72
+ device_udid: 'R58M123',
73
+ device_platform: 'android',
74
+ device_version: '10',
75
+ },
76
+ });
77
+ // An AI provider that answers when the test says.
78
+ let resolve;
79
+ const promise = new Promise((r) => (resolve = r));
80
+ explain = { resolve, promise };
81
+ sinon_1.default.stub(AIService_1.AI_SERVICE, 'isEnabled').returns(true);
82
+ sinon_1.default.stub(AIService_1.AI_SERVICE, 'analyzeFailure').returns(promise);
83
+ }));
84
+ afterEach(() => __awaiter(void 0, void 0, void 0, function* () {
85
+ // The analysis the session's end started finishes before the stubs go,
86
+ // so it never reaches a real AI provider or database. One still pending
87
+ // gets no answer, so it writes nothing; asking again for the same
88
+ // session waits for the one running.
89
+ explain.resolve(null);
90
+ yield (0, failure_analysis_service_1.explainSessionFailure)(ID);
91
+ sinon_1.default.restore();
92
+ restore();
93
+ }));
94
+ const row = () => scratch.db.session.findUniqueOrThrow({ where: { id: ID } });
95
+ const within = (ms, p) => Promise.race([
96
+ p.then(() => 'ended'),
97
+ new Promise((r) => setTimeout(() => r('still waiting for the AI'), ms)),
98
+ ]);
99
+ const end = () => event_manager_1.DASHBORD_EVENT_MANAGER.onSessionStopped(ID, SessionStatus_1.SessionStatus.FAILED, 'no such element', {
100
+ notify: false,
101
+ });
102
+ it('returns while the AI is still explaining the failure', () => __awaiter(void 0, void 0, void 0, function* () {
103
+ (0, chai_1.expect)(yield within(10000, end())).to.equal('ended');
104
+ // The AI is still asked, once the session has ended.
105
+ const analyzeFailure = AIService_1.AI_SERVICE.analyzeFailure;
106
+ for (let i = 0; i < 500 && !analyzeFailure.called; i++) {
107
+ yield new Promise((r) => setTimeout(r, 20));
108
+ }
109
+ (0, chai_1.expect)(analyzeFailure.calledOnce).to.equal(true);
110
+ (0, chai_1.expect)(analyzeFailure.firstCall.args[0]).to.include({
111
+ sessionId: ID,
112
+ failureReason: 'no such element',
113
+ });
114
+ }));
115
+ it('has filed the session under its category when it returns', () => __awaiter(void 0, void 0, void 0, function* () {
116
+ yield within(10000, end());
117
+ const saved = yield row();
118
+ (0, chai_1.expect)(saved.status).to.equal(SessionStatus_1.SessionStatus.FAILED);
119
+ (0, chai_1.expect)(saved.failure_category).to.equal('ELEMENT_NOT_FOUND');
120
+ (0, chai_1.expect)(saved.ai_analysis).to.equal(null);
121
+ }));
122
+ it("saves the AI's explanation when it comes", () => __awaiter(void 0, void 0, void 0, function* () {
123
+ yield within(10000, end());
124
+ explain.resolve('Root Cause: the Login button was covered by a dialog.');
125
+ let saved = yield row();
126
+ for (let i = 0; i < 500 && !saved.ai_analysis; i++) {
127
+ yield new Promise((r) => setTimeout(r, 20));
128
+ saved = yield row();
129
+ }
130
+ (0, chai_1.expect)(saved.ai_analysis).to.equal('Root Cause: the Login button was covered by a dialog.');
131
+ (0, chai_1.expect)(saved.failure_category).to.equal('ELEMENT_NOT_FOUND');
132
+ }));
133
+ });
@@ -17,6 +17,7 @@ const chai_1 = require("chai");
17
17
  const supertest_1 = __importDefault(require("supertest"));
18
18
  const typedi_1 = require("typedi");
19
19
  const CleanupService_1 = require("../../src/services/CleanupService");
20
+ const SelectorVerificationJob_1 = require("../../src/services/SelectorVerificationJob");
20
21
  const scratch_database_1 = require("../helpers/scratch-database");
21
22
  const selector_health_fixture_1 = require("../helpers/selector-health-fixture");
22
23
  /** The SQL statements `run` makes. */
@@ -30,16 +31,24 @@ function sqlOf(db, run) {
30
31
  /** How SQLite reads `table` in these statements: one line per step. */
31
32
  function stepsOn(db, table, queries) {
32
33
  return __awaiter(this, void 0, void 0, function* () {
33
- var _a;
34
+ var _a, _b;
34
35
  const steps = [];
35
36
  for (const q of queries) {
36
- if (!q.query.includes(`\`${table}\``))
37
+ // Prisma's own SQL quotes names with backticks; a raw query's, with ".
38
+ if (!q.query.includes(`\`${table}\``) && !q.query.includes(`"${table}"`))
37
39
  continue;
38
40
  // SQLite plans without looking at the values, and Prisma's logged
39
41
  // parameters aren't JSON, so every placeholder gets null.
40
42
  const placeholders = ((_a = q.query.match(/\?/g)) !== null && _a !== void 0 ? _a : []).length;
41
43
  const plan = yield db.db.$queryRawUnsafe(`EXPLAIN QUERY PLAN ${q.query}`, ...new Array(placeholders).fill(null));
42
- steps.push(...plan.map((p) => p.detail).filter((d) => new RegExp(`\\b${table}\\b`).test(d)));
44
+ // A raw query may name the table by an alias (`"SessionLog" sl`), which
45
+ // is what SQLite's plan then says.
46
+ const alias = (_b = q.query.match(new RegExp(`"${table}"\\s+(?:AS\\s+)?(\\w+)`, 'i'))) === null || _b === void 0 ? void 0 : _b[1];
47
+ for (const { detail } of plan) {
48
+ const named = alias ? detail.replace(new RegExp(`\\b${alias}\\b`), table) : detail;
49
+ if (new RegExp(`\\b${table}\\b`).test(named))
50
+ steps.push(named);
51
+ }
43
52
  }
44
53
  return steps;
45
54
  });
@@ -145,3 +154,55 @@ describe("A session's logs and profiling are read through an index", function ()
145
154
  }
146
155
  }));
147
156
  });
157
+ /**
158
+ * The selector verification reads every find of a selector being verified
159
+ * since it was marked fixed (SelectorVerificationJob), every 15 minutes, by
160
+ * the selector's own index.
161
+ */
162
+ describe('The selector verification reads SessionLog through an index', function () {
163
+ this.timeout(30000);
164
+ const scratch = (0, scratch_database_1.useScratchDatabase)({ captureQueries: true });
165
+ before(() => __awaiter(this, void 0, void 0, function* () {
166
+ yield scratch.db.build.create({ data: { id: 'sv-build', name: 'sv' } });
167
+ yield scratch.db.session.create({
168
+ data: {
169
+ id: 'sv-session',
170
+ build_id: 'sv-build',
171
+ device_udid: 'sv-phone',
172
+ device_platform: 'android',
173
+ device_version: '14',
174
+ desired_capabilities: '{}',
175
+ session_capabilities: '{}',
176
+ node_id: 'localhost',
177
+ has_live_video: false,
178
+ },
179
+ });
180
+ yield scratch.db.sessionLog.create({
181
+ data: {
182
+ session_id: 'sv-session',
183
+ command_name: 'findElement',
184
+ url: '/findElement',
185
+ method: 'POST',
186
+ title: 'Find element',
187
+ response: '{"value":{"ELEMENT":"e-1"}}',
188
+ original_strategy: 'xpath',
189
+ original_selector: '//sv',
190
+ },
191
+ });
192
+ yield scratch.db.selectorState.create({
193
+ data: {
194
+ original_strategy: 'xpath',
195
+ original_selector: '//sv',
196
+ status: 'pending',
197
+ fixed_at: new Date(Date.now() - 60 * 60 * 1000),
198
+ },
199
+ });
200
+ }));
201
+ it("reads a selector's finds without a scan", () => __awaiter(this, void 0, void 0, function* () {
202
+ const socket = { emitToDashboardForSelector: () => __awaiter(this, void 0, void 0, function* () { return undefined; }) };
203
+ const ran = yield sqlOf(scratch, () => new SelectorVerificationJob_1.SelectorVerificationJob(scratch.db, socket).run());
204
+ const steps = yield stepsOn(scratch, 'SessionLog', ran);
205
+ (0, chai_1.expect)(steps, 'no SessionLog statement was captured').to.not.be.empty;
206
+ (0, chai_1.expect)(scansOf('SessionLog', steps), steps.join('\n')).to.deep.equal([]);
207
+ }));
208
+ });