@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,145 @@
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 NetworkConditioningService_1 = require("../../../src/services/NetworkConditioningService");
19
+ const PhoneNetworkLedger_1 = require("../../../src/services/network/PhoneNetworkLedger");
20
+ const scratch_database_1 = require("../../helpers/scratch-database");
21
+ const fake_adb_1 = require("../../helpers/fake-adb");
22
+ // The live module object, so a spy sees a call made through any import of it.
23
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
24
+ const childProcess = require('child_process');
25
+ const phone = (udid, extra = {}) => (Object.assign({ udid, platform: 'android', deviceType: 'real', host: 'http://127.0.0.1:4723' }, extra));
26
+ const RADIOS_ON = {
27
+ 'shell settings get global wifi_on': '1',
28
+ 'shell settings get global mobile_data': '1',
29
+ };
30
+ describe('NetworkConditioningService', function () {
31
+ this.timeout(60000);
32
+ (0, scratch_database_1.useScratchDatabase)();
33
+ let adb;
34
+ (0, fake_adb_1.useFakeAdb)(() => adb);
35
+ let ledger;
36
+ beforeEach(() => __awaiter(this, void 0, void 0, function* () {
37
+ adb = (0, fake_adb_1.fakeAdb)(Object.assign({}, RADIOS_ON));
38
+ ledger = new PhoneNetworkLedger_1.PhoneNetworkLedger();
39
+ for (const entry of yield ledger.list())
40
+ yield ledger.forget(entry.sessionId);
41
+ }));
42
+ afterEach(() => sinon_1.default.restore());
43
+ describe('Offline on an Android phone', () => {
44
+ it('turns mobile data and Wi-Fi off through the resolved adb, never a shell', () => __awaiter(this, void 0, void 0, function* () {
45
+ const exec = sinon_1.default.spy(childProcess, 'exec');
46
+ const execFile = sinon_1.default.spy(childProcess, 'execFile');
47
+ const svc = new NetworkConditioningService_1.NetworkConditioningService();
48
+ yield svc.applyProfile('s1', phone('R5CT'), 'Offline');
49
+ (0, chai_1.expect)(adb.commands('R5CT')).to.deep.equal([
50
+ 'shell settings get global wifi_on',
51
+ 'shell settings get global mobile_data',
52
+ 'shell svc data disable',
53
+ 'shell svc wifi disable',
54
+ ]);
55
+ (0, chai_1.expect)(adb.calls.every((args) => args[0] === '-s' && args[1] === 'R5CT')).to.equal(true);
56
+ (0, chai_1.expect)(exec.called, 'child_process.exec').to.equal(false);
57
+ (0, chai_1.expect)(execFile.called, 'child_process.execFile').to.equal(false);
58
+ }));
59
+ it('writes down what it changed before changing it, so a restart can undo it', () => __awaiter(this, void 0, void 0, function* () {
60
+ adb.answers['shell settings get global mobile_data'] = '0';
61
+ yield new NetworkConditioningService_1.NetworkConditioningService().applyProfile('s1', phone('R5CT'), 'Offline');
62
+ const entries = yield ledger.list();
63
+ (0, chai_1.expect)(entries).to.have.length(1);
64
+ (0, chai_1.expect)(entries[0]).to.include({ sessionId: 's1', udid: 'R5CT' });
65
+ (0, chai_1.expect)(entries[0].offline).to.deep.equal({ wifiOn: true, dataOn: false });
66
+ }));
67
+ it('puts back what was on, and leaves off what was off', () => __awaiter(this, void 0, void 0, function* () {
68
+ adb.answers['shell settings get global mobile_data'] = '0';
69
+ const svc = new NetworkConditioningService_1.NetworkConditioningService();
70
+ yield svc.applyProfile('s1', phone('R5CT'), 'Offline');
71
+ adb.calls.length = 0;
72
+ yield svc.reset('s1');
73
+ (0, chai_1.expect)(adb.commands('R5CT')).to.deep.equal(['shell svc wifi enable']);
74
+ (0, chai_1.expect)(yield ledger.list()).to.deep.equal([]);
75
+ (0, chai_1.expect)(svc.has('s1')).to.equal(false);
76
+ }));
77
+ it('turns both back on when it could not read what they were', () => __awaiter(this, void 0, void 0, function* () {
78
+ adb.answers['shell settings get global wifi_on'] = new Error('device offline');
79
+ adb.answers['shell settings get global mobile_data'] = 'null';
80
+ const svc = new NetworkConditioningService_1.NetworkConditioningService();
81
+ yield svc.applyProfile('s1', phone('R5CT'), 'Offline');
82
+ adb.calls.length = 0;
83
+ yield svc.reset('s1');
84
+ (0, chai_1.expect)(adb.commands('R5CT')).to.deep.equal([
85
+ 'shell svc data enable',
86
+ 'shell svc wifi enable',
87
+ ]);
88
+ }));
89
+ it('keeps the record when the phone cannot be reached at the end', () => __awaiter(this, void 0, void 0, function* () {
90
+ const svc = new NetworkConditioningService_1.NetworkConditioningService();
91
+ yield svc.applyProfile('s1', phone('R5CT'), 'Offline');
92
+ adb.answers['shell svc data enable'] = new Error("device 'R5CT' not found");
93
+ yield svc.reset('s1');
94
+ const entries = yield ledger.list();
95
+ (0, chai_1.expect)(entries.map((e) => e.sessionId)).to.deep.equal(['s1']);
96
+ (0, chai_1.expect)(svc.has('s1')).to.equal(false);
97
+ }));
98
+ it('undoes a session once, however many times it is asked', () => __awaiter(this, void 0, void 0, function* () {
99
+ const svc = new NetworkConditioningService_1.NetworkConditioningService();
100
+ yield svc.applyProfile('s1', phone('R5CT'), 'Offline');
101
+ adb.calls.length = 0;
102
+ yield Promise.all([svc.reset('s1'), svc.reset('s1')]);
103
+ yield svc.reset('s1');
104
+ (0, chai_1.expect)(adb.commands('R5CT')).to.deep.equal([
105
+ 'shell svc data enable',
106
+ 'shell svc wifi enable',
107
+ ]);
108
+ }));
109
+ });
110
+ it("changes nothing on the phone for 3G, at the start or the end; it's a delay", () => __awaiter(this, void 0, void 0, function* () {
111
+ const svc = new NetworkConditioningService_1.NetworkConditioningService();
112
+ yield svc.applyProfile('s1', phone('R5CT'), '3G');
113
+ (0, chai_1.expect)(svc.getLatency('s1')).to.equal(100);
114
+ yield svc.reset('s1');
115
+ (0, chai_1.expect)(adb.calls).to.deep.equal([]);
116
+ (0, chai_1.expect)(svc.getLatency('s1')).to.equal(0);
117
+ (0, chai_1.expect)(yield ledger.list()).to.deep.equal([]);
118
+ }));
119
+ it('turns the network on for Normal, and has nothing to undo at the end', () => __awaiter(this, void 0, void 0, function* () {
120
+ const svc = new NetworkConditioningService_1.NetworkConditioningService();
121
+ yield svc.applyProfile('s1', phone('R5CT'), 'Normal');
122
+ (0, chai_1.expect)(adb.commands('R5CT')).to.deep.equal(['shell svc data enable', 'shell svc wifi enable']);
123
+ adb.calls.length = 0;
124
+ yield svc.reset('s1');
125
+ (0, chai_1.expect)(adb.calls).to.deep.equal([]);
126
+ (0, chai_1.expect)(yield ledger.list()).to.deep.equal([]);
127
+ }));
128
+ it("doesn't run xcrun for an iOS simulator, and says once that only the delay applies", () => __awaiter(this, void 0, void 0, function* () {
129
+ const exec = sinon_1.default.spy(childProcess, 'exec');
130
+ const execFile = sinon_1.default.spy(childProcess, 'execFile');
131
+ const svc = new NetworkConditioningService_1.NetworkConditioningService();
132
+ const said = sinon_1.default.spy(svc.log, 'info');
133
+ const sim = { udid: 'SIM-1', platform: 'ios', deviceType: 'simulator' };
134
+ yield svc.applyProfile('s1', sim, 'Offline');
135
+ yield svc.applyProfile('s2', Object.assign(Object.assign({}, sim), { udid: 'SIM-2' }), 'Edge');
136
+ yield svc.reset('s1');
137
+ yield svc.reset('s2');
138
+ (0, chai_1.expect)(exec.called).to.equal(false);
139
+ (0, chai_1.expect)(execFile.called).to.equal(false);
140
+ (0, chai_1.expect)(adb.calls).to.deep.equal([]);
141
+ const notes = said.args.filter((a) => /only .*delay/i.test(String(a[0])));
142
+ (0, chai_1.expect)(notes, said.args.map((a) => a[0]).join('\n')).to.have.length(1);
143
+ (0, chai_1.expect)(yield ledger.list()).to.deep.equal([]);
144
+ }));
145
+ });
@@ -0,0 +1,246 @@
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 net_1 = __importDefault(require("net"));
19
+ const typedi_1 = require("typedi");
20
+ const PhoneNetworkRestore_1 = require("../../../src/services/network/PhoneNetworkRestore");
21
+ const PhoneNetworkLedger_1 = require("../../../src/services/network/PhoneNetworkLedger");
22
+ const NetworkConditioningService_1 = require("../../../src/services/NetworkConditioningService");
23
+ const InterceptorService_1 = require("../../../src/services/InterceptorService");
24
+ const PortAllocator_1 = require("../../../src/services/PortAllocator");
25
+ const prisma_1 = require("../../../src/prisma");
26
+ const container_registration_1 = require("../../helpers/container-registration");
27
+ const scratch_database_1 = require("../../helpers/scratch-database");
28
+ const fake_adb_1 = require("../../helpers/fake-adb");
29
+ const DAY = 24 * 60 * 60 * 1000;
30
+ function deferred() {
31
+ let resolve;
32
+ const promise = new Promise((r) => (resolve = r));
33
+ return { promise, resolve };
34
+ }
35
+ /** Stand-ins for the two services that change a phone's network for a session. */
36
+ function fakeServices(live) {
37
+ const net = {
38
+ has: (id) => live.net.has(id),
39
+ sessionIds: () => [...live.net],
40
+ reset: sinon_1.default.stub().callsFake((id) => __awaiter(this, void 0, void 0, function* () { return void live.net.delete(id); })),
41
+ };
42
+ const icp = {
43
+ isActive: (id) => live.icp.has(id),
44
+ sessionIds: () => [...live.icp],
45
+ stop: sinon_1.default.stub().callsFake((id) => __awaiter(this, void 0, void 0, function* () { return void live.icp.delete(id); })),
46
+ };
47
+ return { net, icp };
48
+ }
49
+ describe('PhoneNetworkRestore', function () {
50
+ this.timeout(60000);
51
+ (0, scratch_database_1.useScratchDatabase)();
52
+ let adb;
53
+ (0, fake_adb_1.useFakeAdb)(() => adb);
54
+ let restoreRegs;
55
+ let live;
56
+ let fakes;
57
+ let ledger;
58
+ let range;
59
+ beforeEach(() => __awaiter(this, void 0, void 0, function* () {
60
+ adb = (0, fake_adb_1.fakeAdb)();
61
+ restoreRegs = (0, container_registration_1.saveRegistrations)(NetworkConditioningService_1.NetworkConditioningService, InterceptorService_1.InterceptorService, PortAllocator_1.PortAllocator, PhoneNetworkLedger_1.PhoneNetworkLedger);
62
+ live = { net: new Set(), icp: new Set() };
63
+ fakes = fakeServices(live);
64
+ typedi_1.Container.set(NetworkConditioningService_1.NetworkConditioningService, fakes.net);
65
+ typedi_1.Container.set(InterceptorService_1.InterceptorService, fakes.icp);
66
+ range = [11100, 11199];
67
+ typedi_1.Container.set(PortAllocator_1.PortAllocator, { rangeOf: () => range });
68
+ ledger = new PhoneNetworkLedger_1.PhoneNetworkLedger();
69
+ typedi_1.Container.set(PhoneNetworkLedger_1.PhoneNetworkLedger, ledger);
70
+ for (const entry of yield ledger.list())
71
+ yield ledger.forget(entry.sessionId);
72
+ }));
73
+ afterEach(() => {
74
+ sinon_1.default.restore();
75
+ restoreRegs();
76
+ });
77
+ describe('restoreSession', () => {
78
+ it('resets the profile and stops the interceptor of a session', () => __awaiter(this, void 0, void 0, function* () {
79
+ live.net.add('s1');
80
+ live.icp.add('s1');
81
+ yield new PhoneNetworkRestore_1.PhoneNetworkRestore().restoreSession('s1', 'test');
82
+ (0, chai_1.expect)(fakes.net.reset.calledOnceWith('s1')).to.equal(true);
83
+ (0, chai_1.expect)(fakes.icp.stop.calledOnceWith('s1')).to.equal(true);
84
+ }));
85
+ it('does nothing for a session that changed nothing, or no session', () => __awaiter(this, void 0, void 0, function* () {
86
+ const restore = new PhoneNetworkRestore_1.PhoneNetworkRestore();
87
+ yield restore.restoreSession('s-plain', 'test');
88
+ yield restore.restoreSession(undefined, 'test');
89
+ yield restore.restoreSession(null, 'test');
90
+ (0, chai_1.expect)(fakes.net.reset.called).to.equal(false);
91
+ (0, chai_1.expect)(fakes.icp.stop.called).to.equal(false);
92
+ }));
93
+ it('runs once when two endings race, and both wait until it is done', () => __awaiter(this, void 0, void 0, function* () {
94
+ live.net.add('s1');
95
+ const gate = deferred();
96
+ fakes.net.reset.callsFake((id) => __awaiter(this, void 0, void 0, function* () {
97
+ yield gate.promise;
98
+ live.net.delete(id);
99
+ }));
100
+ const restore = new PhoneNetworkRestore_1.PhoneNetworkRestore();
101
+ let firstDone = false;
102
+ let secondDone = false;
103
+ const first = restore.restoreSession('s1', 'deleted').then(() => (firstDone = true));
104
+ const second = restore.restoreSession('s1', 'idle').then(() => (secondDone = true));
105
+ yield new Promise((r) => setImmediate(r));
106
+ (0, chai_1.expect)(firstDone || secondDone, 'nobody may go on before the network is back').to.equal(false);
107
+ gate.resolve();
108
+ yield Promise.all([first, second]);
109
+ yield restore.restoreSession('s1', 'later');
110
+ (0, chai_1.expect)(fakes.net.reset.callCount).to.equal(1);
111
+ }));
112
+ it("never throws: one service's failure doesn't stop the other's restore", () => __awaiter(this, void 0, void 0, function* () {
113
+ live.net.add('s1');
114
+ live.icp.add('s1');
115
+ fakes.net.reset.rejects(new Error('boom'));
116
+ yield new PhoneNetworkRestore_1.PhoneNetworkRestore().restoreSession('s1', 'test');
117
+ (0, chai_1.expect)(fakes.icp.stop.calledOnceWith('s1')).to.equal(true);
118
+ }));
119
+ });
120
+ it('restoreAll restores every session either service still has', () => __awaiter(this, void 0, void 0, function* () {
121
+ live.net.add('a');
122
+ live.icp.add('a');
123
+ live.icp.add('b');
124
+ yield new PhoneNetworkRestore_1.PhoneNetworkRestore().restoreAll('shutdown');
125
+ (0, chai_1.expect)(fakes.net.reset.args.map((a) => a[0])).to.deep.equal(['a']);
126
+ (0, chai_1.expect)(fakes.icp.stop.args.map((a) => a[0]).sort()).to.deep.equal(['a', 'b']);
127
+ }));
128
+ describe('restoreLeftovers: what an ended run wrote down', () => {
129
+ beforeEach(() => {
130
+ adb.answers['P1 shell settings get global http_proxy'] = '127.0.0.1:11150';
131
+ });
132
+ it('turns back on what was on, and puts back the proxy it set', () => __awaiter(this, void 0, void 0, function* () {
133
+ yield ledger.note('old', 'P1', { offline: { wifiOn: true, dataOn: false } });
134
+ yield ledger.note('old', 'P1', {
135
+ proxy: { set: '127.0.0.1:11150', previous: null, reversePort: 11150 },
136
+ });
137
+ const done = yield new PhoneNetworkRestore_1.PhoneNetworkRestore().restoreLeftovers({});
138
+ (0, chai_1.expect)(done).to.equal(1);
139
+ (0, chai_1.expect)(adb.commands('P1')).to.deep.equal([
140
+ 'shell svc wifi enable',
141
+ 'shell settings get global http_proxy',
142
+ 'shell settings put global http_proxy :0',
143
+ 'reverse --remove tcp:11150',
144
+ ]);
145
+ (0, chai_1.expect)(yield ledger.list()).to.deep.equal([]);
146
+ }));
147
+ it("puts back the phone's own proxy when it had one", () => __awaiter(this, void 0, void 0, function* () {
148
+ yield ledger.note('old', 'P1', {
149
+ proxy: { set: '127.0.0.1:11150', previous: 'proxy.corp:3128' },
150
+ });
151
+ yield new PhoneNetworkRestore_1.PhoneNetworkRestore().restoreLeftovers({});
152
+ (0, chai_1.expect)(adb.commands('P1')).to.include('shell settings put global http_proxy proxy.corp:3128');
153
+ }));
154
+ it('leaves a proxy alone that someone changed since, and forgets it', () => __awaiter(this, void 0, void 0, function* () {
155
+ adb.answers['P1 shell settings get global http_proxy'] = 'proxy.corp:3128';
156
+ yield ledger.note('old', 'P1', { proxy: { set: '127.0.0.1:11150', previous: null } });
157
+ yield new PhoneNetworkRestore_1.PhoneNetworkRestore().restoreLeftovers({});
158
+ (0, chai_1.expect)(adb.commands('P1')).to.deep.equal(['shell settings get global http_proxy']);
159
+ (0, chai_1.expect)(yield ledger.list()).to.deep.equal([]);
160
+ }));
161
+ it('skips a session this server still runs', () => __awaiter(this, void 0, void 0, function* () {
162
+ live.net.add('running');
163
+ yield ledger.note('running', 'P1', { offline: { wifiOn: true, dataOn: true } });
164
+ (0, chai_1.expect)(yield new PhoneNetworkRestore_1.PhoneNetworkRestore().restoreLeftovers({})).to.equal(0);
165
+ (0, chai_1.expect)(adb.calls).to.deep.equal([]);
166
+ (0, chai_1.expect)((yield ledger.list()).map((e) => e.sessionId)).to.deep.equal(['running']);
167
+ }));
168
+ it('touches only the phone it is asked about', () => __awaiter(this, void 0, void 0, function* () {
169
+ yield ledger.note('old-1', 'P1', { offline: { wifiOn: true, dataOn: true } });
170
+ yield ledger.note('old-2', 'P2', { offline: { wifiOn: true, dataOn: true } });
171
+ yield new PhoneNetworkRestore_1.PhoneNetworkRestore().restoreLeftovers({ udid: 'P2' });
172
+ (0, chai_1.expect)(adb.commands('P1')).to.deep.equal([]);
173
+ (0, chai_1.expect)(adb.commands('P2')).to.deep.equal(['shell svc data enable', 'shell svc wifi enable']);
174
+ (0, chai_1.expect)((yield ledger.list()).map((e) => e.sessionId)).to.deep.equal(['old-1']);
175
+ }));
176
+ it('keeps what it could not undo for next time, and gives up after a week', () => __awaiter(this, void 0, void 0, function* () {
177
+ var _a;
178
+ adb.answers['P1 shell svc data enable'] = new Error("device 'P1' not found");
179
+ adb.answers['P9 shell svc data enable'] = new Error("device 'P9' not found");
180
+ yield ledger.note('recent', 'P1', { offline: { wifiOn: true, dataOn: true } });
181
+ yield ledger.note('ancient', 'P9', { offline: { wifiOn: true, dataOn: true } });
182
+ const row = yield prisma_1.prisma.webConfig.findUnique({ where: { name: 'phone-network:ancient' } });
183
+ (0, chai_1.expect)(row, 'the record').to.not.equal(null);
184
+ const value = JSON.parse((_a = row === null || row === void 0 ? void 0 : row.value) !== null && _a !== void 0 ? _a : '{}');
185
+ yield prisma_1.prisma.webConfig.update({
186
+ where: { name: 'phone-network:ancient' },
187
+ data: { value: JSON.stringify(Object.assign(Object.assign({}, value), { at: Date.now() - 8 * DAY })) },
188
+ });
189
+ yield new PhoneNetworkRestore_1.PhoneNetworkRestore().restoreLeftovers({});
190
+ (0, chai_1.expect)((yield ledger.list()).map((e) => e.sessionId)).to.deep.equal(['recent']);
191
+ }));
192
+ });
193
+ describe('cleanUpAtBoot: a proxy left pointing at a dead capture port', () => {
194
+ let server;
195
+ let livePort;
196
+ let deadPort;
197
+ beforeEach(() => __awaiter(this, void 0, void 0, function* () {
198
+ server = net_1.default.createServer((s) => s.destroy());
199
+ yield new Promise((r) => server.listen(0, '127.0.0.1', () => r()));
200
+ livePort = server.address().port;
201
+ const closed = net_1.default.createServer();
202
+ yield new Promise((r) => closed.listen(0, '127.0.0.1', () => r()));
203
+ deadPort = closed.address().port;
204
+ yield new Promise((r) => closed.close(() => r()));
205
+ range = [Math.min(livePort, deadPort), Math.max(livePort, deadPort)];
206
+ }));
207
+ afterEach(() => __awaiter(this, void 0, void 0, function* () {
208
+ yield new Promise((r) => server.close(() => r()));
209
+ }));
210
+ it("clears only a proxy that points at this machine's capture ports where nothing answers", () => __awaiter(this, void 0, void 0, function* () {
211
+ adb.answers['DEAD shell settings get global http_proxy'] = `127.0.0.1:${deadPort}`;
212
+ adb.answers['EMU shell settings get global http_proxy'] = `10.0.2.2:${deadPort}`;
213
+ adb.answers['LIVE shell settings get global http_proxy'] = `127.0.0.1:${livePort}`;
214
+ adb.answers['CORP shell settings get global http_proxy'] = 'proxy.corp:3128';
215
+ adb.answers['NONE shell settings get global http_proxy'] = ':0';
216
+ adb.answers['FAR shell settings get global http_proxy'] = '127.0.0.1:1';
217
+ yield new PhoneNetworkRestore_1.PhoneNetworkRestore().cleanUpAtBoot(['DEAD', 'EMU', 'LIVE', 'CORP', 'NONE', 'FAR']);
218
+ (0, chai_1.expect)(adb.commands('DEAD')).to.deep.equal([
219
+ 'shell settings get global http_proxy',
220
+ 'shell settings put global http_proxy :0',
221
+ `reverse --remove tcp:${deadPort}`,
222
+ ]);
223
+ (0, chai_1.expect)(adb.commands('EMU')).to.deep.equal([
224
+ 'shell settings get global http_proxy',
225
+ 'shell settings put global http_proxy :0',
226
+ ]);
227
+ for (const kept of ['LIVE', 'CORP', 'NONE', 'FAR']) {
228
+ (0, chai_1.expect)(adb.commands(kept), kept).to.deep.equal(['shell settings get global http_proxy']);
229
+ }
230
+ }));
231
+ it('undoes what the ledger holds first, then checks the phones', () => __awaiter(this, void 0, void 0, function* () {
232
+ yield ledger.note('old', 'P1', { offline: { wifiOn: true, dataOn: true } });
233
+ yield new PhoneNetworkRestore_1.PhoneNetworkRestore().cleanUpAtBoot([]);
234
+ (0, chai_1.expect)(adb.commands('P1')).to.deep.equal(['shell svc data enable', 'shell svc wifi enable']);
235
+ }));
236
+ it('never throws, even when a phone cannot be read', () => __awaiter(this, void 0, void 0, function* () {
237
+ adb.answers['shell settings get global http_proxy'] = new Error('unauthorized');
238
+ yield new PhoneNetworkRestore_1.PhoneNetworkRestore().cleanUpAtBoot(['X']);
239
+ }));
240
+ });
241
+ it('can be built by the container', () => {
242
+ restoreRegs();
243
+ restoreRegs = (0, container_registration_1.saveRegistrations)(PhoneNetworkRestore_1.PhoneNetworkRestore);
244
+ (0, chai_1.expect)(() => typedi_1.Container.get(PhoneNetworkRestore_1.PhoneNetworkRestore)).not.to.throw();
245
+ });
246
+ });
@@ -0,0 +1,182 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
36
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
37
+ return new (P || (P = Promise))(function (resolve, reject) {
38
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
39
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
40
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
41
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
42
+ });
43
+ };
44
+ var __importDefault = (this && this.__importDefault) || function (mod) {
45
+ return (mod && mod.__esModule) ? mod : { "default": mod };
46
+ };
47
+ Object.defineProperty(exports, "__esModule", { value: true });
48
+ require("reflect-metadata");
49
+ const chai_1 = require("chai");
50
+ const sinon_1 = __importDefault(require("sinon"));
51
+ const typedi_1 = require("typedi");
52
+ const plugin_1 = require("../../../src/plugin");
53
+ const NodeDevices_1 = __importDefault(require("../../../src/device-managers/NodeDevices"));
54
+ const DeviceService = __importStar(require("../../../src/data-service/device-service"));
55
+ const ActiveLeases = __importStar(require("../../../src/services/lease/activeLeases"));
56
+ const device_utils_1 = require("../../../src/device-utils");
57
+ const SessionLifecycleService_1 = require("../../../src/services/SessionLifecycleService");
58
+ const SessionMetricsService_1 = require("../../../src/services/metrics/SessionMetricsService");
59
+ const OrphanSweeper_1 = require("../../../src/services/OrphanSweeper");
60
+ const ShutdownCoordinator_1 = require("../../../src/services/ShutdownCoordinator");
61
+ const PhoneNetworkRestore_1 = require("../../../src/services/network/PhoneNetworkRestore");
62
+ const event_manager_1 = require("../../../src/dashboard/event-manager");
63
+ const SessionManager_1 = require("../../../src/sessions/SessionManager");
64
+ const prisma_1 = require("../../../src/prisma");
65
+ const container_registration_1 = require("../../helpers/container-registration");
66
+ const scratch_database_1 = require("../../helpers/scratch-database");
67
+ /**
68
+ * A network profile or the interceptor changes the whole phone. Its network
69
+ * was put back only when the test deleted a session Xenon kept in memory.
70
+ * Every other ending (Appium's new-command timeout, Xenon's idle release, a
71
+ * shutdown, a stale heartbeat) left an Offline phone offline and an
72
+ * intercepted phone pointing at a proxy that no longer exists.
73
+ *
74
+ * Each ending now asks PhoneNetworkRestore, keyed by the session id, and
75
+ * before the phone is released: a phone handed to the next session while its
76
+ * network is still being put back could have that session's own profile
77
+ * undone.
78
+ */
79
+ describe('a session’s phone network is put back however the session ends', function () {
80
+ this.timeout(30000);
81
+ const scratch = (0, scratch_database_1.useScratchDatabase)();
82
+ let restoreRegs;
83
+ let order;
84
+ let restoreSession;
85
+ let restoreAll;
86
+ beforeEach(() => {
87
+ restoreRegs = (0, container_registration_1.saveRegistrations)(PhoneNetworkRestore_1.PhoneNetworkRestore, SessionMetricsService_1.SessionMetricsService);
88
+ order = [];
89
+ restoreSession = sinon_1.default.stub().callsFake((id) => __awaiter(this, void 0, void 0, function* () {
90
+ order.push(`restore ${id}`);
91
+ }));
92
+ restoreAll = sinon_1.default.stub().callsFake(() => __awaiter(this, void 0, void 0, function* () {
93
+ order.push('restore all');
94
+ }));
95
+ typedi_1.Container.set(PhoneNetworkRestore_1.PhoneNetworkRestore, { restoreSession, restoreAll });
96
+ typedi_1.Container.set(SessionMetricsService_1.SessionMetricsService, { stop: sinon_1.default.stub().resolves() });
97
+ sinon_1.default.stub(DeviceService, 'releaseSessionDevices').callsFake((id) => __awaiter(this, void 0, void 0, function* () {
98
+ order.push(`release ${id}`);
99
+ }));
100
+ sinon_1.default.stub(event_manager_1.DASHBORD_EVENT_MANAGER, 'onSessionStopped').resolves();
101
+ });
102
+ afterEach(() => {
103
+ sinon_1.default.restore();
104
+ restoreRegs();
105
+ });
106
+ it('when the test deletes a session Xenon keeps no record of', () => __awaiter(this, void 0, void 0, function* () {
107
+ (0, chai_1.expect)(SessionManager_1.SESSION_MANAGER.getSession('s-untracked')).to.equal(undefined);
108
+ const next = sinon_1.default.stub().callsFake(() => __awaiter(this, void 0, void 0, function* () {
109
+ order.push('driver quit');
110
+ }));
111
+ yield typedi_1.Container.get(SessionLifecycleService_1.SessionLifecycleService).deleteSession(next, 's-untracked');
112
+ (0, chai_1.expect)(restoreSession.callCount).to.equal(1);
113
+ (0, chai_1.expect)(order.indexOf('restore s-untracked')).to.be.lessThan(order.indexOf('release s-untracked'));
114
+ }));
115
+ it("when Appium ends the session itself (new-command timeout), on this server's own phone", () => __awaiter(this, void 0, void 0, function* () {
116
+ const plugin = { pluginArgs: {}, xenonLog: { withSession: () => ({ info() { } }) } };
117
+ yield plugin_1.XenonPlugin.prototype.onUnexpectedShutdown.call(plugin, { sessionId: 's-timeout', caps: { udid: 'R5CT' } }, new Error('New command timeout'));
118
+ (0, chai_1.expect)(restoreSession.callCount).to.equal(1);
119
+ (0, chai_1.expect)(restoreSession.firstCall.args[0]).to.equal('s-timeout');
120
+ (0, chai_1.expect)(order).to.deep.equal(['restore s-timeout', 'release s-timeout']);
121
+ }));
122
+ it('when Appium ends the session on a node', () => __awaiter(this, void 0, void 0, function* () {
123
+ const unblock = sinon_1.default.stub(NodeDevices_1.default.prototype, 'unblockDevice').callsFake(() => __awaiter(this, void 0, void 0, function* () {
124
+ order.push('hub unblock');
125
+ return undefined;
126
+ }));
127
+ const plugin = { pluginArgs: { hub: 'http://hub:4723' } };
128
+ yield plugin_1.XenonPlugin.prototype.onUnexpectedShutdown.call(plugin, { sessionId: 's-node', caps: {} }, null);
129
+ (0, chai_1.expect)(unblock.calledOnce).to.equal(true);
130
+ (0, chai_1.expect)(order).to.deep.equal(['restore s-node', 'hub unblock']);
131
+ }));
132
+ it("when Xenon's idle check releases the phone", () => __awaiter(this, void 0, void 0, function* () {
133
+ const idle = {
134
+ udid: 'R5CT',
135
+ host: 'http://127.0.0.1:4723',
136
+ busy: true,
137
+ userBlocked: false,
138
+ session_id: 's-idle',
139
+ claimSessionId: 's-idle',
140
+ lastCmdExecutedAt: Date.now() - 10 * 60 * 1000,
141
+ };
142
+ sinon_1.default.stub(DeviceService, 'getAllDevices').resolves([idle]);
143
+ sinon_1.default.stub(ActiveLeases, 'activeLeasesByDevice').resolves(new Map());
144
+ sinon_1.default.stub(DeviceService, 'releaseSessionDevice').callsFake((_u, _h, id) => __awaiter(this, void 0, void 0, function* () {
145
+ order.push(`release ${id}`);
146
+ return true;
147
+ }));
148
+ yield (0, device_utils_1.releaseBlockedDevices)(60);
149
+ (0, chai_1.expect)(restoreSession.callCount).to.equal(1);
150
+ (0, chai_1.expect)(order).to.deep.equal(['restore s-idle', 'release s-idle']);
151
+ }));
152
+ it('when a graceful shutdown drains a session', () => __awaiter(this, void 0, void 0, function* () {
153
+ yield typedi_1.Container.get(SessionLifecycleService_1.SessionLifecycleService).stopSessionForShutdown('s-down', 'shutdown');
154
+ (0, chai_1.expect)(order.slice(0, 2)).to.deep.equal(['restore s-down', 'release s-down']);
155
+ }));
156
+ it('when a graceful shutdown ends, for sessions Xenon keeps no record of', () => __awaiter(this, void 0, void 0, function* () {
157
+ sinon_1.default.stub(SessionManager_1.SESSION_MANAGER, 'getAllSessions').returns([]);
158
+ yield new ShutdownCoordinator_1.ShutdownCoordinator().drain(1000, 500);
159
+ (0, chai_1.expect)(restoreAll.calledOnce).to.equal(true);
160
+ }));
161
+ it("when a session's heartbeat goes stale", () => __awaiter(this, void 0, void 0, function* () {
162
+ var _a;
163
+ yield scratch.db.session.deleteMany({});
164
+ yield scratch.db.session.create({
165
+ data: {
166
+ id: 's-stale',
167
+ status: 'running',
168
+ desired_capabilities: '{}',
169
+ session_capabilities: '{}',
170
+ node_id: 'n',
171
+ has_live_video: false,
172
+ device_udid: 'R5CT',
173
+ device_platform: 'android',
174
+ device_version: '14',
175
+ last_heartbeat_at: new Date(Date.now() - 60 * 60 * 1000),
176
+ },
177
+ });
178
+ yield new OrphanSweeper_1.OrphanSweeper().sweep({ heartbeatIntervalMs: 1000 });
179
+ (0, chai_1.expect)(order.slice(0, 2)).to.deep.equal(['restore s-stale', 'release s-stale']);
180
+ (0, chai_1.expect)((_a = (yield prisma_1.prisma.session.findUnique({ where: { id: 's-stale' } }))) === null || _a === void 0 ? void 0 : _a.status).to.equal('failed');
181
+ }));
182
+ });