@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,213 @@
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 express_1 = __importDefault(require("express"));
19
+ const loopbackRequest_1 = __importDefault(require("../helpers/loopbackRequest"));
20
+ const index_1 = require("../../src/app/index");
21
+ const IPluginArgs_1 = require("../../src/interfaces/IPluginArgs");
22
+ const InternalHttpClient_1 = require("../../src/InternalHttpClient");
23
+ const config_1 = require("../../src/config");
24
+ const device_store_1 = require("../../src/data-service/device-store");
25
+ const dashboardPluginLink_1 = require("../../src/app/dashboardPluginLink");
26
+ const grid_1 = __importDefault(require("../../src/app/routers/grid"));
27
+ // The store grid.ts took when it was loaded (it keeps the one it got then).
28
+ const gridStore = device_store_1.DeviceStoreFactory.getStore();
29
+ /**
30
+ * Where Xenon looks for the appium-dashboard-plugin, and the `dashboard_link`
31
+ * it hands back with each device.
32
+ *
33
+ * Through 2.14 both came from the Host header of the first /xenon/api request
34
+ * after startup, before the login (a GET /xenon/api/health was enough):
35
+ * Xenon sent a GET to `<that host>/dashboard/api/ping`, and if the answer had
36
+ * `pong`, kept `<that host>/dashboard` for the life of the process. From then
37
+ * on every device list fetched `<that host>/dashboard/api/sessions`, and gave
38
+ * every user a `dashboard_link` to that host. A forged Host made Xenon call a
39
+ * server of the sender's choosing (blind SSRF) and point everyone at it.
40
+ *
41
+ * Both now come from configuration: the plugin is reached on this server's own
42
+ * address and port, and the link is `/dashboard` under XENON_PUBLIC_URL, or a
43
+ * path on the server the dashboard is open on.
44
+ */
45
+ describe('the dashboard-plugin link never comes from a request', () => {
46
+ const EVIL = 'attacker.example:8080';
47
+ let calls;
48
+ let authDisabledBefore;
49
+ let publicUrlBefore;
50
+ beforeEach(() => __awaiter(void 0, void 0, void 0, function* () {
51
+ authDisabledBefore = config_1.config.authDisabled;
52
+ publicUrlBefore = config_1.config.publicUrl;
53
+ config_1.config.authDisabled = true;
54
+ calls = [];
55
+ sinon_1.default.stub(InternalHttpClient_1.InternalHttpClient, 'get').callsFake(((url) => __awaiter(void 0, void 0, void 0, function* () {
56
+ calls.push(url);
57
+ if (url.endsWith('/dashboard/api/ping'))
58
+ return { pong: true };
59
+ return { result: { rows: [] } };
60
+ })));
61
+ sinon_1.default
62
+ .stub(gridStore, 'getAllDevices')
63
+ .resolves([{ udid: 'phone-1', host: 'http://127.0.0.1:4723', platform: 'android' }]);
64
+ }));
65
+ afterEach(() => {
66
+ config_1.config.authDisabled = authDisabledBefore;
67
+ config_1.config.publicUrl = publicUrlBefore;
68
+ sinon_1.default.restore();
69
+ });
70
+ // This server's own address, as ServerManager passes it from Appium's CLI
71
+ // arguments. InternalHttpClient is stubbed, so nothing is sent anywhere.
72
+ function app() {
73
+ const a = (0, express_1.default)();
74
+ a.use('/xenon', (0, index_1.createRouter)(Object.assign(Object.assign({}, IPluginArgs_1.DefaultPluginArgs), { bindHostOrIp: '127.0.0.1' }), {
75
+ address: '0.0.0.0',
76
+ port: 4999,
77
+ }));
78
+ return a;
79
+ }
80
+ // The old check was a layer ahead of the login, first on the router, so a
81
+ // public request was enough: GET /xenon/api/health with a forged Host made
82
+ // Xenon ping that host. Now a public request makes no outbound call at all.
83
+ it('asks nothing of anyone for a public request, whatever Host it names', () => __awaiter(void 0, void 0, void 0, function* () {
84
+ yield (0, loopbackRequest_1.default)(app()).get('/xenon/api/health').set('Host', EVIL);
85
+ (0, chai_1.expect)(calls).to.deep.equal([]);
86
+ }));
87
+ // Lets the middleware's ping (started by a request, never awaited by one)
88
+ // finish.
89
+ const settle = () => new Promise((resolve) => setImmediate(resolve));
90
+ // The middleware ServerManager installs, with the device list, on a router
91
+ // of their own: the process-wide /xenon router may already answer /device
92
+ // from a layer another spec put ahead of it.
93
+ describe('the device list', () => {
94
+ function gridApp(ownServer) {
95
+ const r = express_1.default.Router();
96
+ r.use((req, _res, next) => {
97
+ req.auth = {
98
+ kind: 'user-session',
99
+ userId: 'u1',
100
+ role: 'SUPER_ADMIN',
101
+ scopes: 'admin',
102
+ };
103
+ next();
104
+ });
105
+ r.use((0, dashboardPluginLink_1.dashboardPluginMiddlewareFor)(ownServer));
106
+ grid_1.default.register(r, Object.assign({}, IPluginArgs_1.DefaultPluginArgs));
107
+ const a = (0, express_1.default)();
108
+ a.use('/xenon/api', r);
109
+ return a;
110
+ }
111
+ /** A first request starts the ping; the second sees what it found. */
112
+ function devices(a) {
113
+ return __awaiter(this, void 0, void 0, function* () {
114
+ yield (0, loopbackRequest_1.default)(a).get('/xenon/api/device').set('Host', EVIL);
115
+ yield settle();
116
+ return (0, loopbackRequest_1.default)(a).get('/xenon/api/device').set('Host', EVIL);
117
+ });
118
+ }
119
+ it("fetches the plugin's sessions from this server, never the request's host", () => __awaiter(void 0, void 0, void 0, function* () {
120
+ yield devices(gridApp({ address: '0.0.0.0', port: 4999 }));
121
+ (0, chai_1.expect)(calls.length, 'the ping and the sessions fetch ran').to.be.greaterThan(1);
122
+ (0, chai_1.expect)(calls[0]).to.equal('http://127.0.0.1:4999/dashboard/api/ping');
123
+ for (const url of calls.slice(1)) {
124
+ (0, chai_1.expect)(url).to.match(/^http:\/\/127\.0\.0\.1:4999\/dashboard\/api\/sessions\?/);
125
+ }
126
+ }));
127
+ it('links a device to /dashboard under XENON_PUBLIC_URL, or on the server it is open on', () => __awaiter(void 0, void 0, void 0, function* () {
128
+ config_1.config.publicUrl = 'https://xenon.example.com';
129
+ const withPublic = yield devices(gridApp({ port: 4999 }));
130
+ const phone = withPublic.body.find((d) => d.udid === 'phone-1');
131
+ (0, chai_1.expect)(phone.dashboard_link).to.match(/^https:\/\/xenon\.example\.com\/dashboard\?device_udid=phone-1&/);
132
+ config_1.config.publicUrl = undefined;
133
+ const without = yield devices(gridApp({ port: 4999 }));
134
+ (0, chai_1.expect)(without.body.find((d) => d.udid === 'phone-1').dashboard_link).to.match(/^\/dashboard\?device_udid=phone-1&/);
135
+ }));
136
+ // Appium serves HTTPS with --ssl-certificate-path and --ssl-key-path. A
137
+ // plain-HTTP ping got ECONNRESET, retried three times (about 4 s) while
138
+ // every signed-in request waited on it, every 30 s once the back-off
139
+ // capped, and the plugin's link was lost.
140
+ it('pings over HTTPS when Appium serves it, once, with a short time limit', () => __awaiter(void 0, void 0, void 0, function* () {
141
+ yield devices(gridApp({ address: '0.0.0.0', port: 4999, tls: true }));
142
+ const ping = InternalHttpClient_1.InternalHttpClient.get.firstCall;
143
+ (0, chai_1.expect)(ping.args[0]).to.equal('https://127.0.0.1:4999/dashboard/api/ping');
144
+ (0, chai_1.expect)(ping.args[1]).to.include({ retry: false, timeout: dashboardPluginLink_1.DASHBOARD_PING_TIMEOUT_MS });
145
+ (0, chai_1.expect)(dashboardPluginLink_1.DASHBOARD_PING_TIMEOUT_MS).to.be.at.most(2000);
146
+ }));
147
+ });
148
+ describe('dashboardPluginMiddleware', () => {
149
+ const run = (mw_1, ...args_1) => __awaiter(void 0, [mw_1, ...args_1], void 0, function* (mw, host = EVIL) {
150
+ const req = { headers: { host }, get: () => host, protocol: 'http' };
151
+ yield new Promise((resolve) => mw(req, {}, resolve));
152
+ return req;
153
+ });
154
+ it('reaches the plugin on the server it is given, whatever the request', () => __awaiter(void 0, void 0, void 0, function* () {
155
+ const got = [];
156
+ const mw = (0, dashboardPluginLink_1.dashboardPluginMiddleware)({
157
+ ownServerOrigin: 'http://127.0.0.1:4723',
158
+ get: (url) => __awaiter(void 0, void 0, void 0, function* () {
159
+ got.push(url);
160
+ return { pong: true };
161
+ }),
162
+ publicBase: () => null,
163
+ });
164
+ yield run(mw);
165
+ yield settle();
166
+ const req = yield run(mw);
167
+ (0, chai_1.expect)(got).to.deep.equal(['http://127.0.0.1:4723/dashboard/api/ping']);
168
+ (0, chai_1.expect)(req['dashboard-plugin-url']).to.equal('http://127.0.0.1:4723/dashboard');
169
+ (0, chai_1.expect)(req['dashboard-plugin-link']).to.equal('/dashboard');
170
+ yield run(mw, 'other.example');
171
+ (0, chai_1.expect)(got, 'pinged once, while it answers').to.have.length(1);
172
+ }));
173
+ // A request never waits for the ping: one the plugin doesn't answer
174
+ // (a hung server, the wrong scheme) would hold every signed-in request.
175
+ it('never holds a request while the ping is out', () => __awaiter(void 0, void 0, void 0, function* () {
176
+ const mw = (0, dashboardPluginLink_1.dashboardPluginMiddleware)({
177
+ ownServerOrigin: 'http://127.0.0.1:4723',
178
+ get: () => new Promise(() => undefined),
179
+ });
180
+ const outcome = yield Promise.race([
181
+ run(mw).then(() => 'answered'),
182
+ new Promise((resolve) => setTimeout(() => resolve('held'), 200)),
183
+ ]);
184
+ (0, chai_1.expect)(outcome).to.equal('answered');
185
+ }));
186
+ it('backs off after a failed ping, and links nothing meanwhile', () => __awaiter(void 0, void 0, void 0, function* () {
187
+ let t = 0;
188
+ const get = sinon_1.default.stub().rejects(new Error('ECONNREFUSED'));
189
+ const mw = (0, dashboardPluginLink_1.dashboardPluginMiddleware)({
190
+ ownServerOrigin: 'http://127.0.0.1:4723',
191
+ get,
192
+ now: () => t,
193
+ });
194
+ yield run(mw);
195
+ yield settle();
196
+ const req = yield run(mw);
197
+ (0, chai_1.expect)(req['dashboard-plugin-url']).to.equal('');
198
+ (0, chai_1.expect)(req['dashboard-plugin-link']).to.equal('');
199
+ (0, chai_1.expect)(get.callCount, 'no second ping inside the back-off').to.equal(1);
200
+ t = 1000;
201
+ yield run(mw);
202
+ (0, chai_1.expect)(get.callCount).to.equal(2);
203
+ }));
204
+ it('finds the server on 127.0.0.1 for a wildcard bind address, over HTTPS when asked', () => {
205
+ (0, chai_1.expect)((0, dashboardPluginLink_1.ownServerOrigin)('0.0.0.0', 4723)).to.equal('http://127.0.0.1:4723');
206
+ (0, chai_1.expect)((0, dashboardPluginLink_1.ownServerOrigin)(undefined, 4723)).to.equal('http://127.0.0.1:4723');
207
+ (0, chai_1.expect)((0, dashboardPluginLink_1.ownServerOrigin)('::', 4723)).to.equal('http://127.0.0.1:4723');
208
+ (0, chai_1.expect)((0, dashboardPluginLink_1.ownServerOrigin)('10.0.0.5', 4723)).to.equal('http://10.0.0.5:4723');
209
+ (0, chai_1.expect)((0, dashboardPluginLink_1.ownServerOrigin)('fe80::1', 4723)).to.equal('http://[fe80::1]:4723');
210
+ (0, chai_1.expect)((0, dashboardPluginLink_1.ownServerOrigin)('0.0.0.0', 4723, true)).to.equal('https://127.0.0.1:4723');
211
+ });
212
+ });
213
+ });
@@ -0,0 +1,108 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const chai_1 = require("chai");
7
+ const sinon_1 = __importDefault(require("sinon"));
8
+ const config_1 = require("../../src/config");
9
+ const logger_1 = __importDefault(require("../../src/logger"));
10
+ const database_check_1 = require("../../src/scripts/database-check");
11
+ // The published plugin ships a database client generated for SQLite. Through
12
+ // 2.13.1, `databaseProvider: postgresql` with a SQLite file URL quietly ran on
13
+ // SQLite (Xenon Control profiles did exactly this), and a postgres URL stopped
14
+ // the server with Prisma's "the URL must start with the protocol `file:`" under
15
+ // a hint about adding a column to a table with rows.
16
+ describe('checkDatabase', () => {
17
+ const sqliteFile = 'file:/Users/me/.cache/xenon/xenon.db';
18
+ const postgresUrl = 'postgresql://xenon:s3cret@db.example.com:5432/xenon';
19
+ it('accepts a SQLite file with a SQLite client and says nothing', () => {
20
+ (0, chai_1.expect)((0, database_check_1.checkDatabase)({ configured: 'sqlite', url: sqliteFile, client: 'sqlite' })).to.deep.equal({});
21
+ });
22
+ it('runs a SQLite file on a SQLite client when configured for postgresql, with a warning', () => {
23
+ const { warning } = (0, database_check_1.checkDatabase)({
24
+ configured: 'postgresql',
25
+ url: sqliteFile,
26
+ client: 'sqlite',
27
+ });
28
+ (0, chai_1.expect)(warning).to.be.a('string');
29
+ (0, chai_1.expect)(warning).to.match(/databaseProvider/);
30
+ (0, chai_1.expect)(warning).to.match(/SQLite/);
31
+ (0, chai_1.expect)(warning).to.include('/Users/me/.cache/xenon/xenon.db');
32
+ });
33
+ it('refuses a postgres URL when the client is built for SQLite, without printing the password', () => {
34
+ (0, chai_1.expect)(() => (0, database_check_1.checkDatabase)({ configured: 'postgresql', url: postgresUrl, client: 'sqlite' }))
35
+ .to.throw(/SQLite/)
36
+ .with.property('message')
37
+ .that.does.not.include('s3cret');
38
+ });
39
+ it('refuses a postgres URL even when databaseProvider says sqlite', () => {
40
+ (0, chai_1.expect)(() => (0, database_check_1.checkDatabase)({ configured: 'sqlite', url: postgresUrl, client: 'sqlite' })).to.throw(/db\.example\.com/);
41
+ });
42
+ it('accepts a postgres URL when the client was generated for PostgreSQL', () => {
43
+ (0, chai_1.expect)((0, database_check_1.checkDatabase)({ configured: 'postgresql', url: postgresUrl, client: 'postgresql' })).to.deep.equal({});
44
+ });
45
+ it('refuses a SQLite file when the client was generated for PostgreSQL', () => {
46
+ (0, chai_1.expect)(() => (0, database_check_1.checkDatabase)({ configured: 'postgresql', url: sqliteFile, client: 'postgresql' })).to.throw(/PostgreSQL/);
47
+ });
48
+ it('refuses a URL it cannot place', () => {
49
+ (0, chai_1.expect)(() => (0, database_check_1.checkDatabase)({ configured: 'sqlite', url: 'mysql://h/db', client: 'sqlite' })).to.throw(/mysql:/);
50
+ });
51
+ });
52
+ describe('clientProviderFromSchema', () => {
53
+ it("reads the datasource's provider, not the generator's", () => {
54
+ const schema = [
55
+ 'generator client {',
56
+ ' provider = "prisma-client-js"',
57
+ '}',
58
+ '',
59
+ 'datasource db {',
60
+ ' provider = "postgresql"',
61
+ ' url = env("DATABASE_URL")',
62
+ '}',
63
+ ].join('\n');
64
+ (0, chai_1.expect)((0, database_check_1.clientProviderFromSchema)(schema)).to.equal('postgresql');
65
+ });
66
+ it('returns null when there is no datasource', () => {
67
+ (0, chai_1.expect)((0, database_check_1.clientProviderFromSchema)('generator client {}')).to.equal(null);
68
+ });
69
+ });
70
+ describe('describeDatabaseUrl', () => {
71
+ it('keeps a SQLite path and drops credentials from a server URL', () => {
72
+ (0, chai_1.expect)((0, database_check_1.describeDatabaseUrl)('file:/data/xenon.db')).to.equal('/data/xenon.db');
73
+ (0, chai_1.expect)((0, database_check_1.describeDatabaseUrl)('postgresql://u:p@h:5432/db?schema=x')).to.equal('postgresql://h:5432/db');
74
+ });
75
+ });
76
+ describe('assertSupportedDatabase', () => {
77
+ let saved;
78
+ beforeEach(() => {
79
+ saved = { provider: config_1.config.databaseProvider, url: config_1.config.databaseUrl };
80
+ });
81
+ afterEach(() => {
82
+ config_1.config.databaseProvider = saved.provider;
83
+ config_1.config.databaseUrl = saved.url;
84
+ sinon_1.default.restore();
85
+ });
86
+ // These read the generated client this repo ships (src/generated/client),
87
+ // which is built for SQLite.
88
+ it('stops a server whose URL is a postgres database', () => {
89
+ config_1.config.databaseProvider = 'postgresql';
90
+ config_1.config.databaseUrl = 'postgresql://xenon:pw@127.0.0.1:5432/xenon';
91
+ (0, chai_1.expect)(() => (0, database_check_1.assertSupportedDatabase)()).to.throw(/built for SQLite/);
92
+ });
93
+ it('starts a postgresql-labelled server on its SQLite file, and says so once', () => {
94
+ const warn = sinon_1.default.stub(logger_1.default, 'warn');
95
+ config_1.config.databaseProvider = 'postgresql';
96
+ config_1.config.databaseUrl = 'file:/tmp/xenon-check.db';
97
+ (0, database_check_1.assertSupportedDatabase)();
98
+ (0, chai_1.expect)(warn.callCount).to.equal(1);
99
+ (0, chai_1.expect)(String(warn.firstCall.args[0])).to.include('/tmp/xenon-check.db');
100
+ });
101
+ it('is silent for the default SQLite setup', () => {
102
+ const warn = sinon_1.default.stub(logger_1.default, 'warn');
103
+ config_1.config.databaseProvider = 'sqlite';
104
+ config_1.config.databaseUrl = 'file:/tmp/xenon-check.db';
105
+ (0, database_check_1.assertSupportedDatabase)();
106
+ (0, chai_1.expect)(warn.called).to.equal(false);
107
+ });
108
+ });
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const chai_1 = require("chai");
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const IPluginArgs_1 = require("../../src/interfaces/IPluginArgs");
10
+ const schema = JSON.parse(fs_1.default.readFileSync(path_1.default.join(__dirname, '..', '..', 'schema.json'), 'utf8'));
11
+ /**
12
+ * Appium fills every default schema.json declares, so a server started the
13
+ * normal way runs on those. DefaultPluginArgs (the block in
14
+ * scripts/generate-types-from-schema.js) is the copy the plugin merges under
15
+ * its options, and used to say 86400000 ms for the health check while the
16
+ * schema, and so the running server, said 300000. A second copy that can
17
+ * disagree is how the Settings page came to show a third number.
18
+ */
19
+ describe('DefaultPluginArgs', () => {
20
+ it('agrees with every default schema.json declares', () => {
21
+ const disagreements = Object.entries(schema.properties)
22
+ .filter(([key, prop]) => 'default' in prop && key in IPluginArgs_1.DefaultPluginArgs)
23
+ .filter(([key, prop]) => IPluginArgs_1.DefaultPluginArgs[key] !== undefined &&
24
+ JSON.stringify(IPluginArgs_1.DefaultPluginArgs[key]) !==
25
+ JSON.stringify(prop.default))
26
+ .map(([key, prop]) => ({
27
+ key,
28
+ schema: prop.default,
29
+ defaultPluginArgs: IPluginArgs_1.DefaultPluginArgs[key],
30
+ }));
31
+ (0, chai_1.expect)(disagreements).to.deep.equal([]);
32
+ });
33
+ });
@@ -0,0 +1,180 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const chai_1 = require("chai");
4
+ const deviceLogBook_1 = require("../../src/services/logcat/deviceLogBook");
5
+ const logcatParse_1 = require("../../src/services/logcat/logcatParse");
6
+ const pad = (n, w = 2) => String(n).padStart(w, '0');
7
+ /** A threadtime line logged at `at` by this machine's local clock. */
8
+ function lineAt(at, level = 'I', msg = 'hello', tag = 'Tag', pid = 4127) {
9
+ const t = new Date(at);
10
+ return (`${pad(t.getMonth() + 1)}-${pad(t.getDate())} ${pad(t.getHours())}:${pad(t.getMinutes())}:` +
11
+ `${pad(t.getSeconds())}.${pad(t.getMilliseconds(), 3)} ${pid} ${pid} ${level} ${tag}: ${msg}`);
12
+ }
13
+ function recAt(at, level = 'I', msg = 'hello', tag = 'Tag') {
14
+ const rec = (0, logcatParse_1.parseThreadtimeLine)(lineAt(at, level, msg, tag), new Date(at));
15
+ if (!rec)
16
+ throw new Error('not a line');
17
+ return rec;
18
+ }
19
+ // The dashboard's own reading of a device log line (web log-derive.ts).
20
+ const DASHBOARD_THREADTIME = /^\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}\.\d+\s+\d+\s+\d+\s+([VDIWEF])\s/;
21
+ // Mid-January, midday: no daylight-saving change within hours of it.
22
+ const T0 = new Date(2026, 0, 15, 12, 0, 0, 0).getTime();
23
+ describe('DeviceLogBook', () => {
24
+ describe('formatThreadtime', () => {
25
+ it('prints a record as logcat printed it, so it parses back the same', () => {
26
+ const line = '01-15 12:00:00.120 4127 4130 E AndroidRuntime: FATAL EXCEPTION: main';
27
+ const rec = (0, logcatParse_1.parseThreadtimeLine)(line, new Date(T0));
28
+ const printed = (0, deviceLogBook_1.formatThreadtime)(rec);
29
+ (0, chai_1.expect)(printed).to.equal(line);
30
+ (0, chai_1.expect)((0, logcatParse_1.parseThreadtimeLine)(printed, new Date(T0))).to.deep.equal(rec);
31
+ });
32
+ it("pads a short tag to logcat's eight columns, and the dashboard reads its level", () => {
33
+ var _a;
34
+ const printed = (0, deviceLogBook_1.formatThreadtime)(recAt(T0, 'F', 'boom', 'DEBUG'));
35
+ (0, chai_1.expect)(printed).to.match(/ F DEBUG {3}: boom$/);
36
+ (0, chai_1.expect)((_a = DASHBOARD_THREADTIME.exec(printed)) === null || _a === void 0 ? void 0 : _a[1]).to.equal('F');
37
+ });
38
+ it('keeps a wrapped message whole', () => {
39
+ const rec = Object.assign(Object.assign({}, recAt(T0)), { message: 'first\nwrapped remainder' });
40
+ (0, chai_1.expect)((0, deviceLogBook_1.formatThreadtime)(rec)).to.match(/Tag {5}: first\nwrapped remainder$/);
41
+ });
42
+ });
43
+ describe('the session window', () => {
44
+ it('keeps lines from shortly before the phone was given to the session, and not before', () => {
45
+ const book = new deviceLogBook_1.DeviceLogBook({ since: T0, clock: deviceLogBook_1.UNKNOWN_CLOCK });
46
+ (0, chai_1.expect)(book.add(recAt(T0 - deviceLogBook_1.WINDOW_SLACK_MS - 1))).to.deep.equal([]);
47
+ (0, chai_1.expect)(book.add(recAt(T0 - deviceLogBook_1.WINDOW_SLACK_MS))).to.have.length(1);
48
+ (0, chai_1.expect)(book.add(recAt(T0 + 60000))).to.have.length(1);
49
+ });
50
+ it('stamps each line with its own time, and keeps the text the phone printed', () => {
51
+ const book = new deviceLogBook_1.DeviceLogBook({ since: T0, clock: deviceLogBook_1.UNKNOWN_CLOCK });
52
+ const [row] = book.add(recAt(T0 + 1234, 'W', 'low memory'));
53
+ (0, chai_1.expect)(row.timestamp.getTime()).to.equal(T0 + 1234);
54
+ (0, chai_1.expect)(row.message).to.equal(lineAt(T0 + 1234, 'W', 'low memory').replace('Tag:', 'Tag :'));
55
+ });
56
+ it('reads a phone in another time zone at the moment it logged, not hours off', () => {
57
+ // The phone is 5 h 30 min ahead: its 17:30 is this server's 12:00.
58
+ const ahead = 5.5 * 3600000;
59
+ const book = new deviceLogBook_1.DeviceLogBook({ since: T0, clock: { zoneShiftMs: -ahead, skewMs: 0 } });
60
+ // Printed an hour before the session by the phone's clock: left out.
61
+ (0, chai_1.expect)(book.add(recAt(T0 + ahead - 3600000))).to.deep.equal([]);
62
+ const [row] = book.add(recAt(T0 + ahead + 5000));
63
+ (0, chai_1.expect)(row.timestamp.getTime()).to.equal(T0 + 5000);
64
+ // The text is the phone's own, as it printed it.
65
+ (0, chai_1.expect)(row.message).to.match(/^01-15 17:30:05\.000 /);
66
+ });
67
+ it("cuts and stamps by this server's clock when the phone's runs behind", () => {
68
+ // The phone's clock is 40 s slow: a line it stamps 30 s before the
69
+ // session came 10 s after the session began, and one it stamps 43 s
70
+ // before came 3 s before.
71
+ const book = new deviceLogBook_1.DeviceLogBook({ since: T0, clock: { zoneShiftMs: 0, skewMs: 40000 } });
72
+ (0, chai_1.expect)(book.add(recAt(T0 - 43000))).to.deep.equal([]);
73
+ const [row] = book.add(recAt(T0 - 30000));
74
+ (0, chai_1.expect)(row.timestamp.getTime()).to.equal(T0 + 10000);
75
+ // The text keeps the time the phone printed.
76
+ (0, chai_1.expect)(row.message).to.match(/^01-15 11:59:30\.000 /);
77
+ });
78
+ it("ignores the stream's own notes", () => {
79
+ const book = new deviceLogBook_1.DeviceLogBook({ since: T0, clock: deviceLogBook_1.UNKNOWN_CLOCK });
80
+ (0, chai_1.expect)(book.add(Object.assign(Object.assign({}, recAt(T0)), { tag: 'xenon', synthetic: true }))).to.deep.equal([]);
81
+ });
82
+ });
83
+ describe('after an interruption', () => {
84
+ it('counts only lines newer than the last it saw, by content at that millisecond', () => {
85
+ const book = new deviceLogBook_1.DeviceLogBook({ since: T0, clock: deviceLogBook_1.UNKNOWN_CLOCK });
86
+ book.add(recAt(T0 + 1000, 'I', 'one'));
87
+ book.add(recAt(T0 + 2000, 'E', 'stack line 1'));
88
+ book.add(recAt(T0 + 2000, 'E', 'stack line 2'));
89
+ const [note] = book.interrupted(new Date(T0 + 3000));
90
+ (0, chai_1.expect)(note.message).to.equal("Xenon: The phone's log was interrupted. Lines may be missing from here.");
91
+ // The new stream's history repeats what was kept...
92
+ (0, chai_1.expect)(book.add(recAt(T0 + 1000, 'I', 'one'))).to.deep.equal([]);
93
+ (0, chai_1.expect)(book.add(recAt(T0 + 2000, 'E', 'stack line 1'))).to.deep.equal([]);
94
+ (0, chai_1.expect)(book.add(recAt(T0 + 2000, 'E', 'stack line 2'))).to.deep.equal([]);
95
+ // ...and has a line of that same millisecond that hadn't arrived yet.
96
+ (0, chai_1.expect)(book.add(recAt(T0 + 2000, 'E', 'stack line 3'))).to.have.length(1);
97
+ (0, chai_1.expect)(book.add(recAt(T0 + 4000, 'I', 'after'))).to.have.length(1);
98
+ (0, chai_1.expect)(book.keptLines).to.equal(5);
99
+ });
100
+ it('keeps the session window when nothing had arrived yet', () => {
101
+ const book = new deviceLogBook_1.DeviceLogBook({ since: T0, clock: deviceLogBook_1.UNKNOWN_CLOCK });
102
+ book.interrupted(new Date(T0));
103
+ (0, chai_1.expect)(book.add(recAt(T0 - 60000))).to.deep.equal([]);
104
+ (0, chai_1.expect)(book.add(recAt(T0))).to.have.length(1);
105
+ });
106
+ });
107
+ describe('the limit', () => {
108
+ it('keeps every line up to the limit, then errors only, then nothing, and says so', () => {
109
+ const book = new deviceLogBook_1.DeviceLogBook({
110
+ since: T0,
111
+ clock: deviceLogBook_1.UNKNOWN_CLOCK,
112
+ lineLimit: 3,
113
+ errorLimit: 2,
114
+ });
115
+ const kept = [];
116
+ const add = (level, msg, at) => kept.push(...book.add(recAt(T0 + at, level, msg)).map((r) => r.message));
117
+ add('D', 'a', 1);
118
+ add('I', 'b', 2);
119
+ add('W', 'c', 3);
120
+ add('I', 'chatter', 4); // past the limit: left out, after the note
121
+ add('E', 'FATAL EXCEPTION: main', 5);
122
+ add('D', 'more chatter', 6);
123
+ add('F', 'abort', 7);
124
+ add('E', 'one error too many', 8);
125
+ (0, chai_1.expect)(kept.map((m) => m.replace(/^.*Tag {5}: /, ''))).to.deep.equal([
126
+ 'a',
127
+ 'b',
128
+ 'c',
129
+ 'Xenon: This session reached 3 device log lines. From here on, only errors are kept.',
130
+ 'FATAL EXCEPTION: main',
131
+ 'abort',
132
+ ]);
133
+ (0, chai_1.expect)(book.finish(new Date(T0 + 9)).map((r) => r.message)).to.deep.equal([
134
+ "Xenon: 3 device log lines were left out to keep this session's log within its limit.",
135
+ ]);
136
+ });
137
+ it('says nothing at the end when nothing was left out', () => {
138
+ const book = new deviceLogBook_1.DeviceLogBook({ since: T0, clock: deviceLogBook_1.UNKNOWN_CLOCK });
139
+ book.add(recAt(T0));
140
+ (0, chai_1.expect)(book.finish(new Date(T0))).to.deep.equal([]);
141
+ });
142
+ it('is 10,000 lines, then 2,000 errors', () => {
143
+ (0, chai_1.expect)(deviceLogBook_1.DEVICE_LOG_LINE_LIMIT).to.equal(10000);
144
+ (0, chai_1.expect)(deviceLogBook_1.DEVICE_LOG_ERROR_LIMIT).to.equal(2000);
145
+ });
146
+ });
147
+ describe('parseDeviceClock', () => {
148
+ const fields = (at) => {
149
+ const t = new Date(at);
150
+ return `${pad(t.getMonth() + 1)}-${pad(t.getDate())}.${pad(t.getHours())}:${pad(t.getMinutes())}:${pad(t.getSeconds())}`;
151
+ };
152
+ it("finds a phone's time zone against this server's, to the quarter hour", () => {
153
+ const secs = Math.floor(T0 / 1000);
154
+ const ahead = 5.5 * 3600000;
155
+ // The phone prints its local time, 5 h 30 min ahead of this server's.
156
+ const out = `${secs}.348157848.${fields(secs * 1000 + ahead)}\n`;
157
+ const clock = (0, deviceLogBook_1.parseDeviceClock)(out, T0 + 300);
158
+ (0, chai_1.expect)(clock === null || clock === void 0 ? void 0 : clock.zoneShiftMs).to.equal(-ahead);
159
+ (0, chai_1.expect)(clock === null || clock === void 0 ? void 0 : clock.skewMs).to.equal(300 - 348);
160
+ });
161
+ it('finds a phone in the same zone, its clock 2.8 s slow, as the lab S9+ was', () => {
162
+ const secs = Math.floor(T0 / 1000) - 3;
163
+ const clock = (0, deviceLogBook_1.parseDeviceClock)(`${secs}.200000000.${fields(secs * 1000)}`, T0);
164
+ (0, chai_1.expect)(clock === null || clock === void 0 ? void 0 : clock.zoneShiftMs).to.equal(0);
165
+ (0, chai_1.expect)(clock === null || clock === void 0 ? void 0 : clock.skewMs).to.equal(2800);
166
+ });
167
+ it("takes the middle of the second when the phone's date prints no nanoseconds", () => {
168
+ const secs = Math.floor(T0 / 1000) + 7;
169
+ for (const nanos of ['%N', 'N', '']) {
170
+ const clock = (0, deviceLogBook_1.parseDeviceClock)(`${secs}.${nanos}.${fields(secs * 1000)}`, T0);
171
+ (0, chai_1.expect)(clock === null || clock === void 0 ? void 0 : clock.skewMs, nanos).to.equal(-7500);
172
+ }
173
+ });
174
+ it("is null for anything that isn't the date it asked for", () => {
175
+ (0, chai_1.expect)((0, deviceLogBook_1.parseDeviceClock)('', T0)).to.equal(null);
176
+ (0, chai_1.expect)((0, deviceLogBook_1.parseDeviceClock)('date: bad format', T0)).to.equal(null);
177
+ (0, chai_1.expect)((0, deviceLogBook_1.parseDeviceClock)('%s.%N.%m-%d.%H:%M:%S', T0)).to.equal(null);
178
+ });
179
+ });
180
+ });
@@ -0,0 +1,81 @@
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 device_service_1 = require("../../src/data-service/device-service");
20
+ const NotificationService_1 = require("../../src/services/NotificationService");
21
+ const SocketServer_1 = require("../../src/services/SocketServer");
22
+ const container_registration_1 = require("../helpers/container-registration");
23
+ const loki_stores_1 = require("../helpers/loki-stores");
24
+ /**
25
+ * The `device_offline` webhook carries the device's name and platform, as its
26
+ * documented payload says (src/services/webhookEvents.ts). Every caller of
27
+ * `removeDevice` holds only a udid and a host, so the event used to carry just
28
+ * those two and `{{name}}` stayed unfilled in a template.
29
+ */
30
+ describe('device_offline: what the event carries', () => {
31
+ (0, loki_stores_1.useLokiStores)();
32
+ let restore;
33
+ let dispatch;
34
+ const HOST = 'http://127.0.0.1:4723';
35
+ beforeEach(() => {
36
+ restore = (0, container_registration_1.saveRegistrations)(NotificationService_1.NotificationService, SocketServer_1.SocketServer);
37
+ dispatch = sinon_1.default.stub().resolves();
38
+ typedi_1.Container.set({ id: NotificationService_1.NotificationService, value: { dispatchEvent: dispatch } });
39
+ typedi_1.Container.set({
40
+ id: SocketServer_1.SocketServer,
41
+ value: {
42
+ emitToDashboard: () => undefined,
43
+ emitToDashboardForDevices: () => __awaiter(void 0, void 0, void 0, function* () { return undefined; }),
44
+ hasScopedDashboard: () => false,
45
+ },
46
+ });
47
+ });
48
+ afterEach(() => {
49
+ sinon_1.default.restore();
50
+ restore();
51
+ });
52
+ const phone = () => ({
53
+ udid: 'R58M123',
54
+ name: 'Galaxy S9+',
55
+ platform: 'android',
56
+ sdk: '10',
57
+ host: HOST,
58
+ busy: false,
59
+ offline: false,
60
+ userBlocked: false,
61
+ deviceType: 'real',
62
+ realDevice: true,
63
+ });
64
+ it('names the device and its platform, though the caller passed only a udid and a host', () => __awaiter(void 0, void 0, void 0, function* () {
65
+ yield (0, device_service_1.addNewDevice)([phone()]);
66
+ dispatch.resetHistory();
67
+ yield (0, device_service_1.removeDevice)([{ udid: 'R58M123', host: HOST }]);
68
+ (0, chai_1.expect)(dispatch.calledOnce).to.equal(true);
69
+ (0, chai_1.expect)(dispatch.firstCall.args[0]).to.equal('device_offline');
70
+ (0, chai_1.expect)(dispatch.firstCall.args[1]).to.include({
71
+ udid: 'R58M123',
72
+ host: HOST,
73
+ name: 'Galaxy S9+',
74
+ platform: 'android',
75
+ });
76
+ }));
77
+ it('still reports a device the store no longer knows, by its udid and host', () => __awaiter(void 0, void 0, void 0, function* () {
78
+ yield (0, device_service_1.removeDevice)([{ udid: 'gone-1', host: HOST }]);
79
+ (0, chai_1.expect)(dispatch.firstCall.args[1]).to.deep.include({ udid: 'gone-1', host: HOST });
80
+ }));
81
+ });