@xenon-device-management/xenon 2.13.2 → 2.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (350) hide show
  1. package/README.md +17 -8
  2. package/lib/package.json +5 -5
  3. package/lib/public/assets/{AnnotationOverlay-BXruClDk.js → AnnotationOverlay-Btq3dZRI.js} +1 -1
  4. package/lib/public/assets/{ApiKeyGate-qTS4LPLC.js → ApiKeyGate-C9OoAgg_.js} +1 -1
  5. package/lib/public/assets/DeviceMosaicView-DBC_xczB.js +2 -0
  6. package/lib/public/assets/{EmptyState-Cr17PdDv.js → EmptyState-DqWU2-vM.js} +1 -1
  7. package/lib/public/assets/{FieldGroup-Bvn08svv.js → FieldGroup-CveQ3xrA.js} +1 -1
  8. package/lib/public/assets/{FilterMenu-DDSO8aDf.js → FilterMenu-BfKDSfHz.js} +1 -1
  9. package/lib/public/assets/{Menu-B-Qn2X3T.js → Menu-CtdCybzA.js} +1 -1
  10. package/lib/public/assets/{Modal-Cg9V5JYU.js → Modal-Bsvj1tm2.js} +1 -1
  11. package/lib/public/assets/{RecordingPage-BTUs9VTW.js → RecordingPage-CRofOiyx.js} +1 -1
  12. package/lib/public/assets/{RecordingsPage-DYcfxITK.js → RecordingsPage-jNQHb2IR.js} +2 -2
  13. package/lib/public/assets/{SegmentedControl-Zy7suhT5.js → SegmentedControl-C8HtTha-.js} +1 -1
  14. package/lib/public/assets/{SettingCard-CWYmycoe.js → SettingCard-D8K1LZNo.js} +1 -1
  15. package/lib/public/assets/{Table-io9fwa98.js → Table-zNTX92Pn.js} +1 -1
  16. package/lib/public/assets/{activity-Bdf_uIWl.js → activity-CJ8LwZq9.js} +1 -1
  17. package/lib/public/assets/ai-settings-CgxQZqrc.js +16 -0
  18. package/lib/public/assets/{api-keys-CPqtVpm3.js → api-keys-ECpo-zTo.js} +1 -1
  19. package/lib/public/assets/{apps-BgZan0H9.js → apps-BjGymyf3.js} +2 -2
  20. package/lib/public/assets/{arrow-left-B27ckB31.js → arrow-left-BWfCbeHP.js} +1 -1
  21. package/lib/public/assets/{arrow-right-DOF8ITCz.js → arrow-right-DC7UGf_n.js} +1 -1
  22. package/lib/public/assets/{arrow-up-right-eFG5KTFS.js → arrow-up-right-BOjwybDk.js} +1 -1
  23. package/lib/public/assets/{auth-shell-DlC3_27j.js → auth-shell-CxMTo9aH.js} +2 -2
  24. package/lib/public/assets/{builds-page-BuXpPfKo.js → builds-page-DOCm4HkA.js} +1 -1
  25. package/lib/public/assets/{button-fIW8owMI.js → button-Cg08-EMT.js} +1 -1
  26. package/lib/public/assets/{calendar-WNjmrF-a.js → calendar-CA1ikLJs.js} +1 -1
  27. package/lib/public/assets/{check-BdTh1Sq7.js → check-BMTaH72W.js} +1 -1
  28. package/lib/public/assets/{chevron-right-B4BKlsqk.js → chevron-right-B2QOv9Nx.js} +1 -1
  29. package/lib/public/assets/{circle-check-zJU80_EF.js → circle-check-CYXBoUqT.js} +1 -1
  30. package/lib/public/assets/{circle-x-5Uwquo0h.js → circle-x-DTwnSAnl.js} +1 -1
  31. package/lib/public/assets/{clock-BtUprDsZ.js → clock-C8_HViQP.js} +1 -1
  32. package/lib/public/assets/{copy-DGSbZI3u.js → copy-Be7LNMXV.js} +1 -1
  33. package/lib/public/assets/derive-UnLRp4r3.js +1 -0
  34. package/lib/public/assets/device-explorer-BOqtfGXz.js +282 -0
  35. package/lib/public/assets/{download-WG6l7BgD.js → download-B56lwxWP.js} +1 -1
  36. package/lib/public/assets/forgot-password-CCU6bzLn.js +1 -0
  37. package/lib/public/assets/h264Stream-C-OtmIuk.js +26 -0
  38. package/lib/public/assets/index-B9KQ6LsY.js +15 -0
  39. package/lib/public/assets/{index-DI1fGIyj.js → index-BOxe43p1.js} +5 -5
  40. package/lib/public/assets/{index-HDQVe1gi.js → index-CfxsUslp.js} +1 -1
  41. package/lib/public/assets/{input-BjDRKCea.js → input-CZHFjbSg.js} +1 -1
  42. package/lib/public/assets/{line-chart-C0zdgd0v.js → line-chart-Gl_Dz3X_.js} +1 -1
  43. package/lib/public/assets/{list-checks-BdW5CjM8.js → list-checks-CCgjeyGY.js} +1 -1
  44. package/lib/public/assets/{lock-Dy3L4I8E.js → lock-CkOwgDcx.js} +1 -1
  45. package/lib/public/assets/{login-BHbY9CLG.js → login-D5cc4w7M.js} +1 -1
  46. package/lib/public/assets/maintenance-settings-t7qr3fvX.js +6 -0
  47. package/lib/public/assets/{monitor-8xjkeHHi.js → monitor-DEJixKQO.js} +1 -1
  48. package/lib/public/assets/{mouse-pointer-2-CXdKMXQ8.js → mouse-pointer-2-CtOrk4M8.js} +1 -1
  49. package/lib/public/assets/{network-Cj8JOKON.js → network-Dii9qdvk.js} +1 -1
  50. package/lib/public/assets/{overview-CTg7F0im.js → overview-rolAACrO.js} +1 -1
  51. package/lib/public/assets/{page-header--aTyJXq6.js → page-header-COQZxJfR.js} +1 -1
  52. package/lib/public/assets/{play-B-FodgHV.js → play-DrN1Se_R.js} +1 -1
  53. package/lib/public/assets/{plus-CsIk10Cq.js → plus-5aq-9uRx.js} +1 -1
  54. package/lib/public/assets/{profile-page-DkFKkMfl.js → profile-page-6i5kY6kF.js} +1 -1
  55. package/lib/public/assets/{reset-password-I2-zqLlI.js → reset-password-sp2IBrc7.js} +1 -1
  56. package/lib/public/assets/runbook-page-DJDjeopU.js +253 -0
  57. package/lib/public/assets/{select-DvCSv25z.js → select-Bpyn7rWO.js} +1 -1
  58. package/lib/public/assets/{selector-detail-redirect-BLVfMJs_.js → selector-detail-redirect-BH74vmKv.js} +1 -1
  59. package/lib/public/assets/{selector-health-page-oqadRokn.js → selector-health-page-Cw_g218H.js} +2 -2
  60. package/lib/public/assets/session-detail-page-AZJE6XXE.js +4 -0
  61. package/lib/public/assets/settings-C3uyGhyc.js +6 -0
  62. package/lib/public/assets/{settings-Dep_3kbA.css → settings-K8UHWMdI.css} +1 -1
  63. package/lib/public/assets/{stat-tile-DV9Ix-4O.js → stat-tile-xVqAKIlI.js} +1 -1
  64. package/lib/public/assets/{tablet-6b_SEN2X.js → tablet-BxUScAeO.js} +1 -1
  65. package/lib/public/assets/{teams-BjZyfuHh.js → teams-B5ljGkY2.js} +1 -1
  66. package/lib/public/assets/{trash-2-CTZpPSdd.js → trash-2-B701i8sy.js} +1 -1
  67. package/lib/public/assets/{upload-P3XzwQUb.js → upload-C2gr44RS.js} +1 -1
  68. package/lib/public/assets/{use-builds-data-1cOCMf-z.js → use-builds-data-NeQOFEoy.js} +1 -1
  69. package/lib/public/assets/{use-password-reset-mode-DgF9w088.js → use-password-reset-mode-DEzJq5ef.js} +1 -1
  70. package/lib/public/assets/{users-DMF9Mk5H.js → users-D_vv_yz4.js} +1 -1
  71. package/lib/public/assets/{users-CiNTKUiz.js → users-MY6eNe6s.js} +1 -1
  72. package/lib/public/assets/{video-off-CHp1KU_T.js → video-off-OK1lNZQn.js} +1 -1
  73. package/lib/public/assets/webhook-settings-BU-C9lBf.js +1 -0
  74. package/lib/public/assets/{zap-DRz0tDq5.js → zap-DO9ry1iz.js} +1 -1
  75. package/lib/public/index.html +1 -1
  76. package/lib/schema.json +23 -25
  77. package/lib/src/XenonCapabilityManager.js +22 -5
  78. package/lib/src/app/dashboardPluginLink.js +117 -0
  79. package/lib/src/app/index.js +13 -34
  80. package/lib/src/app/openapi/control.yaml +73 -20
  81. package/lib/src/app/openapi/grid.yaml +31 -7
  82. package/lib/src/app/openapi/identity.yaml +103 -31
  83. package/lib/src/app/openapi/platform.yaml +165 -39
  84. package/lib/src/app/openapi/sessions.yaml +10 -3
  85. package/lib/src/app/routers/apikeys.js +14 -2
  86. package/lib/src/app/routers/auth.js +29 -5
  87. package/lib/src/app/routers/control.js +20 -2
  88. package/lib/src/app/routers/dashboard.js +69 -26
  89. package/lib/src/app/routers/grid.js +37 -13
  90. package/lib/src/app/routers/profile.js +21 -0
  91. package/lib/src/app/routers/streamType.js +7 -2
  92. package/lib/src/app/routers/users.js +10 -5
  93. package/lib/src/app/routers/webhook.js +8 -4
  94. package/lib/src/app/swagger.js +2 -0
  95. package/lib/src/app/ws/h264StreamWs.js +10 -1
  96. package/lib/src/config.js +51 -5
  97. package/lib/src/dashboard/commands.js +185 -141
  98. package/lib/src/dashboard/event-manager.js +88 -67
  99. package/lib/src/dashboard/services/failure-analysis-service.js +136 -45
  100. package/lib/src/dashboard/sessionCommandSummary.js +39 -0
  101. package/lib/src/data-service/appleFamily.js +43 -0
  102. package/lib/src/data-service/db.js +11 -0
  103. package/lib/src/data-service/device-service.js +6 -1
  104. package/lib/src/data-service/device-store.js +76 -3
  105. package/lib/src/data-service/deviceClaims.js +37 -0
  106. package/lib/src/data-service/deviceFieldOwners.js +25 -11
  107. package/lib/src/data-service/deviceSettings.js +79 -0
  108. package/lib/src/data-service/prisma-store.js +169 -15
  109. package/lib/src/data-service/web-config-service.js +50 -92
  110. package/lib/src/device-managers/AndroidDeviceManager.js +9 -31
  111. package/lib/src/device-managers/android/AndroidH264StreamService.js +69 -20
  112. package/lib/src/device-managers/android/H264Multiplexer.js +38 -4
  113. package/lib/src/device-managers/ios/WDAClient.js +44 -43
  114. package/lib/src/device-managers/shellCommands.js +96 -0
  115. package/lib/src/device-utils.js +114 -40
  116. package/lib/src/gateway/defaultGateway.js +1 -1
  117. package/lib/src/gateway/forwardToNode.js +2 -0
  118. package/lib/src/gateway/healReport.js +87 -0
  119. package/lib/src/gateway/sessionGateway.js +12 -2
  120. package/lib/src/generated/client/edge.js +18 -3
  121. package/lib/src/generated/client/index-browser.js +15 -0
  122. package/lib/src/generated/client/index.d.ts +2086 -379
  123. package/lib/src/generated/client/index.js +18 -3
  124. package/lib/src/generated/client/package.json +1 -1
  125. package/lib/src/generated/client/schema.prisma +38 -7
  126. package/lib/src/generated/client/wasm.js +15 -0
  127. package/lib/src/interceptors/CommandInterceptor.js +271 -188
  128. package/lib/src/interceptors/xenonScripts.js +55 -0
  129. package/lib/src/interfaces/IPluginArgs.js +1 -2
  130. package/lib/src/middleware/authMiddleware.js +15 -3
  131. package/lib/src/middleware/commandCaller.js +3 -3
  132. package/lib/src/middleware/verifyCredential.js +58 -1
  133. package/lib/src/plugin.js +23 -4
  134. package/lib/src/prisma.js +1 -1
  135. package/lib/src/scripts/database-check.js +124 -0
  136. package/lib/src/services/AICommandService.js +9 -2
  137. package/lib/src/services/AIService.js +281 -32
  138. package/lib/src/services/CleanupService.js +6 -1
  139. package/lib/src/services/EmailService.js +29 -0
  140. package/lib/src/services/InterceptorService.js +70 -13
  141. package/lib/src/services/NetworkConditioningService.js +85 -88
  142. package/lib/src/services/NotificationService.js +36 -29
  143. package/lib/src/services/OrphanSweeper.js +7 -0
  144. package/lib/src/services/PortAllocator.js +5 -0
  145. package/lib/src/services/SelectorVerificationJob.js +15 -4
  146. package/lib/src/services/ServerManager.js +105 -9
  147. package/lib/src/services/SessionLifecycleService.js +133 -75
  148. package/lib/src/services/ShutdownCoordinator.js +33 -0
  149. package/lib/src/services/SocketServer.js +19 -3
  150. package/lib/src/services/TeamService.js +20 -3
  151. package/lib/src/services/bug-report/video-slice.js +4 -1
  152. package/lib/src/services/device-access/leaseOverride.js +7 -5
  153. package/lib/src/services/healing/FuzzyXmlHealingProvider.js +51 -0
  154. package/lib/src/services/healing/HealEtalonService.js +3 -2
  155. package/lib/src/services/healing/HealingOrchestrator.js +85 -71
  156. package/lib/src/services/healing/OcrHealingProvider.js +41 -64
  157. package/lib/src/services/healing/ResilioTreeHealingProvider.js +41 -50
  158. package/lib/src/services/healing/VisualAiHealingProvider.js +1 -1
  159. package/lib/src/services/healing/fingerprintIdentity.js +126 -0
  160. package/lib/src/services/healing/resilioPath.js +274 -0
  161. package/lib/src/services/interceptor/AndroidProxyAdapter.js +18 -0
  162. package/lib/src/services/interceptor/interceptorOptions.js +44 -0
  163. package/lib/src/services/logcat/SessionDeviceLogs.js +271 -0
  164. package/lib/src/services/logcat/deviceLogBook.js +163 -0
  165. package/lib/src/services/network/AndroidRadios.js +71 -0
  166. package/lib/src/services/network/PhoneNetworkLedger.js +184 -0
  167. package/lib/src/services/network/PhoneNetworkRestore.js +330 -0
  168. package/lib/src/services/network/adbForPhone.js +60 -0
  169. package/lib/src/services/network/phoneNetworkLock.js +20 -0
  170. package/lib/src/services/network/xenonProxy.js +107 -0
  171. package/lib/src/services/ocr/ocrData.js +118 -0
  172. package/lib/src/services/ocr/vendor/LICENSE-tessdata.txt +202 -0
  173. package/lib/src/services/ocr/vendor/eng.traineddata.gz +0 -0
  174. package/lib/src/services/omni-vision/OmniVisionService.js +132 -95
  175. package/lib/src/services/omni-vision/VisionAssertionService.js +28 -25
  176. package/lib/src/services/omni-vision/ocrTextMatch.js +105 -0
  177. package/lib/src/services/omni-vision/screenScale.js +68 -0
  178. package/lib/src/services/passwordResetLink.js +50 -9
  179. package/lib/src/services/passwordResetMode.js +3 -1
  180. package/lib/src/services/publicUrl.js +45 -0
  181. package/lib/src/services/recording/concurrency-gate.js +12 -3
  182. package/lib/src/services/session/sessionIdentity.js +5 -3
  183. package/lib/src/services/session/shutdownReason.js +26 -0
  184. package/lib/src/services/settings/SelfHealingSwitch.js +70 -0
  185. package/lib/src/services/settings/aiEngineSettings.js +219 -0
  186. package/lib/src/services/settings/labSettings.js +149 -0
  187. package/lib/src/services/token/mintedLifetime.js +51 -0
  188. package/lib/src/services/webhookEvents.js +173 -0
  189. package/lib/src/sessions/SessionManager.js +8 -2
  190. package/lib/src/sessions/sessionMemory.js +25 -0
  191. package/lib/test/helpers/fake-adb.js +58 -0
  192. package/lib/test/helpers/fake-ai-provider.js +188 -0
  193. package/lib/test/helpers/fake-device-table.js +63 -24
  194. package/lib/test/helpers/loki-stores.js +33 -0
  195. package/lib/test/helpers/page-sources.js +164 -0
  196. package/lib/test/helpers/scratch-database.js +58 -13
  197. package/lib/test/integration/audit-ingest.spec.js +10 -10
  198. package/lib/test/integration/auth-flow.spec.js +9 -14
  199. package/lib/test/integration/auth-rate-limit.spec.js +6 -3
  200. package/lib/test/integration/bug-report-route.spec.js +22 -2
  201. package/lib/test/integration/forgot-password-rate-limit.spec.js +6 -3
  202. package/lib/test/integration/forgot-password.spec.js +17 -18
  203. package/lib/test/integration/legacy-deprecation-headers.spec.js +5 -2
  204. package/lib/test/integration/migration.spec.js +6 -1
  205. package/lib/test/integration/node-pair-auth-rest.spec.js +5 -9
  206. package/lib/test/integration/ports-router.spec.js +4 -4
  207. package/lib/test/integration/prisma-device-store-add-devices.spec.js +2 -3
  208. package/lib/test/integration/prisma-device-store-team-filter.spec.js +2 -5
  209. package/lib/test/integration/profile-tokens.spec.js +10 -16
  210. package/lib/test/integration/recordings-router.spec.js +16 -16
  211. package/lib/test/integration/reset-revokes-sessions.spec.js +4 -9
  212. package/lib/test/integration/role-matrix.spec.js +4 -9
  213. package/lib/test/integration/sdk-leases-router.spec.js +9 -9
  214. package/lib/test/integration/sdk-version.spec.js +2 -2
  215. package/lib/test/integration/selector-health-endpoint.spec.js +5 -5
  216. package/lib/test/integration/team-membership-migration.spec.js +73 -12
  217. package/lib/test/integration/team-visibility-apps.spec.js +15 -22
  218. package/lib/test/integration/team-visibility-control.spec.js +96 -69
  219. package/lib/test/integration/team-visibility-dashboard.spec.js +7 -23
  220. package/lib/test/integration/team-visibility-grid.spec.js +16 -15
  221. package/lib/test/integration/team-visibility-leases.spec.js +8 -11
  222. package/lib/test/integration/team-visibility-recordings.spec.js +6 -19
  223. package/lib/test/integration/team-visibility-sessions.spec.js +6 -19
  224. package/lib/test/integration/users-flow.spec.js +7 -12
  225. package/lib/test/integration/users-lockout.spec.js +5 -3
  226. package/lib/test/unit/EmailService.test.js +66 -0
  227. package/lib/test/unit/ResilioTreeHealingProvider.test.js +46 -19
  228. package/lib/test/unit/TeamService.test.js +3 -0
  229. package/lib/test/unit/XenonCapabilityManager.spec.js +4 -2
  230. package/lib/test/unit/ai-call-time-limit.spec.js +130 -0
  231. package/lib/test/unit/ai-engine-settings.spec.js +338 -0
  232. package/lib/test/unit/ai-provider-switch.spec.js +130 -0
  233. package/lib/test/unit/ai-rate-limit.spec.js +126 -0
  234. package/lib/test/unit/android-h264-viewers.spec.js +272 -0
  235. package/lib/test/unit/android-preview-hold.spec.js +3 -2
  236. package/lib/test/unit/apikeys-router.spec.js +84 -0
  237. package/lib/test/unit/apple-family.spec.js +36 -0
  238. package/lib/test/unit/authMiddleware.bearer.spec.js +9 -0
  239. package/lib/test/unit/authMiddleware.credentialLifetime.spec.js +149 -0
  240. package/lib/test/unit/bug-report/video-slice.spec.js +19 -0
  241. package/lib/test/unit/cleanup-settings-db.spec.js +103 -0
  242. package/lib/test/unit/cleanup-settings.spec.js +244 -0
  243. package/lib/test/unit/dashboard-api-paths.spec.js +101 -0
  244. package/lib/test/unit/dashboard-commands.spec.js +249 -0
  245. package/lib/test/unit/dashboard-events-device-logs.spec.js +207 -0
  246. package/lib/test/unit/dashboard-events-team-scope.spec.js +9 -6
  247. package/lib/test/unit/dashboard-plugin-link.spec.js +213 -0
  248. package/lib/test/unit/database-check.spec.js +108 -0
  249. package/lib/test/unit/default-plugin-args.spec.js +33 -0
  250. package/lib/test/unit/device-log-book.spec.js +180 -0
  251. package/lib/test/unit/device-offline-webhook.spec.js +81 -0
  252. package/lib/test/unit/device-setting-columns.spec.js +66 -0
  253. package/lib/test/unit/device-settings-loki.spec.js +87 -0
  254. package/lib/test/unit/device-settings-survive.spec.js +555 -0
  255. package/lib/test/unit/device-sync-lost-update.spec.js +5 -0
  256. package/lib/test/unit/emulator-boot.spec.js +91 -0
  257. package/lib/test/unit/enable-dashboard-option.spec.js +57 -0
  258. package/lib/test/unit/event-log-session-data.spec.js +244 -0
  259. package/lib/test/unit/failure-analysis.spec.js +182 -0
  260. package/lib/test/unit/failure-categories.spec.js +128 -0
  261. package/lib/test/unit/fingerprint-identity.spec.js +433 -0
  262. package/lib/test/unit/h264-multiplexer.spec.js +46 -0
  263. package/lib/test/unit/heal-report.spec.js +71 -0
  264. package/lib/test/unit/healing-instrumentation.spec.js +6 -1
  265. package/lib/test/unit/healing-tier-gate.spec.js +73 -29
  266. package/lib/test/unit/healing-tiers-every-session.spec.js +219 -0
  267. package/lib/test/unit/hub-node-gateway-appium-server.spec.js +219 -0
  268. package/lib/test/unit/hub-node-same-host.spec.js +5 -0
  269. package/lib/test/unit/interceptor-visual-elements.spec.js +192 -0
  270. package/lib/test/unit/iphone-ipad-only.spec.js +132 -0
  271. package/lib/test/unit/json-logging-option.spec.js +74 -0
  272. package/lib/test/unit/lab-settings.spec.js +218 -0
  273. package/lib/test/unit/lease/lease-override.spec.js +23 -6
  274. package/lib/test/unit/lease/session-lease-proof.spec.js +16 -4
  275. package/lib/test/unit/max-sessions.spec.js +240 -0
  276. package/lib/test/unit/ocr-healing-tier-real-ocr.spec.js +113 -0
  277. package/lib/test/unit/ocr-healing-tier.spec.js +122 -0
  278. package/lib/test/unit/ocr-offline.spec.js +213 -0
  279. package/lib/test/unit/ocr-text-match.spec.js +44 -0
  280. package/lib/test/unit/omni-virtual-element-store.spec.js +301 -0
  281. package/lib/test/unit/omni-vision-analyze-screen.spec.js +72 -0
  282. package/lib/test/unit/omni-vision-ios-points.spec.js +159 -0
  283. package/lib/test/unit/omni-vision-tap-text.spec.js +129 -0
  284. package/lib/test/unit/omni-vision.spec.js +6 -8
  285. package/lib/test/unit/package-engines.spec.js +27 -0
  286. package/lib/test/unit/partial-config-file.spec.js +14 -0
  287. package/lib/test/unit/passwordResetMode.test.js +10 -2
  288. package/lib/test/unit/phone-network/boot-clean-up.spec.js +64 -0
  289. package/lib/test/unit/phone-network/interceptor-options.spec.js +77 -0
  290. package/lib/test/unit/phone-network/interceptor-proxy-restore.spec.js +145 -0
  291. package/lib/test/unit/phone-network/network-conditioning.spec.js +145 -0
  292. package/lib/test/unit/phone-network/phone-network-restore.spec.js +246 -0
  293. package/lib/test/unit/phone-network/session-end-paths.spec.js +182 -0
  294. package/lib/test/unit/phone-network/session-start.spec.js +110 -0
  295. package/lib/test/unit/phone-network/xenon-proxy.spec.js +69 -0
  296. package/lib/test/unit/plugin.spec.js +3 -3
  297. package/lib/test/unit/profile-router.test.js +122 -1
  298. package/lib/test/unit/queue-requester.spec.js +1 -1
  299. package/lib/test/unit/recording-options.spec.js +181 -0
  300. package/lib/test/unit/reset-link-address.spec.js +266 -0
  301. package/lib/test/unit/reset-link.test.js +10 -4
  302. package/lib/test/unit/resilio-path-healing.spec.js +270 -0
  303. package/lib/test/unit/selector-learning-every-session.spec.js +179 -0
  304. package/lib/test/unit/selector-verification-clean-build.spec.js +130 -0
  305. package/lib/test/unit/selector-verification-job.spec.js +13 -13
  306. package/lib/test/unit/self-healing-boot.spec.js +80 -0
  307. package/lib/test/unit/self-healing-setting.spec.js +273 -0
  308. package/lib/test/unit/session-app-resolution.spec.js +5 -1
  309. package/lib/test/unit/session-attribution-wiring.spec.js +41 -13
  310. package/lib/test/unit/session-create-credential-owner.spec.js +242 -0
  311. package/lib/test/unit/session-credential-strip.spec.js +1 -1
  312. package/lib/test/unit/session-device-logs.spec.js +347 -0
  313. package/lib/test/unit/session-end-failure-analysis.spec.js +133 -0
  314. package/lib/test/unit/session-failed-webhook.spec.js +170 -0
  315. package/lib/test/unit/session-log-indexes.spec.js +64 -3
  316. package/lib/test/unit/shell-commands.spec.js +133 -0
  317. package/lib/test/unit/socketServer.scopedEmit.spec.js +2 -0
  318. package/lib/test/unit/socketServer.sessionCookie.spec.js +2 -1
  319. package/lib/test/unit/stream-start-pipelines.spec.js +264 -0
  320. package/lib/test/unit/stream-status-type.spec.js +4 -0
  321. package/lib/test/unit/token-mint-limits.spec.js +165 -0
  322. package/lib/test/unit/unexpected-shutdown-reason.spec.js +163 -0
  323. package/lib/test/unit/verify-credential.spec.js +33 -0
  324. package/lib/test/unit/vision-assertion.spec.js +119 -0
  325. package/lib/test/unit/wda-client-shell.spec.js +133 -0
  326. package/lib/test/unit/web-config-service.spec.js +98 -0
  327. package/lib/test/unit/webhook-delivery.spec.js +24 -0
  328. package/lib/test/unit/webhook-payloads.spec.js +273 -0
  329. package/lib/test/unit/xenon-execute-scripts.spec.js +138 -0
  330. package/lib/tsconfig.tsbuildinfo +1 -1
  331. package/lib/web/src/components/webhook-settings/webhookEventVariables.json +30 -0
  332. package/package.json +5 -5
  333. package/prisma/migrations/20261004140000_keep_device_settings/migration.sql +19 -0
  334. package/prisma/migrations/20261004174413_locator_etalon_path/migration.sql +2 -0
  335. package/prisma/schema.prisma +38 -7
  336. package/schema.json +23 -25
  337. package/scripts/generate-types-from-schema.js +1 -2
  338. package/scripts/lib/always-present-plugin-args.js +4 -1
  339. package/lib/public/assets/BugReportButton-BoGhlI7F.js +0 -15
  340. package/lib/public/assets/DeviceMosaicView-8J0oYqMM.js +0 -2
  341. package/lib/public/assets/ai-settings-Bce5SJob.js +0 -21
  342. package/lib/public/assets/device-explorer-C-KnhMHI.js +0 -279
  343. package/lib/public/assets/forgot-password-DnudqFFo.js +0 -1
  344. package/lib/public/assets/maintenance-settings-zDVlOMVK.js +0 -6
  345. package/lib/public/assets/recording-group-store-WVM3hoXn.js +0 -26
  346. package/lib/public/assets/runbook-page-DJTVCJX7.js +0 -124
  347. package/lib/public/assets/session-detail-page-CGn55Pvl.js +0 -4
  348. package/lib/public/assets/settings-MsVU2ukE.js +0 -6
  349. package/lib/public/assets/webhook-settings-DiS8geIm.js +0 -1
  350. /package/lib/public/assets/{BugReportButton-CPBjfiv4.css → index-CPBjfiv4.css} +0 -0
