@xenon-device-management/xenon 2.0.0 → 2.1.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 (279) hide show
  1. package/README.md +2 -1
  2. package/lib/package.json +1 -2
  3. package/lib/public/assets/{AnnotationOverlay-BmTPCAds.js → AnnotationOverlay-C-Pw_hvw.js} +1 -1
  4. package/lib/public/assets/{ApiKeyGate-B5eB7f8B.js → ApiKeyGate-DmYQ0oSk.js} +1 -1
  5. package/lib/public/assets/BugReportButton-n8T1dFUp.js +10 -0
  6. package/lib/public/assets/{DeviceMosaicView-9DUoZnQi.js → DeviceMosaicView-D-rGOBbJ.js} +1 -1
  7. package/lib/public/assets/{EmptyState-lvyNiXev.js → EmptyState-DExETevp.js} +1 -1
  8. package/lib/public/assets/{FieldGroup-BduMPM9K.js → FieldGroup-CCdFlZm6.js} +1 -1
  9. package/lib/public/assets/{FilterMenu-BSqX6I94.js → FilterMenu-DiUSOSL-.js} +1 -1
  10. package/lib/public/assets/{Menu-Ba5cT7jZ.js → Menu-ZOfpH6gh.js} +1 -1
  11. package/lib/public/assets/{Modal-CceoMY7D.js → Modal-CZNtoB-1.js} +1 -1
  12. package/lib/public/assets/{RecordingPage-6T9dOidc.js → RecordingPage-BKp6h8Jl.js} +1 -1
  13. package/lib/public/assets/{RecordingsPage-qj6FdZXR.js → RecordingsPage-Bf881FX4.js} +1 -1
  14. package/lib/public/assets/{SegmentedControl-CSRuZ8TR.js → SegmentedControl-EUoyizvW.js} +1 -1
  15. package/lib/public/assets/{SettingCard-Bp1kz4By.js → SettingCard-ClT4hf4_.js} +1 -1
  16. package/lib/public/assets/{Table-LV7zFyZg.js → Table-ucR4bnmc.js} +1 -1
  17. package/lib/public/assets/{activity-D17TUloi.js → activity-K-hdylwq.js} +1 -1
  18. package/lib/public/assets/{ai-settings-qvbhY89P.js → ai-settings-BLhdwI3c.js} +1 -1
  19. package/lib/public/assets/{api-keys-BfpvdG7G.js → api-keys-BbiN7rBI.js} +1 -1
  20. package/lib/public/assets/{apps-Dbnzd0fW.js → apps-Dez1petz.js} +1 -1
  21. package/lib/public/assets/{arrow-left-DD1VjtC7.js → arrow-left-UjmFHLAJ.js} +1 -1
  22. package/lib/public/assets/{arrow-right-D0pnM3ma.js → arrow-right-BVyEigbu.js} +1 -1
  23. package/lib/public/assets/{arrow-up-right-BIC8JOV6.js → arrow-up-right-B-ZlBof3.js} +1 -1
  24. package/lib/public/assets/{auth-shell-DWHPXSnI.js → auth-shell-DAQjAgUv.js} +2 -2
  25. package/lib/public/assets/{builds-page-ClCNBUv_.js → builds-page-CmIPa5JK.js} +1 -1
  26. package/lib/public/assets/{button-xtAhYR-f.js → button-Dd-IhMC1.js} +1 -1
  27. package/lib/public/assets/{calendar-CupwJ7Bm.js → calendar-CAIeJwa5.js} +1 -1
  28. package/lib/public/assets/{check-CyuEoKkd.js → check-C4Jrjg8f.js} +1 -1
  29. package/lib/public/assets/{chevron-left-DgX-F9xK.js → chevron-left-BGzlf-4i.js} +1 -1
  30. package/lib/public/assets/{chevron-right-BKJgJnU9.js → chevron-right-_wYqDjDC.js} +1 -1
  31. package/lib/public/assets/{circle-check-CZdg8mMa.js → circle-check-Ckn00yMB.js} +1 -1
  32. package/lib/public/assets/{circle-x-CZH6t9z-.js → circle-x-pdVkomLa.js} +1 -1
  33. package/lib/public/assets/{clock-Cd-t7xpe.js → clock-B8vmwkHj.js} +1 -1
  34. package/lib/public/assets/{copy-C6zerX3I.js → copy-BAqnFI-F.js} +1 -1
  35. package/lib/public/assets/{device-explorer-B1pI6b_M.js → device-explorer-CYq_4F_N.js} +3 -3
  36. package/lib/public/assets/device-explorer-D2AqrrLN.css +1 -0
  37. package/lib/public/assets/{download-x9WP7mcL.js → download-B6UEAp_1.js} +1 -1
  38. package/lib/public/assets/{forgot-password-B0TEH4N-.js → forgot-password-CLqNvmrN.js} +1 -1
  39. package/lib/public/assets/{funnel-Cb40MfFN.js → funnel-Da90BBbT.js} +1 -1
  40. package/lib/public/assets/{index-BR1wTIPC.js → index-BuIIcmGp.js} +2 -2
  41. package/lib/public/assets/{index-BvH_ekRy.js → index-MX7IzqyW.js} +1 -1
  42. package/lib/public/assets/{input-DejcCiTI.js → input-Bq9aY-We.js} +1 -1
  43. package/lib/public/assets/{keycodes-Mavx5iGg.js → keycodes-CIhXIJyP.js} +1 -1
  44. package/lib/public/assets/{layers-BKqYLiHE.js → layers-bXjpswQI.js} +1 -1
  45. package/lib/public/assets/{lock-LFsFhc17.js → lock-pJ72y5Fy.js} +1 -1
  46. package/lib/public/assets/{login-x2pkzt-t.js → login-UOcHALV2.js} +1 -1
  47. package/lib/public/assets/{maintenance-settings-CYafcoV-.js → maintenance-settings-XnJgp5yb.js} +1 -1
  48. package/lib/public/assets/{monitor-_7_uJ8d9.js → monitor-QkAhI1Jx.js} +1 -1
  49. package/lib/public/assets/{mouse-pointer-2-BfLuURKN.js → mouse-pointer-2-B42Da0m4.js} +1 -1
  50. package/lib/public/assets/{network-Z_zBhpsD.js → network-CKVqcyp6.js} +1 -1
  51. package/lib/public/assets/{overview-BY5uv08S.js → overview-BORGkN3N.js} +1 -1
  52. package/lib/public/assets/{page-header-CP2UkykG.js → page-header-CKWFOjOI.js} +1 -1
  53. package/lib/public/assets/{plus-BTD0dnQ0.js → plus-BpFKVdC8.js} +1 -1
  54. package/lib/public/assets/{profile-page-BhJZuCS7.js → profile-page-CcbKBicY.js} +1 -1
  55. package/lib/public/assets/{reset-password-CMN0SO81.js → reset-password-BA0W9bxl.js} +1 -1
  56. package/lib/public/assets/{runbook-page-CBpPX9_5.js → runbook-page-Cp7ZBq3s.js} +1 -1
  57. package/lib/public/assets/{select-Uc9E5pye.js → select-DIZWIoVR.js} +1 -1
  58. package/lib/public/assets/{selector-detail-page-BSyBnRuU.js → selector-detail-page-C_4YuvAp.js} +1 -1
  59. package/lib/public/assets/{selector-health-DzqxNkzm.js → selector-health-CXjD_Xth.js} +1 -1
  60. package/lib/public/assets/{selector-health-page-CyPLjoD1.js → selector-health-page-CTqIHeoG.js} +1 -1
  61. package/lib/public/assets/{session-detail-page-BZQZN1P-.js → session-detail-page-B0KYkk8z.js} +1 -1
  62. package/lib/public/assets/{settings-CkSWiZq8.js → settings-BcRBKUXd.js} +1 -1
  63. package/lib/public/assets/{status-pill-outline-BFYni2qo.js → status-pill-outline-Dh2PXSf_.js} +1 -1
  64. package/lib/public/assets/{tablet-BSd-xSZK.js → tablet-_sXqZztt.js} +1 -1
  65. package/lib/public/assets/{teams-PpoEQz31.js → teams-DGHT8MKm.js} +1 -1
  66. package/lib/public/assets/{trash-2-BorJv5_u.js → trash-2-BV88cV3Z.js} +1 -1
  67. package/lib/public/assets/{upload-C3i7sAZl.js → upload-CIDH9wkj.js} +1 -1
  68. package/lib/public/assets/{use-password-reset-mode-CeZQ3EsH.js → use-password-reset-mode-DYGuSNwl.js} +1 -1
  69. package/lib/public/assets/{users-CEJ3fC78.js → users-7yviqflc.js} +1 -1
  70. package/lib/public/assets/{users-Drd7ifJx.js → users-BUE7MoTq.js} +1 -1
  71. package/lib/public/assets/{video-off-CNzn6bPE.js → video-off-BTRxtlGo.js} +1 -1
  72. package/lib/public/assets/{webhook-settings-BFqX6QXV.js → webhook-settings-DMKD9Pgm.js} +1 -1
  73. package/lib/public/assets/{zap-4yN8Jdfw.js → zap-CWE94qcK.js} +1 -1
  74. package/lib/public/index.html +1 -1
  75. package/lib/src/InternalHttpClient.js +5 -1
  76. package/lib/src/XenonCapabilityManager.js +15 -1
  77. package/lib/src/app/appiumBasePath.js +32 -0
  78. package/lib/src/app/index.js +12 -0
  79. package/lib/src/app/insertBeforeRoutes.js +24 -4
  80. package/lib/src/app/registerCommandAuth.js +152 -33
  81. package/lib/src/app/routers/control.js +57 -13
  82. package/lib/src/app/routers/grid.js +28 -8
  83. package/lib/src/app/ws/h264StreamWs.js +13 -8
  84. package/lib/src/app/ws/logcatWs.js +12 -7
  85. package/lib/src/app/ws/upgradeRouter.js +215 -0
  86. package/lib/src/dashboard/event-manager.js +7 -6
  87. package/lib/src/data-service/device-service.js +152 -49
  88. package/lib/src/data-service/device-store.js +81 -10
  89. package/lib/src/data-service/deviceClaims.js +98 -0
  90. package/lib/src/data-service/deviceFieldOwners.js +32 -10
  91. package/lib/src/data-service/prisma-store.js +94 -31
  92. package/lib/src/device-managers/AndroidDeviceManager.js +13 -15
  93. package/lib/src/device-managers/HealthMonitorService.js +17 -2
  94. package/lib/src/device-managers/IOSDeviceManager.js +5 -1
  95. package/lib/src/device-managers/NodeDevices.js +7 -2
  96. package/lib/src/device-managers/android/AndroidH264StreamService.js +3 -3
  97. package/lib/src/device-managers/android/AndroidStreamService.js +6 -5
  98. package/lib/src/device-managers/android/previewHold.js +2 -2
  99. package/lib/src/device-managers/ios/IOSDiscoveryService.js +19 -8
  100. package/lib/src/device-managers/ios/IOSStreamService.js +158 -38
  101. package/lib/src/device-managers/localDeviceHosts.js +86 -0
  102. package/lib/src/device-managers/ownDeviceRow.js +38 -0
  103. package/lib/src/device-utils.js +78 -24
  104. package/lib/src/gateway/createHandoff.js +73 -0
  105. package/lib/src/gateway/defaultGateway.js +80 -0
  106. package/lib/src/gateway/forwardToNode.js +147 -0
  107. package/lib/src/gateway/hubSessionToken.js +356 -0
  108. package/lib/src/gateway/internalCall.js +90 -0
  109. package/lib/src/gateway/nodeSessionStatus.js +140 -0
  110. package/lib/src/gateway/nodeWebDriverUrl.js +139 -0
  111. package/lib/src/gateway/sessionCreate.js +136 -0
  112. package/lib/src/gateway/sessionGateway.js +201 -0
  113. package/lib/src/gateway/sessionLocator.js +100 -0
  114. package/lib/src/generated/client/edge.js +7 -4
  115. package/lib/src/generated/client/index-browser.js +4 -1
  116. package/lib/src/generated/client/index.d.ts +112 -1
  117. package/lib/src/generated/client/index.js +7 -4
  118. package/lib/src/generated/client/package.json +1 -1
  119. package/lib/src/generated/client/schema.prisma +9 -0
  120. package/lib/src/generated/client/wasm.js +4 -1
  121. package/lib/src/helpers/index.js +5 -1
  122. package/lib/src/logger.js +5 -0
  123. package/lib/src/middleware/authMiddleware.js +80 -0
  124. package/lib/src/middleware/commandAuth.js +31 -17
  125. package/lib/src/middleware/csrfMiddleware.js +3 -2
  126. package/lib/src/middleware/sessionListingFilter.js +153 -0
  127. package/lib/src/middleware/sessionUpgradeGuard.js +203 -0
  128. package/lib/src/plugin.js +27 -1
  129. package/lib/src/prisma.js +2 -1
  130. package/lib/src/services/DeviceReconciler.js +5 -2
  131. package/lib/src/services/OrphanSweeper.js +12 -19
  132. package/lib/src/services/PortAllocator.js +75 -16
  133. package/lib/src/services/ServerManager.js +50 -30
  134. package/lib/src/services/SessionLifecycleService.js +335 -76
  135. package/lib/src/services/ShutdownCoordinator.js +7 -1
  136. package/lib/src/services/SocketServer.js +11 -2
  137. package/lib/src/services/device-access/LiveSessionOwners.js +53 -0
  138. package/lib/src/services/device-access/SessionOwnerResolver.js +74 -0
  139. package/lib/src/services/lease/LeaseOrphanSweeper.js +32 -6
  140. package/lib/src/services/lease/LeaseService.js +19 -5
  141. package/lib/src/services/session/nodeCreateCaps.js +55 -0
  142. package/lib/src/services/session/sessionCredentials.js +7 -59
  143. package/lib/src/sessions/CloudSession.js +12 -0
  144. package/lib/src/sessions/LocalSession.js +52 -26
  145. package/lib/src/sessions/RemoteSession.js +116 -9
  146. package/lib/src/sessions/SessionManager.js +49 -6
  147. package/lib/src/sessions/appiumUmbrella.js +32 -0
  148. package/lib/test/helpers/appium-umbrella.js +84 -0
  149. package/lib/test/helpers/fake-device-table.js +97 -0
  150. package/lib/test/helpers/loki-stores.js +41 -0
  151. package/lib/test/helpers/loopbackRequest.js +77 -0
  152. package/lib/test/helpers/scratch-database.js +100 -0
  153. package/lib/test/helpers/scratch-port-leases.js +119 -0
  154. package/lib/test/helpers/test-container.js +35 -27
  155. package/lib/test/integration/androidDevices.spec.js +5 -4
  156. package/lib/test/integration/ios/01iOSSimulator.spec.js +6 -5
  157. package/lib/test/integration/ios/02iOSDevices.spec.js +3 -2
  158. package/lib/test/integration/prisma-device-store-add-devices.spec.js +14 -4
  159. package/lib/test/unit/AndroidDeviceManager.spec.js +8 -1
  160. package/lib/test/unit/DeviceModel.spec.js +9 -9
  161. package/lib/test/unit/HealingCascade.test.js +5 -1
  162. package/lib/test/unit/OrphanSweeper.test.js +17 -13
  163. package/lib/test/unit/SessionHeartbeatWrite.test.js +6 -7
  164. package/lib/test/unit/StreamTicketService.spec.js +4 -1
  165. package/lib/test/unit/TokenEndpoint.spec.js +17 -3
  166. package/lib/test/unit/aggregate-hotspots-session-scope.spec.js +5 -5
  167. package/lib/test/unit/android-h264-service.spec.js +6 -0
  168. package/lib/test/unit/android-stream-stall-eof.spec.js +10 -0
  169. package/lib/test/unit/apps-team-visibility.spec.js +16 -16
  170. package/lib/test/unit/auth-me-auth-disabled.test.js +7 -4
  171. package/lib/test/unit/authMiddleware.appTicket.spec.js +5 -5
  172. package/lib/test/unit/authMiddleware.bearer.spec.js +4 -1
  173. package/lib/test/unit/authMiddleware.mcpAudience.spec.js +4 -1
  174. package/lib/test/unit/authMiddleware.streamTicket.spec.js +19 -7
  175. package/lib/test/unit/authMiddleware.test.js +3 -0
  176. package/lib/test/unit/bug-report/route.spec.js +9 -9
  177. package/lib/test/unit/cliargs-admin-only.spec.js +3 -3
  178. package/lib/test/unit/clipboard-route.spec.js +18 -8
  179. package/lib/test/unit/command-auth-appium-server.spec.js +187 -32
  180. package/lib/test/unit/command-auth.spec.js +31 -31
  181. package/lib/test/unit/control-device-lookup.spec.js +9 -9
  182. package/lib/test/unit/control-forward-decision.spec.js +115 -0
  183. package/lib/test/unit/control-lookup-failure.spec.js +3 -3
  184. package/lib/test/unit/create-error-message.spec.js +43 -0
  185. package/lib/test/unit/create-handoff.spec.js +86 -0
  186. package/lib/test/unit/dashboard-events-team-scope.spec.js +6 -3
  187. package/lib/test/unit/device-access-guard.spec.js +28 -28
  188. package/lib/test/unit/device-service.spec.js +4 -0
  189. package/lib/test/unit/device-store-clear-at-boot.spec.js +48 -0
  190. package/lib/test/unit/device-sync-lost-update.spec.js +17 -89
  191. package/lib/test/unit/device-team-guard.spec.js +20 -20
  192. package/lib/test/unit/device-utils.spec.js +8 -0
  193. package/lib/test/unit/extract-team-cap.spec.js +65 -0
  194. package/lib/test/unit/grid-admin-only.spec.js +3 -3
  195. package/lib/test/unit/healing-tier-gate.spec.js +5 -1
  196. package/lib/test/unit/health-monitor-own-phones.spec.js +102 -0
  197. package/lib/test/unit/health-monitor-stream-lookup.spec.js +11 -1
  198. package/lib/test/unit/helpers.spec.js +27 -0
  199. package/lib/test/unit/hub-boot-sweep-recovery.spec.js +192 -0
  200. package/lib/test/unit/hub-create-once.spec.js +190 -0
  201. package/lib/test/unit/hub-create-session-umbrella.spec.js +160 -0
  202. package/lib/test/unit/hub-gateway-create-appium-server.spec.js +591 -0
  203. package/lib/test/unit/hub-node-control-forward.spec.js +291 -0
  204. package/lib/test/unit/hub-node-gateway-appium-server.spec.js +594 -0
  205. package/lib/test/unit/hub-node-race.spec.js +401 -0
  206. package/lib/test/unit/hub-node-same-host.spec.js +356 -0
  207. package/lib/test/unit/hub-register-removal.spec.js +165 -0
  208. package/lib/test/unit/hub-remote-session-heartbeat.spec.js +277 -0
  209. package/lib/test/unit/hub-routed-session-recovery.spec.js +151 -0
  210. package/lib/test/unit/hub-session-token.spec.js +265 -0
  211. package/lib/test/unit/insert-before-routes.spec.js +5 -5
  212. package/lib/test/unit/install-repository-app-team.spec.js +2 -2
  213. package/lib/test/unit/interceptor/MitmProxyHost.spec.js +23 -10
  214. package/lib/test/unit/internal-call.spec.js +181 -0
  215. package/lib/test/unit/ios-discovery-same-host.spec.js +193 -0
  216. package/lib/test/unit/ios-stream-appium-tunnel.spec.js +317 -0
  217. package/lib/test/unit/ios-stream-port-leases.spec.js +299 -0
  218. package/lib/test/unit/ios-wda-status-identity.spec.js +84 -0
  219. package/lib/test/unit/is-xenon-running.spec.js +69 -0
  220. package/lib/test/unit/lease/LeaseOrphanSweeper.spec.js +2 -2
  221. package/lib/test/unit/lease/LeaseService.spec.js +28 -1
  222. package/lib/test/unit/lease/lease-reap-held-phone.spec.js +154 -0
  223. package/lib/test/unit/lease/lease-sweeper-expiry.spec.js +92 -0
  224. package/lib/test/unit/lease/lease-team-visibility.spec.js +2 -2
  225. package/lib/test/unit/lease/lease-token-log-filter.spec.js +242 -64
  226. package/lib/test/unit/lease/lease-token-oracle.spec.js +20 -7
  227. package/lib/test/unit/lease/session-lease-proof.spec.js +8 -3
  228. package/lib/test/unit/logcat-ws.spec.js +4 -1
  229. package/lib/test/unit/loopback-request.spec.js +81 -0
  230. package/lib/test/unit/node-create-caps.spec.js +54 -0
  231. package/lib/test/unit/node-gateway-create-appium-server.spec.js +354 -0
  232. package/lib/test/unit/node-hub-session-owner.spec.js +223 -0
  233. package/lib/test/unit/omni-vision.spec.js +7 -3
  234. package/lib/test/unit/plugin.spec.js +45 -4
  235. package/lib/test/unit/port-allocator-os-probe.spec.js +151 -0
  236. package/lib/test/unit/profile-router.test.js +11 -11
  237. package/lib/test/unit/queue-requester.spec.js +5 -0
  238. package/lib/test/unit/queue-team-scope-routes.spec.js +17 -16
  239. package/lib/test/unit/queue-visibility.spec.js +1 -1
  240. package/lib/test/unit/recording-downloads-visibility.spec.js +2 -2
  241. package/lib/test/unit/recording-instrumentation.spec.js +5 -0
  242. package/lib/test/unit/recording-source-ended.spec.js +5 -0
  243. package/lib/test/unit/recording-store.spec.js +9 -3
  244. package/lib/test/unit/recordings-library-routes.spec.js +51 -51
  245. package/lib/test/unit/recordings-write-visibility.spec.js +2 -2
  246. package/lib/test/unit/remote-session-create.spec.js +130 -0
  247. package/lib/test/unit/reservation-team-visibility.spec.js +5 -5
  248. package/lib/test/unit/reset-link.test.js +14 -14
  249. package/lib/test/unit/session-app-resolution.spec.js +14 -10
  250. package/lib/test/unit/session-assets.spec.js +9 -9
  251. package/lib/test/unit/session-attribution-wiring.spec.js +29 -2
  252. package/lib/test/unit/session-create-layer.spec.js +255 -0
  253. package/lib/test/unit/session-credential-strip.spec.js +25 -9
  254. package/lib/test/unit/session-credentials.spec.js +0 -47
  255. package/lib/test/unit/session-listing-auth.spec.js +345 -0
  256. package/lib/test/unit/session-locator.spec.js +250 -0
  257. package/lib/test/unit/session-node-calls.spec.js +174 -0
  258. package/lib/test/unit/session-owner-resolver.spec.js +107 -0
  259. package/lib/test/unit/session-recovery-routing.spec.js +118 -0
  260. package/lib/test/unit/session-upgrade-guard.spec.js +571 -0
  261. package/lib/test/unit/shutdown-drain-remote.spec.js +58 -0
  262. package/lib/test/unit/socketServer.bearer.spec.js +3 -1
  263. package/lib/test/unit/stream-leave-route.spec.js +15 -7
  264. package/lib/test/unit/stream-own-device-row.spec.js +164 -0
  265. package/lib/test/unit/stream-stop-ownership.spec.js +13 -5
  266. package/lib/test/unit/stream-stop-recording.spec.js +13 -5
  267. package/lib/test/unit/stream-ticket-identity.spec.js +15 -5
  268. package/lib/test/unit/upgrade-router.spec.js +446 -0
  269. package/lib/test/unit/users-router.test.js +11 -8
  270. package/lib/test/unit/verify-credential.spec.js +4 -1
  271. package/lib/test/unit/webdriver-info-route.spec.js +64 -0
  272. package/lib/test/unit/ws-upgrade-routing-appium-server.spec.js +500 -0
  273. package/lib/tsconfig.tsbuildinfo +1 -1
  274. package/package.json +1 -2
  275. package/prisma/migrations/20260929120000_device_claims/migration.sql +8 -0
  276. package/prisma/schema.prisma +9 -0
  277. package/lib/public/assets/BugReportButton-DKYsOdGV.js +0 -10
  278. package/lib/public/assets/device-explorer-DV8rWRxL.css +0 -1
  279. package/lib/src/proxy/wd-command-proxy.js +0 -221
@@ -21,10 +21,12 @@ const prisma_store_1 = require("../../src/data-service/prisma-store");
21
21
  const device_store_1 = require("../../src/data-service/device-store");
22
22
  const device_service_1 = require("../../src/data-service/device-service");
23
23
  const device_utils_1 = require("../../src/device-utils");
24
+ const localDeviceHosts_1 = require("../../src/device-managers/localDeviceHosts");
24
25
  const device_managers_1 = require("../../src/device-managers");
25
26
  const SocketServer_1 = require("../../src/services/SocketServer");
26
27
  const NotificationService_1 = require("../../src/services/NotificationService");
27
28
  const container_registration_1 = require("../helpers/container-registration");
29
+ const fake_device_table_1 = require("../helpers/fake-device-table");
28
30
  /**
29
31
  * The periodic device sync reads every row, runs discovery (seconds of adb or
30
32
  * simctl), then writes each phone back. A write that lands in between (a
@@ -38,81 +40,7 @@ const container_registration_1 = require("../helpers/container-registration");
38
40
  */
39
41
  const HOST = 'http://127.0.0.1:4723';
40
42
  const BIND = '127.0.0.1';
41
- function fakeDeviceTable(seed) {
42
- const rows = new Map();
43
- const key = (udid, host) => `${udid}|${host}`;
44
- const matches = (row, where) => {
45
- if (!where)
46
- return true;
47
- return Object.entries(where).every(([field, cond]) => {
48
- var _a;
49
- if (field === 'OR')
50
- return cond.some((w) => matches(row, w));
51
- if (field === 'AND')
52
- return cond.every((w) => matches(row, w));
53
- if (cond && typeof cond === 'object' && 'in' in cond)
54
- return cond.in.includes(row[field]);
55
- if (cond && typeof cond === 'object' && 'contains' in cond)
56
- return String((_a = row[field]) !== null && _a !== void 0 ? _a : '').includes(cond.contains);
57
- return row[field] === cond;
58
- });
59
- };
60
- const write = (row, data) => {
61
- for (const [field, value] of Object.entries(data))
62
- if (value !== undefined)
63
- row[field] = value;
64
- };
65
- const defaults = () => ({
66
- busy: false,
67
- userBlocked: false,
68
- offline: false,
69
- session_id: null,
70
- teamId: null,
71
- sessionProgress: '',
72
- sessionStartTime: 0,
73
- totalUtilizationTimeMilliSec: 0,
74
- });
75
- for (const r of seed)
76
- rows.set(key(r.udid, r.host), Object.assign(Object.assign({}, defaults()), r));
77
- return {
78
- row: (udid) => [...rows.values()].find((r) => r.udid === udid),
79
- delegate: {
80
- findMany: (...args_1) => __awaiter(this, [...args_1], void 0, function* (args = {}) { return [...rows.values()].filter((r) => matches(r, args.where)).map((r) => (Object.assign({}, r))); }),
81
- findUnique: (_a) => __awaiter(this, [_a], void 0, function* ({ where }) {
82
- const r = rows.get(key(where.udid_host.udid, where.udid_host.host));
83
- return r ? Object.assign({}, r) : null;
84
- }),
85
- upsert: (_a) => __awaiter(this, [_a], void 0, function* ({ where, create, update, }) {
86
- const k = key(where.udid_host.udid, where.udid_host.host);
87
- const found = rows.get(k);
88
- if (found) {
89
- write(found, update);
90
- return Object.assign({}, found);
91
- }
92
- const created = defaults();
93
- write(created, create);
94
- rows.set(k, created);
95
- return Object.assign({}, created);
96
- }),
97
- updateMany: (_a) => __awaiter(this, [_a], void 0, function* ({ where, data }) {
98
- let count = 0;
99
- for (const r of rows.values()) {
100
- if (matches(r, where)) {
101
- write(r, data);
102
- count++;
103
- }
104
- }
105
- return { count };
106
- }),
107
- deleteMany: (_a) => __awaiter(this, [_a], void 0, function* ({ where }) {
108
- for (const [k, r] of rows)
109
- if (matches(r, where))
110
- rows.delete(k);
111
- return { count: 0 };
112
- }),
113
- },
114
- };
115
- }
43
+ const LOCAL = (0, localDeviceHosts_1.localDeviceHosts)({ bindHostOrIp: BIND }, 4723);
116
44
  /** What AndroidDeviceManager returns for a phone it already knows (~158-166). */