@@ -0,0 +1,347 @@
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 Android 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
+ (0, chai_1.expect)(logs.appliesTo(Object.assign(Object.assign({}, PHONE), { platform: 'ios' }))).to.equal(false);
318
+ (0, chai_1.expect)(logs.appliesTo(Object.assign(Object.assign({}, PHONE), { cloud: true }))).to.equal(false);
319
+ (0, chai_1.expect)(logs.appliesTo(undefined)).to.equal(false);
320
+ });
321
+ it("reads lines as this server's time when the phone's clock can't be read", () => __awaiter(this, void 0, void 0, function* () {
322
+ logs.clock = null;
323
+ const since = Date.now() - 60000;
324
+ yield logs.start({ sessionId, device: PHONE, since });
325
+ logcat.proc.stdout.write(lineAt(since - 600000, 'before'));
326
+ logcat.proc.stdout.write(lineAt(since + 10, 'during'));
327
+ yield settle();
328
+ yield logs.stop(sessionId);
329
+ (0, chai_1.expect)(textOf(yield deviceLogs(sessionId))).to.deep.equal(['during']);
330
+ }));
331
+ it('stops however often it is told to', () => __awaiter(this, void 0, void 0, function* () {
332
+ yield logs.start({ sessionId, device: PHONE, since: Date.now() });
333
+ yield logs.stop(sessionId);
334
+ yield logs.stop(sessionId);
335
+ (0, chai_1.expect)(logs.isRecording(sessionId)).to.equal(false);
336
+ }));
337
+ it('starts nothing when stopped before the phone answered', () => __awaiter(this, void 0, void 0, function* () {
338
+ let answer;
339
+ logs.clock = null;
340
+ logs.readClock = () => new Promise((r) => (answer = r));
341
+ const started = logs.start({ sessionId, device: PHONE, since: Date.now() });
342
+ yield logs.stop(sessionId);
343
+ answer(deviceLogBook_1.UNKNOWN_CLOCK);
344
+ yield started;
345
+ (0, chai_1.expect)(logcat.procs).to.have.length(0);
346
+ }));
347
+ });
@@ -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(2000, 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 < 100 && !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(2000, 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(2000, 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 < 100 && !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
+ });
@@ -0,0 +1,170 @@
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 axios_1 = __importDefault(require("axios"));
18
+ const sinon_1 = __importDefault(require("sinon"));
19
+ const typedi_1 = require("typedi");
20
+ const event_manager_1 = require("../../src/dashboard/event-manager");
21
+ const SessionManager_1 = require("../../src/sessions/SessionManager");
22
+ const SessionMetricsService_1 = require("../../src/services/metrics/SessionMetricsService");
23
+ const SocketServer_1 = require("../../src/services/SocketServer");
24
+ const MetricsService_1 = require("../../src/services/MetricsService");
25
+ const NotificationService_1 = require("../../src/services/NotificationService");
26
+ const device_store_1 = require("../../src/data-service/device-store");
27
+ const SessionStatus_1 = require("../../src/types/SessionStatus");
28
+ const prisma_1 = require("../../src/prisma");
29
+ const container_registration_1 = require("../helpers/container-registration");
30
+ /**
31
+ * `session_failed` goes out when a session ends as failed, whichever way it
32
+ * ends, and once per session.
33
+ *
34
+ * It was sent only from the client's own delete, so a session that timed out
35
+ * for inactivity, whose driver crashed, or whose heartbeat stopped was marked
36
+ * failed on the dashboard and counted in the failure metric, and no webhook
37
+ * heard of it. Every one of those ends through `onSessionStopped`, which is
38
+ * where the final status is decided, so that is where it is sent. A session
39
+ * that ends twice (a crash, then the client's delete) is still sent once.
40
+ */
41
+ describe('session_failed: when it is sent', () => {
42
+ const URL = 'https://hooks.example.com/T000/B000/xyz';
43
+ let restore;
44
+ let post;
45
+ let row;
46
+ let svc;
47
+ beforeEach(() => {
48
+ restore = (0, container_registration_1.saveRegistrations)(SessionMetricsService_1.SessionMetricsService, SocketServer_1.SocketServer, MetricsService_1.MetricsService);
49
+ typedi_1.Container.set(SessionMetricsService_1.SessionMetricsService, { stop: sinon_1.default.stub().resolves() });
50
+ typedi_1.Container.set(SocketServer_1.SocketServer, {
51
+ emitToDashboard: sinon_1.default.stub(),
52
+ emitToDashboardForDevices: sinon_1.default.stub().resolves(),
53
+ hasScopedDashboard: sinon_1.default.stub().returns(false),
54
+ });
55
+ typedi_1.Container.set(MetricsService_1.MetricsService, {
56
+ incrementSessionSuccess: sinon_1.default.stub(),
57
+ incrementSessionFailure: sinon_1.default.stub(),
58
+ });
59
+ sinon_1.default.stub(SessionManager_1.SESSION_MANAGER, 'getSession').returns(undefined);
60
+ sinon_1.default
61
+ .stub(device_store_1.DeviceStoreFactory, 'getStore')
62
+ .returns({ getDevices: () => __awaiter(void 0, void 0, void 0, function* () { return []; }), findDevices: () => __awaiter(void 0, void 0, void 0, function* () { return []; }) });
63
+ sinon_1.default.stub(prisma_1.prisma.sessionLog, 'findFirst').resolves(null);
64
+ row = {
65
+ id: 'sess-1',
66
+ name: 'Checkout flow',
67
+ status: 'running',
68
+ failure_reason: null,
69
+ device_udid: 'R58M123',
70
+ device_name: 'Galaxy S9+',
71
+ device_platform: 'android',
72
+ device_version: '10',
73
+ startTime: new Date('2026-10-04T09:00:00.000Z'),
74
+ endTime: null,
75
+ desired_capabilities: '{"appium:app":"/secret.apk"}',
76
+ };
77
+ sinon_1.default.stub(prisma_1.prisma.session, 'findFirst').callsFake((() => __awaiter(void 0, void 0, void 0, function* () { return (Object.assign({}, row)); })));
78
+ sinon_1.default.stub(prisma_1.prisma.session, 'update').callsFake(((_a) => __awaiter(void 0, [_a], void 0, function* ({ data }) {
79
+ row = Object.assign(Object.assign({}, row), data);
80
+ return row;
81
+ })));
82
+ // A real service with a slack webhook for the event, so what is checked is
83
+ // what would reach the channel.
84
+ svc = typedi_1.Container.get(NotificationService_1.NotificationService);
85
+ post = sinon_1.default.stub(axios_1.default, 'post').resolves({ status: 200 });
86
+ sinon_1.default
87
+ .stub(svc, 'getConfigs')
88
+ .resolves([
89
+ { id: 'w', url: URL, events: '["session_failed"]', type: 'slack', active: true },
90
+ ]);
91
+ // A session id nobody has notified for yet, however many specs ran before.
92
+ row.id = `sess-${Math.random().toString(36).slice(2)}`;
93
+ });
94
+ afterEach(() => {
95
+ sinon_1.default.restore();
96
+ restore();
97
+ });
98
+ /** onSessionStopped fires the webhook without being awaited: let it finish. */
99
+ const settle = () => new Promise((resolve) => setTimeout(resolve, 30));
100
+ it('is sent when a session times out for inactivity (device-utils)', () => __awaiter(void 0, void 0, void 0, function* () {
101
+ yield event_manager_1.DASHBORD_EVENT_MANAGER.onSessionStopped(row.id, 'failed', 'Session timed out due to inactivity');
102
+ yield settle();
103
+ (0, chai_1.expect)(post.calledOnce).to.equal(true);
104
+ const text = post.firstCall.args[1].attachments[0].text;
105
+ (0, chai_1.expect)(text).to.contain(row.id);
106
+ (0, chai_1.expect)(text).to.contain('Session timed out due to inactivity');
107
+ (0, chai_1.expect)(text).to.contain('Galaxy S9+');
108
+ (0, chai_1.expect)(text).not.to.contain('undefined');
109
+ }));
110
+ it('is sent when the driver crashes (plugin onUnexpectedShutdown)', () => __awaiter(void 0, void 0, void 0, function* () {
111
+ yield event_manager_1.DASHBORD_EVENT_MANAGER.onSessionStopped(row.id, SessionStatus_1.SessionStatus.FAILED, 'Driver shut down unexpectedly');
112
+ yield settle();
113
+ (0, chai_1.expect)(post.calledOnce).to.equal(true);
114
+ (0, chai_1.expect)(post.firstCall.args[1].attachments[0].text).to.contain('Driver shut down unexpectedly');
115
+ }));
116
+ it('is sent when the heartbeat stops (OrphanSweeper)', () => __awaiter(void 0, void 0, void 0, function* () {
117
+ yield event_manager_1.DASHBORD_EVENT_MANAGER.onSessionStopped(row.id, SessionStatus_1.SessionStatus.FAILED, 'Session heartbeat timeout');
118
+ yield settle();
119
+ (0, chai_1.expect)(post.calledOnce).to.equal(true);
120
+ }));
121
+ it('is sent when the client deletes a session a command failed in', () => __awaiter(void 0, void 0, void 0, function* () {
122
+ prisma_1.prisma.sessionLog.findFirst.resolves({
123
+ command_name: 'findElement',
124
+ response: '{"value":{"error":"no such element"}}',
125
+ });
126
+ yield event_manager_1.DASHBORD_EVENT_MANAGER.onSessionStopped(row.id);
127
+ yield settle();
128
+ (0, chai_1.expect)(post.calledOnce).to.equal(true);
129
+ (0, chai_1.expect)(post.firstCall.args[1].attachments[0].text).to.contain('no such element');
130
+ }));
131
+ it('is sent when the test marked the session failed and the client then deletes it', () => __awaiter(void 0, void 0, void 0, function* () {
132
+ row.status = 'failed';
133
+ row.failure_reason = 'Assertion failed: total is 12';
134
+ yield event_manager_1.DASHBORD_EVENT_MANAGER.onSessionStopped(row.id);
135
+ yield settle();
136
+ (0, chai_1.expect)(post.calledOnce).to.equal(true);
137
+ (0, chai_1.expect)(post.firstCall.args[1].attachments[0].text).to.contain('Assertion failed: total is 12');
138
+ }));
139
+ it('is not sent for a session that passes', () => __awaiter(void 0, void 0, void 0, function* () {
140
+ yield event_manager_1.DASHBORD_EVENT_MANAGER.onSessionStopped(row.id);
141
+ yield settle();
142
+ (0, chai_1.expect)(row.status).to.equal('success');
143
+ (0, chai_1.expect)(post.called).to.equal(false);
144
+ }));
145
+ it('is sent once when a session ends twice: a crash, then the client’s delete', () => __awaiter(void 0, void 0, void 0, function* () {
146
+ yield event_manager_1.DASHBORD_EVENT_MANAGER.onSessionStopped(row.id, SessionStatus_1.SessionStatus.FAILED, 'Driver shut down unexpectedly');
147
+ yield event_manager_1.DASHBORD_EVENT_MANAGER.onSessionStopped(row.id);
148
+ yield settle();
149
+ (0, chai_1.expect)(post.callCount).to.equal(1);
150
+ }));
151
+ it('is not sent when the server itself is shutting down', () => __awaiter(void 0, void 0, void 0, function* () {
152
+ yield event_manager_1.DASHBORD_EVENT_MANAGER.onSessionStopped(row.id, SessionStatus_1.SessionStatus.FAILED, 'Hub shutdown', {
153
+ notify: false,
154
+ });
155
+ yield settle();
156
+ (0, chai_1.expect)(row.status).to.equal('failed'); // still recorded as failed
157
+ (0, chai_1.expect)(post.called).to.equal(false);
158
+ }));
159
+ it('does not hold the session’s end up for a webhook, and survives one that fails', () => __awaiter(void 0, void 0, void 0, function* () {
160
+ post.rejects(new Error('slack is down'));
161
+ yield event_manager_1.DASHBORD_EVENT_MANAGER.onSessionStopped(row.id, SessionStatus_1.SessionStatus.FAILED, 'boom');
162
+ yield settle();
163
+ (0, chai_1.expect)(row.status).to.equal('failed');
164
+ }));
165
+ it('does not send the session’s capabilities to the webhook', () => __awaiter(void 0, void 0, void 0, function* () {
166
+ yield event_manager_1.DASHBORD_EVENT_MANAGER.onSessionStopped(row.id, SessionStatus_1.SessionStatus.FAILED, 'boom');
167
+ yield settle();
168
+ (0, chai_1.expect)(JSON.stringify(post.firstCall.args[1])).not.to.contain('secret.apk');
169
+ }));
170
+ });