117
45
  const androidEcho = (known) => (Object.assign(Object.assign({}, known), { state: 'device', busy: known.busy || false }));
118
46
  /** What IOSDiscoveryService.getSimulators returns for every simulator, known or not. */
@@ -136,7 +64,7 @@ describe('Device sync keeps writes made while it ran (lost update)', () => {
136
64
  let restoreContainer;
137
65
  let savedStore;
138
66
  const seed = (rows) => {
139
- table = fakeDeviceTable(rows);
67
+ table = (0, fake_device_table_1.fakeDeviceTable)(rows);
140
68
  for (const m of Object.keys(table.delegate)) {
141
69
  sinon_1.default.stub(prisma_1.prisma.device, m).callsFake(table.delegate[m]);
142
70
  }
@@ -178,7 +106,7 @@ describe('Device sync keeps writes made while it ran (lost update)', () => {
178
106
  yield (0, device_service_1.blockDevice)('s9', HOST, 'manual_user-1_s9');
179
107
  return known.map(androidEcho);
180
108
  });
181
- yield (0, device_utils_1.updateDeviceList)(BIND);
109
+ yield (0, device_utils_1.updateDeviceList)(LOCAL);
182
110
  (0, chai_1.expect)(table.row('s9')).to.include({ busy: true, session_id: 'manual_user-1_s9' });
183
111
  }));
184
112
  it('keeps a session lock taken while discovery ran, so the phone is not handed out twice', () => __awaiter(void 0, void 0, void 0, function* () {
@@ -190,7 +118,7 @@ describe('Device sync keeps writes made while it ran (lost update)', () => {
190
118
  (0, chai_1.expect)(locked === null || locked === void 0 ? void 0 : locked.udid).to.equal('s9');
191
119
  return known.map(androidEcho);
192
120
  });
193
- yield (0, device_utils_1.updateDeviceList)(BIND);
121
+ yield (0, device_utils_1.updateDeviceList)(LOCAL);
194
122
  (0, chai_1.expect)(table.row('s9')).to.include({ busy: true });
195
123
  const second = yield device_store_1.DeviceStoreFactory.getStore().findAndLockDevice({
196
124
  platform: 'android',
@@ -203,7 +131,7 @@ describe('Device sync keeps writes made while it ran (lost update)', () => {
203
131
  yield device_store_1.DeviceStoreFactory.getStore().updateDevice('s9', HOST, { teamId: 'team-b' });
204
132
  return known.map(androidEcho);
205
133
  });
206
- yield (0, device_utils_1.updateDeviceList)(BIND);
134
+ yield (0, device_utils_1.updateDeviceList)(LOCAL);
207
135
  (0, chai_1.expect)(table.row('s9')).to.include({ teamId: 'team-b' });
208
136
  }));
209
137
  it("keeps a stream's port written while discovery ran", () => __awaiter(void 0, void 0, void 0, function* () {
@@ -212,7 +140,7 @@ describe('Device sync keeps writes made while it ran (lost update)', () => {
212
140
  yield device_store_1.DeviceStoreFactory.getStore().updateDevice('s9', HOST, { mjpegServerPort: 9200 });
213
141
  return known.map(androidEcho);
214
142
  });
215
- yield (0, device_utils_1.updateDeviceList)(BIND);
143
+ yield (0, device_utils_1.updateDeviceList)(LOCAL);
216
144
  (0, chai_1.expect)(table.row('s9')).to.include({ mjpegServerPort: 9200 });
217
145
  }));
218
146
  it('leaves a simulator in a session busy, though discovery always reports it free', () => __awaiter(void 0, void 0, void 0, function* () {
@@ -232,7 +160,7 @@ describe('Device sync keeps writes made while it ran (lost update)', () => {
232
160
  },
233
161
  ]);
234
162
  discover = () => __awaiter(void 0, void 0, void 0, function* () { return [simulatorAsDiscovered()]; });
235
- yield (0, device_utils_1.updateDeviceList)(BIND);
163
+ yield (0, device_utils_1.updateDeviceList)(LOCAL);
236
164
  (0, chai_1.expect)(table.row('sim-1')).to.include({
237
165
  busy: true,
238
166
  session_id: 'sess-1',
@@ -268,7 +196,7 @@ describe('Device sync keeps writes made while it ran (lost update)', () => {
268
196
  Object.assign(Object.assign({}, known.find((d) => d.udid === 'iphone')), { ip: '10.0.0.9' }),
269
197
  ];
270
198
  });
271
- yield (0, device_utils_1.updateDeviceList)(BIND);
199
+ yield (0, device_utils_1.updateDeviceList)(LOCAL);
272
200
  (0, chai_1.expect)(table.row('sim-1')).to.include({ state: 'Booted', wdaLocalPort: 8101 });
273
201
  (0, chai_1.expect)(table.row('iphone')).to.include({ ip: '10.0.0.9' });
274
202
  }));
@@ -276,7 +204,7 @@ describe('Device sync keeps writes made while it ran (lost update)', () => {
276
204
  seed([S9]);
277
205
  const pixel = Object.assign(Object.assign({}, S9), { udid: 'pixel', name: 'Pixel 8' });
278
206
  discover = (known) => __awaiter(void 0, void 0, void 0, function* () { return [...known.map(androidEcho), pixel]; });
279
- yield (0, device_utils_1.updateDeviceList)(BIND);
207
+ yield (0, device_utils_1.updateDeviceList)(LOCAL);
280
208
  (0, chai_1.expect)(emits.filter(([e]) => e === 'device_added').map(([, d]) => d.udid)).to.deep.equal([
281
209
  'pixel',
282
210
  ]);
@@ -297,16 +225,16 @@ describe('Device sync keeps writes made while it ran (lost update)', () => {
297
225
  });
298
226
  (0, chai_1.expect)(emits.filter(([e]) => e === 'device_added')).to.have.length(0);
299
227
  }));
300
- it("the hub stores a node's own report of its phones as sent", () => __awaiter(void 0, void 0, void 0, function* () {
228
+ it("the hub takes a node's report of its phone busy as the node's, not a session of its own", () => __awaiter(void 0, void 0, void 0, function* () {
301
229
  seed([Object.assign(Object.assign({}, S9), { host: 'http://10.0.0.2:4723' })]);
302
- yield (0, device_service_1.addNewDevice)([Object.assign(Object.assign({}, S9), { host: 'http://10.0.0.2:4723', busy: true, session_id: 'node-sess' })], undefined, { mirror: true });
303
- (0, chai_1.expect)(table.row('s9')).to.include({ busy: true, session_id: 'node-sess' });
230
+ yield (0, device_service_1.addNewDevice)([Object.assign(Object.assign({}, S9), { host: 'http://10.0.0.2:4723', busy: true, session_id: 'node-sess' })], undefined, { nodeReport: true });
231
+ (0, chai_1.expect)(table.row('s9')).to.include({ busy: true, nodeBusy: true, session_id: null });
304
232
  (0, chai_1.expect)(emits.filter(([e]) => e === 'device_added')).to.have.length(0);
305
233
  }));
306
234
  });
307
235
  describe('Device sync keeps writes made while it ran, on the Loki test store', () => {
308
236
  const LOKI_HOST = 'http://loki-sync-test:4723';
309
- const LOKI_BIND = 'loki-sync-test';
237
+ const LOKI_LOCAL = (0, localDeviceHosts_1.localDeviceHosts)({ bindHostOrIp: 'loki-sync-test' }, 4723);
310
238
  const UDIDS = ['loki-s9', 'loki-sim', 'loki-pixel'];
311
239
  let emits;
312
240
  let discover;
@@ -358,7 +286,7 @@ describe('Device sync keeps writes made while it ran, on the Loki test store', (
358
286
  yield (0, device_service_1.blockDevice)('loki-s9', LOKI_HOST, 'manual_user-1_loki-s9');
359
287
  return known.filter((d) => d.host === LOKI_HOST).map(androidEcho);
360
288
  });
361
- yield (0, device_utils_1.updateDeviceList)(LOKI_BIND);
289
+ yield (0, device_utils_1.updateDeviceList)(LOKI_LOCAL);
362
290
  (0, chai_1.expect)(yield row('loki-s9')).to.include({ busy: true, session_id: 'manual_user-1_loki-s9' });
363
291
  }));
364
292
  it('leaves a simulator in a session busy, and still takes its new state', () => __awaiter(void 0, void 0, void 0, function* () {
@@ -371,7 +299,7 @@ describe('Device sync keeps writes made while it ran, on the Loki test store', (
371
299
  simulatorAsDiscovered({ udid: 'loki-sim', host: LOKI_HOST, state: 'Booted' }),
372
300
  ];
373
301
  });
374
- yield (0, device_utils_1.updateDeviceList)(LOKI_BIND);
302
+ yield (0, device_utils_1.updateDeviceList)(LOKI_LOCAL);
375
303
  (0, chai_1.expect)(yield row('loki-sim')).to.include({
376
304
  busy: true,
377
305
  session_id: 'sess-1',
@@ -15,7 +15,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
15
15
  require("reflect-metadata");
16
16
  const chai_1 = require("chai");
17
17
  const express_1 = __importDefault(require("express"));
18
- const supertest_1 = __importDefault(require("supertest"));
18
+ const loopbackRequest_1 = __importDefault(require("../helpers/loopbackRequest"));
19
19
  const deviceTeamGuard_1 = require("../../src/middleware/deviceTeamGuard");
20
20
  const controlDevice_1 = require("../../src/middleware/controlDevice");
21
21
  const deviceVisibility_1 = require("../../src/services/device-access/deviceVisibility");
@@ -93,7 +93,7 @@ function appWith(caller, findDevice = (udid) => __awaiter(this, void 0, void 0,
93
93
  return { app, seen };
94
94
  }
95
95
  function call(app, method, path) {
96
- return (0, supertest_1.default)(app)[method](path);
96
+ return (0, loopbackRequest_1.default)(app)[method](path);
97
97
  }
98
98
  /** What a caller can observe of an answer, with the requested udid masked out. */
99
99
  function observed(res, udid) {
@@ -153,7 +153,7 @@ describe('deviceTeamGuard', () => {
153
153
  }
154
154
  it('hands a hidden phone on as a udid no device has, keeping the original URL', () => __awaiter(void 0, void 0, void 0, function* () {
155
155
  const { app, seen } = appWith(ALICE);
156
- yield (0, supertest_1.default)(app).get(`/control/${PHONE_B}/screenshot?x=1`);
156
+ yield (0, loopbackRequest_1.default)(app).get(`/control/${PHONE_B}/screenshot?x=1`);
157
157
  (0, chai_1.expect)(seen).to.deep.equal([controlDevice_1.HIDDEN_DEVICE_UDID]);
158
158
  (0, chai_1.expect)(DEVICES[controlDevice_1.HIDDEN_DEVICE_UDID]).to.equal(undefined);
159
159
  }));
@@ -171,7 +171,7 @@ describe('deviceTeamGuard', () => {
171
171
  res.end();
172
172
  });
173
173
  app.use('/control', router);
174
- yield (0, supertest_1.default)(app).get(`/control/${PHONE_B}/screenshot?x=1`);
174
+ yield (0, loopbackRequest_1.default)(app).get(`/control/${PHONE_B}/screenshot?x=1`);
175
175
  (0, chai_1.expect)(originalUrl).to.equal(`/control/${PHONE_B}/screenshot?x=1`);
176
176
  }));
177
177
  describe('restoreHiddenDeviceUrl, mounted after the router at the parent', () => {
@@ -196,7 +196,7 @@ describe('deviceTeamGuard', () => {
196
196
  return { app, seenByRoute };
197
197
  }
198
198
  it('gives a later layer the real req.url after the router falls through', () => __awaiter(void 0, void 0, void 0, function* () {
199
- const res = yield (0, supertest_1.default)(parentApp(ALICE).app).get(`/control/${PHONE_B}/bogus?x=1`);
199
+ const res = yield (0, loopbackRequest_1.default)(parentApp(ALICE).app).get(`/control/${PHONE_B}/bogus?x=1`);
200
200
  (0, chai_1.expect)(res.body).to.deep.equal({
201
201
  url: `/control/${PHONE_B}/bogus?x=1`,
202
202
  path: `/control/${PHONE_B}/bogus`,
@@ -204,26 +204,26 @@ describe('deviceTeamGuard', () => {
204
204
  }));
205
205
  it('answers a hidden phone and an unknown udid alike, even where a layer echoes req.url', () => __awaiter(void 0, void 0, void 0, function* () {
206
206
  const { app } = parentApp(ALICE);
207
- const hidden = yield (0, supertest_1.default)(app).get(`/control/${PHONE_B}/bogus`);
208
- const unknown = yield (0, supertest_1.default)(app).get(`/control/${UNKNOWN}/bogus`);
207
+ const hidden = yield (0, loopbackRequest_1.default)(app).get(`/control/${PHONE_B}/bogus`);
208
+ const unknown = yield (0, loopbackRequest_1.default)(app).get(`/control/${UNKNOWN}/bogus`);
209
209
  (0, chai_1.expect)(observed(hidden, PHONE_B)).to.deep.equal(observed(unknown, UNKNOWN));
210
210
  (0, chai_1.expect)(hidden.text).to.not.include(controlDevice_1.HIDDEN_DEVICE_UDID);
211
211
  }));
212
212
  it('still hands a routed request the placeholder, not the real udid', () => __awaiter(void 0, void 0, void 0, function* () {
213
213
  const { app, seenByRoute } = parentApp(ALICE);
214
- const res = yield (0, supertest_1.default)(app).get(`/control/${PHONE_B}/screenshot`);
214
+ const res = yield (0, loopbackRequest_1.default)(app).get(`/control/${PHONE_B}/screenshot`);
215
215
  (0, chai_1.expect)(res.text).to.equal('Device not found');
216
216
  (0, chai_1.expect)(seenByRoute).to.deep.equal([controlDevice_1.HIDDEN_DEVICE_UDID]);
217
217
  }));
218
218
  it('leaves a request it did not rewrite alone', () => __awaiter(void 0, void 0, void 0, function* () {
219
- const res = yield (0, supertest_1.default)(parentApp(ALICE).app).get(`/control/${PHONE_A}/bogus`);
219
+ const res = yield (0, loopbackRequest_1.default)(parentApp(ALICE).app).get(`/control/${PHONE_A}/bogus`);
220
220
  (0, chai_1.expect)(res.body.url).to.equal(`/control/${PHONE_A}/bogus`);
221
221
  }));
222
222
  });
223
223
  it('gives a member in no team the shared pool only', () => __awaiter(void 0, void 0, void 0, function* () {
224
224
  const { app } = appWith(NO_TEAM);
225
- (0, chai_1.expect)((yield (0, supertest_1.default)(app).get(`/control/${SHARED}/screenshot`)).status).to.equal(200);
226
- const a = yield (0, supertest_1.default)(app).get(`/control/${PHONE_A}/screenshot`);
225
+ (0, chai_1.expect)((yield (0, loopbackRequest_1.default)(app).get(`/control/${SHARED}/screenshot`)).status).to.equal(200);
226
+ const a = yield (0, loopbackRequest_1.default)(app).get(`/control/${PHONE_A}/screenshot`);
227
227
  (0, chai_1.expect)(a.status).to.equal(404);
228
228
  (0, chai_1.expect)(a.text).to.equal('Device not found');
229
229
  }));
@@ -231,16 +231,16 @@ describe('deviceTeamGuard', () => {
231
231
  // plain-text default. A hidden phone gets the same body, from the handler.
232
232
  for (const action of ['display', 'appium-session']) {
233
233
  it(`answers ${action} with that handler's own JSON 404 body`, () => __awaiter(void 0, void 0, void 0, function* () {
234
- const res = yield (0, supertest_1.default)(appWith(ALICE).app).get(`/control/${PHONE_B}/${action}`);
234
+ const res = yield (0, loopbackRequest_1.default)(appWith(ALICE).app).get(`/control/${PHONE_B}/${action}`);
235
235
  (0, chai_1.expect)(res.status).to.equal(404);
236
236
  (0, chai_1.expect)(res.body).to.deep.equal({ status: 'error', message: 'Device not found' });
237
237
  }));
238
238
  }
239
239
  it('guards an action in any letter case', () => __awaiter(void 0, void 0, void 0, function* () {
240
240
  const { app, seen } = appWith(ALICE);
241
- const start = yield (0, supertest_1.default)(app).post(`/control/${PHONE_B}/Stream/Start`);
241
+ const start = yield (0, loopbackRequest_1.default)(app).post(`/control/${PHONE_B}/Stream/Start`);
242
242
  (0, chai_1.expect)(start.status).to.equal(404);
243
- const display = yield (0, supertest_1.default)(app).get(`/control/${PHONE_B}/Display`);
243
+ const display = yield (0, loopbackRequest_1.default)(app).get(`/control/${PHONE_B}/Display`);
244
244
  (0, chai_1.expect)(display.status).to.equal(404);
245
245
  (0, chai_1.expect)(display.body).to.deep.equal({ status: 'error', message: 'Device not found' });
246
246
  (0, chai_1.expect)(seen).to.not.include(PHONE_B);
@@ -272,7 +272,7 @@ describe('deviceTeamGuard', () => {
272
272
  // drive a phone the device list hides from them.
273
273
  it('ignores an admin scope on a caller whose teams are scoped', () => __awaiter(void 0, void 0, void 0, function* () {
274
274
  const scoped = { role: 'MEMBER', scopes: 'admin,devices', teamIds: [TEAM_A] };
275
- const res = yield (0, supertest_1.default)(appWith(scoped).app).post(`/control/${PHONE_B}/tap`);
275
+ const res = yield (0, loopbackRequest_1.default)(appWith(scoped).app).post(`/control/${PHONE_B}/tap`);
276
276
  (0, chai_1.expect)(res.status).to.equal(404);
277
277
  }));
278
278
  it('does not look the device up for an admin', () => __awaiter(void 0, void 0, void 0, function* () {
@@ -282,13 +282,13 @@ describe('deviceTeamGuard', () => {
282
282
  lookups += 1;
283
283
  return (_a = DEVICES[udid]) !== null && _a !== void 0 ? _a : null;
284
284
  }));
285
- const res = yield (0, supertest_1.default)(app).get(`/control/${PHONE_B}/screenshot`);
285
+ const res = yield (0, loopbackRequest_1.default)(app).get(`/control/${PHONE_B}/screenshot`);
286
286
  (0, chai_1.expect)(res.status).to.equal(200);
287
287
  (0, chai_1.expect)(lookups).to.equal(0);
288
288
  }));
289
289
  it('passes an unknown device on untouched, for the handler to answer', () => __awaiter(void 0, void 0, void 0, function* () {
290
290
  const { app, seen } = appWith(ALICE);
291
- const res = yield (0, supertest_1.default)(app).post(`/control/${UNKNOWN}/tap`);
291
+ const res = yield (0, loopbackRequest_1.default)(app).post(`/control/${UNKNOWN}/tap`);
292
292
  (0, chai_1.expect)(res.status).to.equal(404);
293
293
  (0, chai_1.expect)(seen).to.deep.equal([UNKNOWN]);
294
294
  }));
@@ -296,7 +296,7 @@ describe('deviceTeamGuard', () => {
296
296
  const { app, seen } = appWith(ALICE, () => __awaiter(void 0, void 0, void 0, function* () {
297
297
  throw new Error('store hiccup');
298
298
  }));
299
- const res = yield (0, supertest_1.default)(app).get(`/control/${PHONE_A}/screenshot`);
299
+ const res = yield (0, loopbackRequest_1.default)(app).get(`/control/${PHONE_A}/screenshot`);
300
300
  (0, chai_1.expect)(res.status).to.equal(503);
301
301
  (0, chai_1.expect)(res.body).to.deep.equal({
302
302
  success: false,
@@ -310,13 +310,13 @@ describe('deviceTeamGuard', () => {
310
310
  // An async middleware that throws never answers under Express 4, so keep
311
311
  // a short timeout: a regression should fail fast, not after 60s.
312
312
  this.timeout(5000);
313
- const res = yield (0, supertest_1.default)(appWith(ALICE).app).get('/control/100%/screenshot');
313
+ const res = yield (0, loopbackRequest_1.default)(appWith(ALICE).app).get('/control/100%/screenshot');
314
314
  (0, chai_1.expect)(res.status).to.equal(400);
315
315
  (0, chai_1.expect)(res.body).to.deep.equal({ success: false, error: 'invalid_udid' });
316
316
  });
317
317
  });
318
318
  it('401s when there is no caller', () => __awaiter(void 0, void 0, void 0, function* () {
319
- const res = yield (0, supertest_1.default)(appWith(null).app).get(`/control/${PHONE_A}/screenshot`);
319
+ const res = yield (0, loopbackRequest_1.default)(appWith(null).app).get(`/control/${PHONE_A}/screenshot`);
320
320
  (0, chai_1.expect)(res.status).to.equal(401);
321
321
  }));
322
322
  });
@@ -57,11 +57,18 @@ const device_service_1 = require("../../src/data-service/device-service");
57
57
  const device_utils_1 = require("../../src/device-utils");
58
58
  const IPluginArgs_1 = require("../../src/interfaces/IPluginArgs");
59
59
  const test_container_1 = require("../helpers/test-container");
60
+ const loki_stores_1 = require("../helpers/loki-stores");
61
+ const scratch_database_1 = require("../helpers/scratch-database");
60
62
  chai_1.default.should();
61
63
  chai_1.default.use(sinon_chai_1.default);
62
64
  const expect = chai_1.default.expect;
63
65
  const sandbox = sinon_1.default.createSandbox();
64
66
  describe('Device Utils', () => {
67
+ // The in-memory device store, also when this file runs on its own, and a
68
+ // database of its own for what allocation reads through Prisma. It passed
69
+ // only against the developer's ~/.cache/xenon/xenon.db, which has the tables.
70
+ (0, loki_stores_1.useLokiStores)();
71
+ (0, scratch_database_1.useScratchDatabase)();
65
72
  const hub1Device = {
66
73
  systemPort: 56205,
67
74
  sdk: '10',
@@ -162,6 +169,7 @@ describe('Device Utils', () => {
162
169
  yield (0, test_container_1.resetTestContainer)();
163
170
  yield (0, test_container_1.setupTestContainer)();
164
171
  }));
172
+ after(() => (0, test_container_1.resetTestContainer)());
165
173
  it('Allocate devices for session with host filter', () => __awaiter(void 0, void 0, void 0, function* () {
166
174
  (yield db_1.XenonDatabase.DeviceModel).removeDataOnly();
167
175
  const deviceManager = (0, test_container_1.createTestXenonManager)(Object.assign({}, pluginArgs, { maxSessions: 3, platform: 'android' }));
@@ -0,0 +1,65 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const chai_1 = require("chai");
4
+ const XenonCapabilityManager_1 = require("../../src/XenonCapabilityManager");
5
+ // The team a session asks to be placed in. Documented as xe:options.team;
6
+ // xenon:options is read as its alias, through the same precedence helper as
7
+ // every other Xenon option. The flat Xenon-prefixed caps older clients send
8
+ // (xenon:team, xe:teamId, ...) still work, below the options. df: is never
9
+ // read.
10
+ const always = (alwaysMatch) => ({ alwaysMatch, firstMatch: [{}] });
11
+ describe('extractTeamCap', () => {
12
+ it('reads xe:options.team, the documented form', () => {
13
+ (0, chai_1.expect)((0, XenonCapabilityManager_1.extractTeamCap)(always({ 'xe:options': { team: 'team_a' } }))).to.equal('team_a');
14
+ });
15
+ it('reads xe:options.teamId', () => {
16
+ (0, chai_1.expect)((0, XenonCapabilityManager_1.extractTeamCap)(always({ 'xe:options': { teamId: 'team_a' } }))).to.equal('team_a');
17
+ });
18
+ it('still reads the xenon:options alias alone', () => {
19
+ (0, chai_1.expect)((0, XenonCapabilityManager_1.extractTeamCap)(always({ 'xenon:options': { team: 'team_a' } }))).to.equal('team_a');
20
+ (0, chai_1.expect)((0, XenonCapabilityManager_1.extractTeamCap)(always({ 'xenon:options': { teamId: 'team_a' } }))).to.equal('team_a');
21
+ });
22
+ it('lets xe:options win over xenon:options', () => {
23
+ (0, chai_1.expect)((0, XenonCapabilityManager_1.extractTeamCap)(always({ 'xenon:options': { team: 'team_old' }, 'xe:options': { team: 'team_new' } }))).to.equal('team_new');
24
+ // Across alwaysMatch and firstMatch, as allocation merges them.
25
+ (0, chai_1.expect)((0, XenonCapabilityManager_1.extractTeamCap)({
26
+ alwaysMatch: { 'xenon:options': { teamId: 'team_old' } },
27
+ firstMatch: [{ 'xe:options': { teamId: 'team_new' } }],
28
+ })).to.equal('team_new');
29
+ });
30
+ it('lets an options field win over a flat cap', () => {
31
+ for (const flat of ['xenon:team', 'xenon:teamId', 'xe:team', 'xe:teamId', 'appium:team']) {
32
+ (0, chai_1.expect)((0, XenonCapabilityManager_1.extractTeamCap)(always({ [flat]: 'team_flat', 'xe:options': { team: 'team_opt' } })), flat).to.equal('team_opt');
33
+ (0, chai_1.expect)((0, XenonCapabilityManager_1.extractTeamCap)(always({ [flat]: 'team_flat', 'xenon:options': { teamId: 'team_opt' } })), flat).to.equal('team_opt');
34
+ }
35
+ });
36
+ it('keeps every flat form working when no option is set', () => {
37
+ for (const prefix of ['xenon:', 'xe:', 'appium:', '']) {
38
+ for (const name of ['teamId', 'team_id', 'team']) {
39
+ const key = `${prefix}${name}`;
40
+ (0, chai_1.expect)((0, XenonCapabilityManager_1.extractTeamCap)(always({ [key]: 'team_flat' })), key).to.equal('team_flat');
41
+ }
42
+ }
43
+ });
44
+ it('falls back to a flat cap when the option is empty or not a string', () => {
45
+ for (const team of ['', 7, null, { id: 'team_x' }]) {
46
+ (0, chai_1.expect)((0, XenonCapabilityManager_1.extractTeamCap)(always({ 'xe:options': { team }, 'xenon:team': 'team_flat' })), JSON.stringify(team)).to.equal('team_flat');
47
+ }
48
+ });
49
+ it('never reads df:', () => {
50
+ for (const caps of [
51
+ { 'df:options': { team: 'team_df' } },
52
+ { 'df:options': { teamId: 'team_df' } },
53
+ { 'xenon:df:options': { team: 'team_df' } },
54
+ { 'appium:df:options': { team: 'team_df' } },
55
+ { 'df:team': 'team_df' },
56
+ { 'df:teamId': 'team_df' },
57
+ ]) {
58
+ (0, chai_1.expect)((0, XenonCapabilityManager_1.extractTeamCap)(always(caps)), JSON.stringify(caps)).to.equal(undefined);
59
+ }
60
+ });
61
+ it('is undefined when nothing asks for a team', () => {
62
+ (0, chai_1.expect)((0, XenonCapabilityManager_1.extractTeamCap)(always({}))).to.equal(undefined);
63
+ (0, chai_1.expect)((0, XenonCapabilityManager_1.extractTeamCap)({})).to.equal(undefined);
64
+ });
65
+ });
@@ -15,7 +15,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
15
15
  require("reflect-metadata");
16
16
  const chai_1 = require("chai");
17
17
  const express_1 = __importDefault(require("express"));
18
- const supertest_1 = __importDefault(require("supertest"));
18
+ const loopbackRequest_1 = __importDefault(require("../helpers/loopbackRequest"));
19
19
  const grid_1 = __importDefault(require("../../src/app/routers/grid"));
20
20
  const authMiddleware_1 = require("../../src/middleware/authMiddleware");
21
21
  /**
@@ -44,12 +44,12 @@ describe('grid operator reads are admin-only', () => {
44
44
  }
45
45
  for (const url of ['/logs/requests', '/node/status', '/node/10.0.0.2/status']) {
46
46
  it(`a member gets 403 from GET ${url}`, () => __awaiter(void 0, void 0, void 0, function* () {
47
- const r = yield (0, supertest_1.default)(app('MEMBER')).get(url);
47
+ const r = yield (0, loopbackRequest_1.default)(app('MEMBER')).get(url);
48
48
  (0, chai_1.expect)(r.status).to.equal(403);
49
49
  }));
50
50
  }
51
51
  it('an admin still reads the request log', () => __awaiter(void 0, void 0, void 0, function* () {
52
- const r = yield (0, supertest_1.default)(app('ADMIN')).get('/logs/requests');
52
+ const r = yield (0, loopbackRequest_1.default)(app('ADMIN')).get('/logs/requests');
53
53
  (0, chai_1.expect)(r.status).to.equal(200);
54
54
  }));
55
55
  });
@@ -9,12 +9,14 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
9
9
  });
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
+ require("reflect-metadata");
12
13
  const chai_1 = require("chai");
13
14
  const typedi_1 = require("typedi");
14
15
  const HealingOrchestrator_1 = require("../../src/services/healing/HealingOrchestrator");
15
16
  const HealEtalonService_1 = require("../../src/services/healing/HealEtalonService");
16
17
  const HealedLocatorGenerator_1 = require("../../src/services/healing/HealedLocatorGenerator");
17
18
  const types_1 = require("../../src/services/healing/types");
19
+ const container_registration_1 = require("../helpers/container-registration");
18
20
  // §2.7 healing-tier capability gate: xe:options.healingTiers restricts which
19
21
  // self-healing providers may be dispatched. Tier index mapping used by the gate
20
22
  // (array-position based, NOT the internal HealingTier enum): 1=Resilio,
@@ -104,8 +106,9 @@ describe('HealingOrchestrator.attemptHealing allowedTiers dispatch gate', () =>
104
106
  let orchestrator;
105
107
  let mockEtalonService;
106
108
  const dispatched = [];
109
+ let restoreContainer;
107
110
  beforeEach(() => {
108
- typedi_1.Container.reset();
111
+ restoreContainer = (0, container_registration_1.saveRegistrations)(HealEtalonService_1.HealEtalonService, HealedLocatorGenerator_1.HealedLocatorGenerator);
109
112
  dispatched.length = 0;
110
113
  mockEtalonService = {
111
114
  getSignature: () => __awaiter(void 0, void 0, void 0, function* () { return null; }),
@@ -123,6 +126,7 @@ describe('HealingOrchestrator.attemptHealing allowedTiers dispatch gate', () =>
123
126
  });
124
127
  });
125
128
  });
129
+ afterEach(() => restoreContainer());
126
130
  const mockDriver = {
127
131
  getPageSource: () => __awaiter(void 0, void 0, void 0, function* () { return '<xml/>'; }),
128
132
  getScreenshot: () => __awaiter(void 0, void 0, void 0, function* () { return 'fake-screenshot'; }),
@@ -0,0 +1,102 @@
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 HealthMonitorService_1 = require("../../src/device-managers/HealthMonitorService");
20
+ const device_managers_1 = require("../../src/device-managers");
21
+ const device_store_1 = require("../../src/data-service/device-store");
22
+ const PluginContext_1 = require("../../src/PluginContext");
23
+ const IPluginArgs_1 = require("../../src/interfaces/IPluginArgs");
24
+ const SessionManager_1 = require("../../src/sessions/SessionManager");
25
+ const container_registration_1 = require("../helpers/container-registration");
26
+ /**
27
+ * A hub keeps its nodes' phones in its own Device table. Its health monitor
28
+ * ran adb (or go-ios) health checks on every row, so it checked node phones it
29
+ * cannot reach from its own machine: each came back unhealthy, the hub wrote
30
+ * that over the node's report, started a "recovery" with its own adb, and
31
+ * reclaimed a busy node phone whose session it didn't hold in memory. Only
32
+ * this server's own phones are checked (isOwnDevice: by nodeId, else by exact
33
+ * host).
34
+ */
35
+ // Named like the real manager: the monitor picks one by constructor.name.
36
+ class AndroidDeviceManager {
37
+ constructor() {
38
+ this.checked = [];
39
+ this.checkHealth = (device) => __awaiter(this, void 0, void 0, function* () {
40
+ this.checked.push(device.udid);
41
+ return { healthStatus: 'Healthy' };
42
+ });
43
+ }
44
+ }
45
+ describe('HealthMonitorService checks only this server’s own phones', () => {
46
+ const HUB_NODE_ID = 'hub-node-id';
47
+ const HUB_HOST = 'http://10.0.0.1:4799';
48
+ let manager;
49
+ let updateDevice;
50
+ let context;
51
+ let saved;
52
+ let restore;
53
+ const devices = [
54
+ // Found by this hub's own discovery.
55
+ { udid: 'hub-phone', host: HUB_HOST, nodeId: HUB_NODE_ID },
56
+ // A node's, reported over /register with the node's id.
57
+ { udid: 'node-phone', host: 'http://10.0.0.2:4725', nodeId: 'node-peer' },
58
+ // A node's on the same machine, differing only by port.
59
+ { udid: 'same-machine-node-phone', host: 'http://10.0.0.1:4725', nodeId: 'node-2' },
60
+ // A node's busy phone, its session not in this hub's memory.
61
+ {
62
+ udid: 'busy-node-phone',
63
+ host: 'http://10.0.0.2:4725',
64
+ nodeId: 'node-peer',
65
+ busy: true,
66
+ session_id: 'node-session',
67
+ },
68
+ // A row from before nodeId, known by its host alone.
69
+ { udid: 'legacy-hub-phone', host: HUB_HOST, nodeId: null },
70
+ { udid: 'legacy-node-phone', host: 'http://10.0.0.3:4725', nodeId: null },
71
+ ].map((d) => (Object.assign({ platform: 'android', busy: false, cloud: null }, d)));
72
+ beforeEach(() => {
73
+ sinon_1.default.stub(process, 'kill');
74
+ manager = new AndroidDeviceManager();
75
+ restore = (0, container_registration_1.saveRegistrations)(device_managers_1.XenonManager);
76
+ typedi_1.Container.set(device_managers_1.XenonManager, { deviceInstances: () => __awaiter(void 0, void 0, void 0, function* () { return [manager]; }) });
77
+ updateDevice = sinon_1.default.spy(() => __awaiter(void 0, void 0, void 0, function* () { return undefined; }));
78
+ sinon_1.default.stub(device_store_1.DeviceStoreFactory, 'getStore').returns({
79
+ getAllDevices: () => __awaiter(void 0, void 0, void 0, function* () { return devices; }),
80
+ updateDevice,
81
+ });
82
+ sinon_1.default.stub(SessionManager_1.SESSION_MANAGER, 'isValidSession').returns(false);
83
+ context = typedi_1.Container.get(PluginContext_1.PluginContext);
84
+ saved = Object.assign({}, context);
85
+ context.setContext(Object.assign(Object.assign({}, IPluginArgs_1.DefaultPluginArgs), { bindHostOrIp: '10.0.0.1' }), 4799, HUB_NODE_ID, '');
86
+ });
87
+ afterEach(() => {
88
+ Object.assign(context, saved);
89
+ sinon_1.default.restore();
90
+ restore();
91
+ });
92
+ const run = () => new HealthMonitorService_1.HealthMonitorService({}).checkAllDevices();
93
+ it('checks the hub’s own phones and none of its nodes’', () => __awaiter(void 0, void 0, void 0, function* () {
94
+ yield run();
95
+ (0, chai_1.expect)(manager.checked.sort()).to.deep.equal(['hub-phone', 'legacy-hub-phone']);
96
+ }));
97
+ it('never writes to a node’s phone, nor reclaims a busy one', () => __awaiter(void 0, void 0, void 0, function* () {
98
+ yield run();
99
+ const written = updateDevice.getCalls().map((c) => c.args[0]);
100
+ (0, chai_1.expect)(written.sort()).to.deep.equal(['hub-phone', 'legacy-hub-phone']);
101
+ }));
102
+ });