@xenon-device-management/xenon 2.14.0 → 2.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (335) hide show
  1. package/README.md +44 -29
  2. package/lib/package.json +6 -5
  3. package/lib/public/assets/{AnnotationOverlay-B_VNtKBH.js → AnnotationOverlay-2W4JvEyZ.js} +1 -1
  4. package/lib/public/assets/{ApiKeyGate-BwuaMjkX.js → ApiKeyGate-GmckX_7r.js} +1 -1
  5. package/lib/public/assets/DeviceMosaicView-DqODzkyo.js +2 -0
  6. package/lib/public/assets/{EmptyState-BSjhk_IV.js → EmptyState-7hETKEWb.js} +1 -1
  7. package/lib/public/assets/{FieldGroup-Cb5QZsE1.js → FieldGroup-GOgssFEP.js} +1 -1
  8. package/lib/public/assets/{FilterMenu-BAaSlWbW.js → FilterMenu-Yvf6RGMd.js} +1 -1
  9. package/lib/public/assets/{Menu-JGGYrSQQ.js → Menu-KrPnfdsf.js} +1 -1
  10. package/lib/public/assets/{Modal-CFCp3n9d.js → Modal-C6WjZFeI.js} +1 -1
  11. package/lib/public/assets/{RecordingPage-CbJj_EBT.js → RecordingPage-CSRIhZEh.js} +1 -1
  12. package/lib/public/assets/{RecordingsPage-BJeYnygR.js → RecordingsPage-CB8ST3nE.js} +1 -1
  13. package/lib/public/assets/{SegmentedControl-Bg42Atjy.js → SegmentedControl-C9o90KId.js} +1 -1
  14. package/lib/public/assets/{SettingCard-Njz1pUIJ.js → SettingCard-CPm3w9l2.js} +1 -1
  15. package/lib/public/assets/{Table-BcTm5xV2.js → Table-DwIgERRt.js} +1 -1
  16. package/lib/public/assets/{activity-DOVdsfMQ.js → activity-DrLbHifn.js} +1 -1
  17. package/lib/public/assets/ai-settings-DU_Ls06P.js +16 -0
  18. package/lib/public/assets/{api-keys-BKnNSTw9.js → api-keys-CWYdEzJ_.js} +1 -1
  19. package/lib/public/assets/{apps-D0SC6uw6.js → apps-BqNVdln7.js} +1 -1
  20. package/lib/public/assets/{arrow-left-MMbTmgoE.js → arrow-left-BtB0_Jg_.js} +1 -1
  21. package/lib/public/assets/{arrow-right-BMfnGmxW.js → arrow-right-C8NhdtyN.js} +1 -1
  22. package/lib/public/assets/{arrow-up-right-DuAftMWB.js → arrow-up-right-gMg0ffvv.js} +1 -1
  23. package/lib/public/assets/{auth-shell-nZC2JNDO.js → auth-shell-CiYZ3N4M.js} +2 -2
  24. package/lib/public/assets/{builds-page-hDCgTwkS.js → builds-page-EaaqDmLN.js} +1 -1
  25. package/lib/public/assets/{button-Dda4irn_.js → button-DDau4Axv.js} +1 -1
  26. package/lib/public/assets/{calendar-BMr0Q62v.js → calendar-DQ83B1ed.js} +1 -1
  27. package/lib/public/assets/{check-CZ9iw9-e.js → check-K1ovTQam.js} +1 -1
  28. package/lib/public/assets/{chevron-right-BRYJJVC9.js → chevron-right-Biq00pMt.js} +1 -1
  29. package/lib/public/assets/{circle-check-CqAJjQ5O.js → circle-check-CBv88cH7.js} +1 -1
  30. package/lib/public/assets/{circle-x-DlDJtnVz.js → circle-x-Bmltailn.js} +1 -1
  31. package/lib/public/assets/{clock-CpuDKysU.js → clock-BPrld-3h.js} +1 -1
  32. package/lib/public/assets/{copy-CAQydNFe.js → copy-t0QB14JI.js} +1 -1
  33. package/lib/public/assets/derive-UnLRp4r3.js +1 -0
  34. package/lib/public/assets/device-explorer-BgGZ8AvV.js +282 -0
  35. package/lib/public/assets/{download-C9XG6lLv.js → download-BroHxNOh.js} +1 -1
  36. package/lib/public/assets/forgot-password-Bjmf49i6.js +1 -0
  37. package/lib/public/assets/{h264Stream-VQBphYhg.js → h264Stream-DeD__6wa.js} +6 -6
  38. package/lib/public/assets/{index-DJGc1M2f.js → index-72oF89LJ.js} +1 -1
  39. package/lib/public/assets/index-B0JeuZbg.js +15 -0
  40. package/lib/public/assets/index-B0gQkEg0.js +219 -0
  41. package/lib/public/assets/{input-Cm0tmSoc.js → input-DIc4MSC7.js} +1 -1
  42. package/lib/public/assets/{line-chart-D2DyJCml.js → line-chart-BcWm2vW8.js} +1 -1
  43. package/lib/public/assets/{list-checks-UWf1GY99.js → list-checks-B06IJJad.js} +1 -1
  44. package/lib/public/assets/{lock-CCJv9m_I.js → lock-pVLvnN30.js} +1 -1
  45. package/lib/public/assets/{login-f8vOVW-U.js → login-CN77vy--.js} +1 -1
  46. package/lib/public/assets/{maintenance-settings-C1PrkuAH.js → maintenance-settings-CSdzQSkY.js} +1 -1
  47. package/lib/public/assets/{monitor-B2jDgwm7.js → monitor-BtrN5IkI.js} +1 -1
  48. package/lib/public/assets/{mouse-pointer-2-DLI1RtzU.js → mouse-pointer-2-CNsQfo6w.js} +1 -1
  49. package/lib/public/assets/{network-ebT86qoi.js → network-BSfA8ZxG.js} +1 -1
  50. package/lib/public/assets/{overview-C0aykVrd.js → overview-BfTebCK5.js} +1 -1
  51. package/lib/public/assets/{page-header-BjzgWz-T.js → page-header-D8KUX6Qg.js} +1 -1
  52. package/lib/public/assets/{play-DLG9t8FU.js → play-Cvc8xzic.js} +1 -1
  53. package/lib/public/assets/{plus-nBFSMu_X.js → plus-C9Zj84UF.js} +1 -1
  54. package/lib/public/assets/{profile-page-CSl0UYlv.js → profile-page-Bi27STgs.js} +1 -1
  55. package/lib/public/assets/{reset-password-LeOuHqmB.js → reset-password-CPsOuwSu.js} +1 -1
  56. package/lib/public/assets/runbook-page-DEQ-DBOX.js +304 -0
  57. package/lib/public/assets/{select-88jHcl-Z.js → select-BWpnhWXj.js} +1 -1
  58. package/lib/public/assets/{selector-detail-redirect-BbXKJjZM.js → selector-detail-redirect-BefSCSpe.js} +1 -1
  59. package/lib/public/assets/{selector-health-page-B4rA1Xkv.js → selector-health-page-MSk96Zz7.js} +1 -1
  60. package/lib/public/assets/session-detail-page-DPyBNef_.js +4 -0
  61. package/lib/public/assets/{settings-Dep_3kbA.css → settings-K8UHWMdI.css} +1 -1
  62. package/lib/public/assets/{settings-GF6ZMSQ6.js → settings-V6x-cXwy.js} +1 -1
  63. package/lib/public/assets/{stat-tile-BV98e_Sv.js → stat-tile-Duex-GZD.js} +1 -1
  64. package/lib/public/assets/{tablet-BiAsnFDj.js → tablet-BBcoqBmL.js} +1 -1
  65. package/lib/public/assets/{teams-B4-jrr-1.js → teams-ts8p5rEh.js} +1 -1
  66. package/lib/public/assets/{trash-2-C5RSuX0e.js → trash-2-CZ2ew6AZ.js} +1 -1
  67. package/lib/public/assets/{upload-BG7_rMXK.js → upload-Dv99y_le.js} +1 -1
  68. package/lib/public/assets/{use-builds-data-J-riQYyC.js → use-builds-data-DrYC8JCF.js} +1 -1
  69. package/lib/public/assets/{use-password-reset-mode-C8-eH-UG.js → use-password-reset-mode-C7QkBNkP.js} +1 -1
  70. package/lib/public/assets/{users-CFba5TSy.js → users-D7tKMESH.js} +1 -1
  71. package/lib/public/assets/{users-Cp0beJm3.js → users-gJ5Y01yo.js} +1 -1
  72. package/lib/public/assets/{video-off-DDsEISjV.js → video-off-CqZVuZGs.js} +1 -1
  73. package/lib/public/assets/{webhook-settings-BGu32vSB.js → webhook-settings-DI5DSW99.js} +1 -1
  74. package/lib/public/assets/{zap-l4VaXzq8.js → zap-ClcHFxfo.js} +1 -1
  75. package/lib/public/index.html +1 -1
  76. package/lib/schema.json +16 -12
  77. package/lib/src/InternalHttpClient.js +27 -5
  78. package/lib/src/XenonCapabilityManager.js +10 -3
  79. package/lib/src/app/dashboardPluginLink.js +117 -0
  80. package/lib/src/app/index.js +13 -34
  81. package/lib/src/app/openapi/control.yaml +6 -4
  82. package/lib/src/app/openapi/grid.yaml +8 -2
  83. package/lib/src/app/openapi/identity.yaml +103 -31
  84. package/lib/src/app/openapi/platform.yaml +33 -15
  85. package/lib/src/app/openapi/sessions.yaml +12 -3
  86. package/lib/src/app/routers/apikeys.js +14 -2
  87. package/lib/src/app/routers/auth.js +31 -6
  88. package/lib/src/app/routers/control.js +13 -5
  89. package/lib/src/app/routers/dashboard.js +42 -27
  90. package/lib/src/app/routers/grid.js +5 -3
  91. package/lib/src/app/routers/profile.js +21 -0
  92. package/lib/src/app/routers/users.js +12 -6
  93. package/lib/src/app/swagger.js +2 -0
  94. package/lib/src/app/ws/h264StreamWs.js +10 -1
  95. package/lib/src/app/ws/nodeSocketRelay.js +77 -11
  96. package/lib/src/config.js +1 -0
  97. package/lib/src/dashboard/commandLogFields.js +42 -0
  98. package/lib/src/dashboard/commands.js +5 -5
  99. package/lib/src/dashboard/event-manager.js +64 -204
  100. package/lib/src/dashboard/services/failure-analysis-service.js +118 -105
  101. package/lib/src/dashboard/services/failureCategories.js +264 -0
  102. package/lib/src/dashboard/sessionCommandSummary.js +39 -0
  103. package/lib/src/data-service/device-store.js +14 -1
  104. package/lib/src/data-service/prisma-store.js +19 -7
  105. package/lib/src/data-service/web-config-service.js +7 -0
  106. package/lib/src/device-managers/AndroidDeviceManager.js +61 -23
  107. package/lib/src/device-managers/IOSDeviceManager.js +10 -0
  108. package/lib/src/device-managers/android/AndroidH264StreamService.js +69 -20
  109. package/lib/src/device-managers/android/H264Multiplexer.js +38 -4
  110. package/lib/src/device-utils.js +29 -3
  111. package/lib/src/gateway/defaultGateway.js +1 -1
  112. package/lib/src/gateway/forwardToNode.js +6 -11
  113. package/lib/src/gateway/healReport.js +87 -0
  114. package/lib/src/gateway/hubSessionToken.js +27 -4
  115. package/lib/src/gateway/nodeWebDriverUrl.js +3 -5
  116. package/lib/src/gateway/sessionGateway.js +12 -2
  117. package/lib/src/generated/client/edge.js +4 -3
  118. package/lib/src/generated/client/index-browser.js +1 -0
  119. package/lib/src/generated/client/index.d.ts +32 -0
  120. package/lib/src/generated/client/index.js +4 -3
  121. package/lib/src/generated/client/package.json +1 -1
  122. package/lib/src/generated/client/schema.prisma +3 -0
  123. package/lib/src/generated/client/wasm.js +1 -0
  124. package/lib/src/helpers/outboundProxy.js +219 -0
  125. package/lib/src/index.js +21 -0
  126. package/lib/src/interceptors/CommandInterceptor.js +256 -196
  127. package/lib/src/interceptors/xenonScripts.js +28 -7
  128. package/lib/src/logger.js +3 -1
  129. package/lib/src/middleware/authMiddleware.js +15 -3
  130. package/lib/src/middleware/commandCaller.js +3 -3
  131. package/lib/src/middleware/verifyCredential.js +58 -1
  132. package/lib/src/plugin.js +16 -6
  133. package/lib/src/scripts/database-check.js +3 -3
  134. package/lib/src/scripts/initialize-database.js +35 -33
  135. package/lib/src/scripts/run-migrations.js +1079 -27
  136. package/lib/src/services/AICommandService.js +9 -2
  137. package/lib/src/services/AIService.js +192 -42
  138. package/lib/src/services/ApiKeyService.js +16 -5
  139. package/lib/src/services/EmailService.js +29 -0
  140. package/lib/src/services/InterceptorService.js +6 -2
  141. package/lib/src/services/OrphanSweeper.js +8 -0
  142. package/lib/src/services/SelectorStateService.js +11 -7
  143. package/lib/src/services/SelectorVerificationJob.js +21 -6
  144. package/lib/src/services/ServerManager.js +11 -1
  145. package/lib/src/services/SessionLifecycleService.js +118 -44
  146. package/lib/src/services/SocketClient.js +11 -0
  147. package/lib/src/services/SocketServer.js +380 -45
  148. package/lib/src/services/TeamService.js +5 -1
  149. package/lib/src/services/TracingService.js +171 -60
  150. package/lib/src/services/UserService.js +14 -3
  151. package/lib/src/services/UserSessionService.js +16 -4
  152. package/lib/src/services/bug-report/video-slice.js +4 -1
  153. package/lib/src/services/device-access/leaseOverride.js +7 -5
  154. package/lib/src/services/healing/FuzzyXmlHealingProvider.js +51 -0
  155. package/lib/src/services/healing/HealEtalonService.js +3 -2
  156. package/lib/src/services/healing/HealingOrchestrator.js +57 -72
  157. package/lib/src/services/healing/OcrHealingProvider.js +41 -64
  158. package/lib/src/services/healing/ResilioTreeHealingProvider.js +41 -50
  159. package/lib/src/services/healing/VisualAiHealingProvider.js +1 -1
  160. package/lib/src/services/healing/fingerprintIdentity.js +126 -0
  161. package/lib/src/services/healing/resilioPath.js +274 -0
  162. package/lib/src/services/identity/identityChanges.js +41 -0
  163. package/lib/src/services/logcat/SessionDeviceLogs.js +358 -0
  164. package/lib/src/services/logcat/deviceLogBook.js +164 -0
  165. package/lib/src/services/logcat/iosDriverLog.js +178 -0
  166. package/lib/src/services/ocr/ocrData.js +118 -0
  167. package/lib/src/services/ocr/vendor/LICENSE-tessdata.txt +202 -0
  168. package/lib/src/services/ocr/vendor/eng.traineddata.gz +0 -0
  169. package/lib/src/services/omni-vision/OmniVisionService.js +83 -56
  170. package/lib/src/services/passwordResetLink.js +50 -9
  171. package/lib/src/services/passwordResetMode.js +3 -1
  172. package/lib/src/services/publicUrl.js +45 -0
  173. package/lib/src/services/selector-health/SelectorVisibilityResolver.js +121 -0
  174. package/lib/src/services/session/sessionIdentity.js +5 -3
  175. package/lib/src/services/session/shutdownReason.js +26 -0
  176. package/lib/src/services/settings/aiEngineSettings.js +219 -0
  177. package/lib/src/services/token/JwtKeyService.js +8 -2
  178. package/lib/src/services/token/mcpTokenTtl.js +27 -0
  179. package/lib/src/services/token/mintedLifetime.js +51 -0
  180. package/lib/src/sessions/LocalSession.js +29 -1
  181. package/lib/src/sessions/RemoteSession.js +12 -2
  182. package/lib/src/sessions/SessionManager.js +4 -1
  183. package/lib/src/sessions/sessionMemory.js +25 -0
  184. package/lib/test/helpers/driver-log.js +38 -0
  185. package/lib/test/helpers/fake-ai-provider.js +188 -0
  186. package/lib/test/helpers/fake-http-proxy.js +106 -0
  187. package/lib/test/helpers/loki-stores.js +33 -0
  188. package/lib/test/helpers/otlp-probe.js +107 -0
  189. package/lib/test/helpers/page-sources.js +164 -0
  190. package/lib/test/helpers/scratch-database.js +57 -13
  191. package/lib/test/integration/androidDevices.spec.js +1 -1
  192. package/lib/test/integration/audit-ingest.spec.js +10 -10
  193. package/lib/test/integration/auth-flow.spec.js +9 -14
  194. package/lib/test/integration/auth-rate-limit.spec.js +6 -3
  195. package/lib/test/integration/bug-report-route.spec.js +22 -2
  196. package/lib/test/integration/forgot-password-rate-limit.spec.js +6 -3
  197. package/lib/test/integration/forgot-password.spec.js +17 -18
  198. package/lib/test/integration/ios/01iOSSimulator.spec.js +1 -1
  199. package/lib/test/integration/ios/02iOSDevices.spec.js +1 -1
  200. package/lib/test/integration/legacy-deprecation-headers.spec.js +5 -2
  201. package/lib/test/integration/migration.spec.js +6 -1
  202. package/lib/test/integration/node-pair-auth-rest.spec.js +5 -9
  203. package/lib/test/integration/ports-router.spec.js +4 -4
  204. package/lib/test/integration/prisma-device-store-add-devices.spec.js +2 -3
  205. package/lib/test/integration/prisma-device-store-team-filter.spec.js +2 -5
  206. package/lib/test/integration/profile-tokens.spec.js +10 -16
  207. package/lib/test/integration/recordings-router.spec.js +16 -16
  208. package/lib/test/integration/reset-revokes-sessions.spec.js +4 -9
  209. package/lib/test/integration/role-matrix.spec.js +4 -9
  210. package/lib/test/integration/sdk-leases-router.spec.js +9 -9
  211. package/lib/test/integration/sdk-version.spec.js +2 -2
  212. package/lib/test/integration/selector-health-endpoint.spec.js +5 -5
  213. package/lib/test/integration/selector-state-flow.spec.js +5 -5
  214. package/lib/test/integration/team-membership-migration.spec.js +73 -12
  215. package/lib/test/integration/team-visibility-apps.spec.js +15 -22
  216. package/lib/test/integration/team-visibility-control.spec.js +96 -69
  217. package/lib/test/integration/team-visibility-dashboard.spec.js +7 -23
  218. package/lib/test/integration/team-visibility-grid.spec.js +16 -15
  219. package/lib/test/integration/team-visibility-leases.spec.js +8 -11
  220. package/lib/test/integration/team-visibility-recordings.spec.js +6 -19
  221. package/lib/test/integration/team-visibility-sessions.spec.js +6 -19
  222. package/lib/test/integration/users-flow.spec.js +7 -12
  223. package/lib/test/integration/users-lockout.spec.js +5 -3
  224. package/lib/test/unit/EmailService.test.js +66 -0
  225. package/lib/test/unit/ResilioTreeHealingProvider.test.js +46 -19
  226. package/lib/test/unit/XenonCapabilityManager.spec.js +23 -0
  227. package/lib/test/unit/ai-call-time-limit.spec.js +130 -0
  228. package/lib/test/unit/ai-engine-settings.spec.js +338 -0
  229. package/lib/test/unit/ai-provider-switch.spec.js +130 -0
  230. package/lib/test/unit/ai-rate-limit.spec.js +126 -0
  231. package/lib/test/unit/android-adb-retry.spec.js +112 -0
  232. package/lib/test/unit/android-h264-viewers.spec.js +272 -0
  233. package/lib/test/unit/android-preview-hold.spec.js +3 -2
  234. package/lib/test/unit/apikeys-router.spec.js +84 -0
  235. package/lib/test/unit/authMiddleware.bearer.spec.js +9 -0
  236. package/lib/test/unit/authMiddleware.credentialLifetime.spec.js +149 -0
  237. package/lib/test/unit/bug-report/video-slice.spec.js +19 -0
  238. package/lib/test/unit/command-log-network-capture.spec.js +191 -0
  239. package/lib/test/unit/dashboard-events-device-logs.spec.js +255 -0
  240. package/lib/test/unit/dashboard-events-team-scope.spec.js +6 -3
  241. package/lib/test/unit/dashboard-plugin-link.spec.js +213 -0
  242. package/lib/test/unit/database-check.spec.js +1 -0
  243. package/lib/test/unit/device-log-book.spec.js +180 -0
  244. package/lib/test/unit/device-utils.spec.js +13 -4
  245. package/lib/test/unit/enable-dashboard-option.spec.js +57 -0
  246. package/lib/test/unit/event-log-session-data.spec.js +251 -0
  247. package/lib/test/unit/failure-analysis.spec.js +196 -0
  248. package/lib/test/unit/failure-categories.spec.js +130 -0
  249. package/lib/test/unit/failure-categorization.spec.js +393 -0
  250. package/lib/test/unit/fingerprint-identity.spec.js +433 -0
  251. package/lib/test/unit/h264-multiplexer.spec.js +46 -0
  252. package/lib/test/unit/heal-report.spec.js +71 -0
  253. package/lib/test/unit/healing-instrumentation.spec.js +6 -1
  254. package/lib/test/unit/healing-tier-gate.spec.js +74 -29
  255. package/lib/test/unit/healing-tiers-every-session.spec.js +219 -0
  256. package/lib/test/unit/hub-node-gateway-appium-server.spec.js +243 -1
  257. package/lib/test/unit/interceptor-visual-elements.spec.js +192 -0
  258. package/lib/test/unit/ios-driver-log.spec.js +168 -0
  259. package/lib/test/unit/ios-simulator-session-readiness.spec.js +187 -0
  260. package/lib/test/unit/ios-simulator-session-teardown.spec.js +158 -0
  261. package/lib/test/unit/lease/lease-override.spec.js +23 -6
  262. package/lib/test/unit/lease/session-lease-proof.spec.js +16 -4
  263. package/lib/test/unit/logcat-ws.spec.js +78 -37
  264. package/lib/test/unit/logger-json-levels.spec.js +44 -0
  265. package/lib/test/unit/mcp-token-ttl.spec.js +38 -0
  266. package/lib/test/unit/observability-example-loki.spec.js +25 -0
  267. package/lib/test/unit/ocr-healing-tier-real-ocr.spec.js +113 -0
  268. package/lib/test/unit/ocr-healing-tier.spec.js +122 -0
  269. package/lib/test/unit/ocr-offline.spec.js +213 -0
  270. package/lib/test/unit/omni-virtual-element-store.spec.js +301 -0
  271. package/lib/test/unit/omni-vision-ios-points.spec.js +0 -2
  272. package/lib/test/unit/omni-vision.spec.js +5 -7
  273. package/lib/test/unit/openapi-for-site.spec.js +80 -0
  274. package/lib/test/unit/outbound-proxy.spec.js +320 -0
  275. package/lib/test/unit/package-engines.spec.js +27 -0
  276. package/lib/test/unit/passwordResetMode.test.js +10 -2
  277. package/lib/test/unit/port-exhaustion.spec.js +2 -1
  278. package/lib/test/unit/profile-router.test.js +122 -1
  279. package/lib/test/unit/proxy-option.spec.js +358 -0
  280. package/lib/test/unit/queue-requester.spec.js +1 -1
  281. package/lib/test/unit/reset-link-address.spec.js +266 -0
  282. package/lib/test/unit/reset-link.test.js +10 -4
  283. package/lib/test/unit/resilio-path-healing.spec.js +270 -0
  284. package/lib/test/unit/run-migrations-database.spec.js +807 -0
  285. package/lib/test/unit/run-migrations-fail-fast.test.js +41 -27
  286. package/lib/test/unit/run-migrations.test.js +864 -0
  287. package/lib/test/unit/selector-health-actions.spec.js +3 -1
  288. package/lib/test/unit/selector-learning-every-session.spec.js +179 -0
  289. package/lib/test/unit/selector-state-service.spec.js +72 -23
  290. package/lib/test/unit/selector-verification-clean-build.spec.js +130 -0
  291. package/lib/test/unit/selector-verification-job.spec.js +28 -19
  292. package/lib/test/unit/selector-verification-missed-regression.spec.js +2 -2
  293. package/lib/test/unit/selector-visibility-resolver.spec.js +209 -0
  294. package/lib/test/unit/self-healing-boot.spec.js +10 -5
  295. package/lib/test/unit/self-healing-setting.spec.js +7 -6
  296. package/lib/test/unit/session-app-resolution.spec.js +5 -1
  297. package/lib/test/unit/session-attribution-wiring.spec.js +41 -13
  298. package/lib/test/unit/session-create-credential-owner.spec.js +242 -0
  299. package/lib/test/unit/session-credential-strip.spec.js +1 -1
  300. package/lib/test/unit/session-device-logs-ios.spec.js +180 -0
  301. package/lib/test/unit/session-device-logs.spec.js +356 -0
  302. package/lib/test/unit/session-end-failure-analysis.spec.js +133 -0
  303. package/lib/test/unit/session-log-indexes.spec.js +64 -3
  304. package/lib/test/unit/socket-team-scope.integration.spec.js +749 -0
  305. package/lib/test/unit/socketServer.scopedEmit.spec.js +237 -5
  306. package/lib/test/unit/socketServer.sessionCookie.spec.js +2 -1
  307. package/lib/test/unit/stream-start-pipelines.spec.js +72 -18
  308. package/lib/test/unit/token-mint-limits.spec.js +165 -0
  309. package/lib/test/unit/tracing-export-routing.spec.js +137 -0
  310. package/lib/test/unit/tracing-session-spans.spec.js +337 -0
  311. package/lib/test/unit/unexpected-shutdown-reason.spec.js +163 -0
  312. package/lib/test/unit/verify-credential.spec.js +33 -0
  313. package/lib/test/unit/vision-assertion.spec.js +3 -2
  314. package/lib/test/unit/web-config-service.spec.js +19 -0
  315. package/lib/tsconfig.tsbuildinfo +1 -1
  316. package/package.json +6 -5
  317. package/prisma/migrations/20261004174413_locator_etalon_path/migration.sql +2 -0
  318. package/prisma/schema.prisma +3 -0
  319. package/schema.json +16 -12
  320. package/scripts/dev/readme-screenshots.js +276 -23
  321. package/scripts/dev/screenshot-fixtures/logcat.js +157 -0
  322. package/scripts/dev/screenshot-fixtures/phone-screen.html +469 -0
  323. package/scripts/dev/screenshot-fixtures/session.js +242 -0
  324. package/scripts/export-openapi.js +32 -0
  325. package/scripts/lib/openapi-for-site.js +79 -0
  326. package/lib/public/assets/BugReportButton-BAt0_pzq.js +0 -15
  327. package/lib/public/assets/DeviceMosaicView-BYaQHhTf.js +0 -2
  328. package/lib/public/assets/ai-settings-CsuJroRf.js +0 -21
  329. package/lib/public/assets/device-explorer-CdO6_nrn.js +0 -282
  330. package/lib/public/assets/forgot-password-CGnkRgE9.js +0 -1
  331. package/lib/public/assets/index-BbKEXUFH.js +0 -219
  332. package/lib/public/assets/runbook-page-CMMeQml9.js +0 -124
  333. package/lib/public/assets/session-detail-page-COL_RZxw.js +0 -4
  334. package/lib/src/scripts/prepare-prisma.js +0 -89
  335. /package/lib/public/assets/{BugReportButton-CPBjfiv4.css → index-CPBjfiv4.css} +0 -0
@@ -13,6 +13,7 @@ require("reflect-metadata");
13
13
  const chai_1 = require("chai");
14
14
  const prisma_store_1 = require("../../src/data-service/prisma-store");
15
15
  const prisma_1 = require("../../src/prisma");
16
+ const scratch_database_1 = require("../helpers/scratch-database");
16
17
  /**
17
18
  * PrismaDeviceStore.addDevices, run as real queries.
18
19
  *
@@ -23,6 +24,7 @@ const prisma_1 = require("../../src/prisma");
23
24
  */
24
25
  describe('PrismaDeviceStore.addDevices (integration)', function () {
25
26
  this.timeout(60000);
27
+ (0, scratch_database_1.useScratchDatabase)({ wholeSuite: true });
26
28
  const store = new prisma_store_1.PrismaDeviceStore();
27
29
  const stamp = Date.now();
28
30
  const HOST = `http://add-devices-${stamp}:4723`;
@@ -46,9 +48,6 @@ describe('PrismaDeviceStore.addDevices (integration)', function () {
46
48
  },
47
49
  });
48
50
  }));
49
- after(() => __awaiter(this, void 0, void 0, function* () {
50
- yield prisma_1.prisma.device.deleteMany({ where: { udid: { in: UDIDS } } });
51
- }));
52
51
  it('refreshes a known phone’s discovery columns but leaves its session alone', () => __awaiter(this, void 0, void 0, function* () {
53
52
  const added = yield store.addDevices([phone(KNOWN, { sdk: '11', sessionStartTime: 0 })]);
54
53
  (0, chai_1.expect)(added).to.deep.equal([]);
@@ -15,6 +15,7 @@ const typedi_1 = require("typedi");
15
15
  const prisma_store_1 = require("../../src/data-service/prisma-store");
16
16
  const TeamService_1 = require("../../src/services/TeamService");
17
17
  const prisma_1 = require("../../src/prisma");
18
+ const scratch_database_1 = require("../helpers/scratch-database");
18
19
  /**
19
20
  * The team filter on the Prisma device store, run as a real query.
20
21
  *
@@ -25,6 +26,7 @@ const prisma_1 = require("../../src/prisma");
25
26
  */
26
27
  describe('PrismaDeviceStore team filter (integration)', function () {
27
28
  this.timeout(60000);
29
+ (0, scratch_database_1.useScratchDatabase)({ wholeSuite: true });
28
30
  const store = new prisma_store_1.PrismaDeviceStore();
29
31
  let teamA;
30
32
  let teamB;
@@ -51,11 +53,6 @@ describe('PrismaDeviceStore team filter (integration)', function () {
51
53
  beforeEach(() => __awaiter(this, void 0, void 0, function* () {
52
54
  yield prisma_1.prisma.device.updateMany({ where: { udid: { in: UDIDS } }, data: { busy: false } });
53
55
  }));
54
- after(() => __awaiter(this, void 0, void 0, function* () {
55
- yield prisma_1.prisma.device.deleteMany({ where: { udid: { in: UDIDS } } });
56
- yield prisma_1.prisma.team.delete({ where: { id: teamA.id } }).catch(() => undefined);
57
- yield prisma_1.prisma.team.delete({ where: { id: teamB.id } }).catch(() => undefined);
58
- }));
59
56
  const udidsOf = (rows) => rows.map((r) => r.udid).sort();
60
57
  const busy = (udid) => __awaiter(this, void 0, void 0, function* () { var _a; return !!((_a = (yield prisma_1.prisma.device.findFirst({ where: { udid, host: HOST } }))) === null || _a === void 0 ? void 0 : _a.busy); });
61
58
  it('getDevices with one team returns the shared phone and that team’s', () => __awaiter(this, void 0, void 0, function* () {
@@ -15,20 +15,19 @@ 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 profile_1 = require("../../src/app/routers/profile");
20
20
  const authMiddleware_1 = require("../../src/middleware/authMiddleware");
21
21
  const typedi_1 = require("typedi");
22
22
  const UserService_1 = require("../../src/services/UserService");
23
23
  const UserSessionService_1 = require("../../src/services/UserSessionService");
24
- const prisma_1 = require("../../src/prisma");
24
+ const scratch_database_1 = require("../helpers/scratch-database");
25
25
  describe('profile/tokens (integration)', function () {
26
26
  this.timeout(30000);
27
+ (0, scratch_database_1.useScratchDatabase)({ wholeSuite: true });
27
28
  let user;
28
29
  let cookie;
29
30
  before(() => __awaiter(this, void 0, void 0, function* () {
30
- yield prisma_1.prisma.userSession.deleteMany({ where: { user: { email: 'tokens-it@xenon.local' } } });
31
- yield prisma_1.prisma.user.deleteMany({ where: { email: 'tokens-it@xenon.local' } });
32
31
  user = yield typedi_1.Container.get(UserService_1.UserService).createUser({
33
32
  email: 'tokens-it@xenon.local',
34
33
  name: 'Tokens IT',
@@ -38,40 +37,35 @@ describe('profile/tokens (integration)', function () {
38
37
  const session = yield typedi_1.Container.get(UserSessionService_1.UserSessionService).create(user.id);
39
38
  cookie = `xenon_dashboard_session=${session.id}`;
40
39
  }));
41
- after(() => __awaiter(this, void 0, void 0, function* () {
42
- yield prisma_1.prisma.apiKey.deleteMany({ where: { userId: user.id } });
43
- yield prisma_1.prisma.userSession.deleteMany({ where: { userId: user.id } });
44
- yield prisma_1.prisma.user.delete({ where: { id: user.id } });
45
- }));
46
40
  it('creates and lists a token; rotating accessKey keeps the token valid', () => __awaiter(this, void 0, void 0, function* () {
47
41
  const app = (0, express_1.default)();
48
42
  app.use(express_1.default.json());
49
43
  app.use(authMiddleware_1.authMiddleware);
50
44
  app.use('/profile', (0, profile_1.profileRouter)());
51
- const created = yield (0, supertest_1.default)(app)
45
+ const created = yield (0, loopbackRequest_1.default)(app)
52
46
  .post('/profile/tokens')
53
47
  .set('Cookie', cookie)
54
48
  .send({ name: 'CI' });
55
49
  (0, chai_1.expect)(created.status).to.equal(201);
56
50
  const token = created.body.token;
57
- const list = yield (0, supertest_1.default)(app).get('/profile/tokens').set('Cookie', cookie);
51
+ const list = yield (0, loopbackRequest_1.default)(app).get('/profile/tokens').set('Cookie', cookie);
58
52
  (0, chai_1.expect)(list.body.length).to.equal(1);
59
- const me = yield (0, supertest_1.default)(app).get('/profile/access-key').set('Cookie', cookie);
53
+ const me = yield (0, loopbackRequest_1.default)(app).get('/profile/access-key').set('Cookie', cookie);
60
54
  const ak = me.body.accessKey;
61
- const headerCheck = yield (0, supertest_1.default)(app)
55
+ const headerCheck = yield (0, loopbackRequest_1.default)(app)
62
56
  .get('/profile/tokens')
63
57
  .set('x-xenon-access-key', ak)
64
58
  .set('x-xenon-token', token);
65
59
  (0, chai_1.expect)(headerCheck.status).to.equal(200);
66
- const rotated = yield (0, supertest_1.default)(app).post('/profile/access-key/rotate').set('Cookie', cookie);
60
+ const rotated = yield (0, loopbackRequest_1.default)(app).post('/profile/access-key/rotate').set('Cookie', cookie);
67
61
  const newAk = rotated.body.accessKey;
68
62
  (0, chai_1.expect)(newAk).to.not.equal(ak);
69
- const afterRotate = yield (0, supertest_1.default)(app)
63
+ const afterRotate = yield (0, loopbackRequest_1.default)(app)
70
64
  .get('/profile/tokens')
71
65
  .set('x-xenon-access-key', newAk)
72
66
  .set('x-xenon-token', token);
73
67
  (0, chai_1.expect)(afterRotate.status).to.equal(200);
74
- const oldFails = yield (0, supertest_1.default)(app)
68
+ const oldFails = yield (0, loopbackRequest_1.default)(app)
75
69
  .get('/profile/tokens')
76
70
  .set('x-xenon-access-key', ak)
77
71
  .set('x-xenon-token', token);
@@ -16,7 +16,7 @@ require("reflect-metadata");
16
16
  const chai_1 = require("chai");
17
17
  const sinon_1 = __importDefault(require("sinon"));
18
18
  const express_1 = __importDefault(require("express"));
19
- const supertest_1 = __importDefault(require("supertest"));
19
+ const loopbackRequest_1 = __importDefault(require("../helpers/loopbackRequest"));
20
20
  const typedi_1 = require("typedi");
21
21
  const recordings_1 = __importDefault(require("../../src/app/routers/recordings"));
22
22
  const authMiddleware_1 = require("../../src/middleware/authMiddleware");
@@ -76,7 +76,7 @@ describe('Recordings router (integration)', () => {
76
76
  startedAt: new Date(),
77
77
  compositeEnabled: false,
78
78
  });
79
- const r = yield (0, supertest_1.default)(makeApp())
79
+ const r = yield (0, loopbackRequest_1.default)(makeApp())
80
80
  .post('/xenon/api/recordings')
81
81
  .send({ udids: ['U1'] });
82
82
  (0, chai_1.expect)(r.status).to.equal(202);
@@ -88,7 +88,7 @@ describe('Recordings router (integration)', () => {
88
88
  .rejects(new RecordingOrchestrator_1.RecordingError('device_busy', [
89
89
  { udid: 'U2', reason: 'automation', sessionId: 'sess-x' },
90
90
  ]));
91
- const r = yield (0, supertest_1.default)(makeApp())
91
+ const r = yield (0, loopbackRequest_1.default)(makeApp())
92
92
  .post('/xenon/api/recordings')
93
93
  .send({ udids: ['U1', 'U2'] });
94
94
  (0, chai_1.expect)(r.status).to.equal(409);
@@ -101,7 +101,7 @@ describe('Recordings router (integration)', () => {
101
101
  sinon_1.default
102
102
  .stub(typedi_1.Container.get(RecordingOrchestrator_1.RecordingOrchestrator), 'start')
103
103
  .rejects(new RecordingOrchestrator_1.RecordingError('concurrency_cap', undefined, 4, 4));
104
- const r = yield (0, supertest_1.default)(makeApp())
104
+ const r = yield (0, loopbackRequest_1.default)(makeApp())
105
105
  .post('/xenon/api/recordings')
106
106
  .send({ udids: ['U1'] });
107
107
  (0, chai_1.expect)(r.status).to.equal(409);
@@ -109,16 +109,16 @@ describe('Recordings router (integration)', () => {
109
109
  }));
110
110
  it('POST /recordings: 400 when udids missing or empty', () => __awaiter(void 0, void 0, void 0, function* () {
111
111
  const app = makeApp();
112
- yield (0, supertest_1.default)(app).post('/xenon/api/recordings').send({}).expect(400);
113
- yield (0, supertest_1.default)(app).post('/xenon/api/recordings').send({ udids: [] }).expect(400);
114
- yield (0, supertest_1.default)(app).post('/xenon/api/recordings').send({ udids: [123] }).expect(400);
112
+ yield (0, loopbackRequest_1.default)(app).post('/xenon/api/recordings').send({}).expect(400);
113
+ yield (0, loopbackRequest_1.default)(app).post('/xenon/api/recordings').send({ udids: [] }).expect(400);
114
+ yield (0, loopbackRequest_1.default)(app).post('/xenon/api/recordings').send({ udids: [123] }).expect(400);
115
115
  }));
116
116
  it('POST /recordings/:groupId/stop: 200', () => __awaiter(void 0, void 0, void 0, function* () {
117
117
  sinon_1.default.stub(typedi_1.Container.get(RecordingOrchestrator_1.RecordingOrchestrator), 'stop').resolves({
118
118
  groupId: 'g-1',
119
119
  recordings: [{ id: 'r-1', udid: 'U1', status: 'STOPPED', durationMs: 5000, sizeBytes: 1024 }],
120
120
  });
121
- const r = yield (0, supertest_1.default)(makeApp()).post('/xenon/api/recordings/g-1/stop');
121
+ const r = yield (0, loopbackRequest_1.default)(makeApp()).post('/xenon/api/recordings/g-1/stop');
122
122
  (0, chai_1.expect)(r.status).to.equal(200);
123
123
  (0, chai_1.expect)(r.body.recordings[0].status).to.equal('STOPPED');
124
124
  }));
@@ -127,11 +127,11 @@ describe('Recordings router (integration)', () => {
127
127
  sinon_1.default
128
128
  .stub(typedi_1.Container.get(RecordingOrchestrator_1.RecordingOrchestrator), 'addBookmark')
129
129
  .resolves({ id: 'bm-1', label: 'bug here' });
130
- const ok = yield (0, supertest_1.default)(makeApp())
130
+ const ok = yield (0, loopbackRequest_1.default)(makeApp())
131
131
  .post('/xenon/api/recordings/g-1/bookmark')
132
132
  .send({ recordingId: 'r-1', timecodeMs: 1000, label: 'bug here' });
133
133
  (0, chai_1.expect)(ok.status).to.equal(201);
134
- const bad = yield (0, supertest_1.default)(makeApp())
134
+ const bad = yield (0, loopbackRequest_1.default)(makeApp())
135
135
  .post('/xenon/api/recordings/g-1/bookmark')
136
136
  .send({ timecodeMs: 1000 });
137
137
  (0, chai_1.expect)(bad.status).to.equal(400);
@@ -166,7 +166,7 @@ describe('Recordings router (integration)', () => {
166
166
  startedAt: new Date(),
167
167
  compositeEnabled: false,
168
168
  });
169
- const r = yield (0, supertest_1.default)(makeBearerApp())
169
+ const r = yield (0, loopbackRequest_1.default)(makeBearerApp())
170
170
  .post('/xenon/api/recordings')
171
171
  .send({ udids: ['U1'] });
172
172
  (0, chai_1.expect)(r.status).to.equal(202);
@@ -176,7 +176,7 @@ describe('Recordings router (integration)', () => {
176
176
  groupId: 'g-1',
177
177
  recordings: [{ id: 'r-2', udid: 'U2', status: 'RECORDING' }],
178
178
  });
179
- const r = yield (0, supertest_1.default)(makeBearerApp())
179
+ const r = yield (0, loopbackRequest_1.default)(makeBearerApp())
180
180
  .post('/xenon/api/recordings/g-1/add-device')
181
181
  .send({ udid: 'U2' });
182
182
  (0, chai_1.expect)(r.status).to.equal(201);
@@ -215,7 +215,7 @@ describe('Recordings router (integration)', () => {
215
215
  startedAt: new Date(),
216
216
  compositeEnabled: false,
217
217
  });
218
- const r = yield (0, supertest_1.default)(makeHeaderPairApp())
218
+ const r = yield (0, loopbackRequest_1.default)(makeHeaderPairApp())
219
219
  .post('/xenon/api/recordings')
220
220
  .send({ udids: ['U1'] });
221
221
  (0, chai_1.expect)(r.status).to.equal(202);
@@ -227,7 +227,7 @@ describe('Recordings router (integration)', () => {
227
227
  groupId: 'g-1',
228
228
  recordings: [{ id: 'r-2', udid: 'U2', status: 'RECORDING' }],
229
229
  });
230
- const r = yield (0, supertest_1.default)(makeHeaderPairApp())
230
+ const r = yield (0, loopbackRequest_1.default)(makeHeaderPairApp())
231
231
  .post('/xenon/api/recordings/g-1/add-device')
232
232
  .send({ udid: 'U2' });
233
233
  (0, chai_1.expect)(r.status).to.equal(201);
@@ -239,7 +239,7 @@ describe('Recordings router (integration)', () => {
239
239
  sinon_1.default
240
240
  .stub(typedi_1.Container.get(RecordingOrchestrator_1.RecordingOrchestrator), 'addAnnotation')
241
241
  .resolves({ id: 'an-1' });
242
- const ok = yield (0, supertest_1.default)(makeApp())
242
+ const ok = yield (0, loopbackRequest_1.default)(makeApp())
243
243
  .post('/xenon/api/recordings/g-1/annotation')
244
244
  .send({
245
245
  recordingId: 'r-1',
@@ -249,7 +249,7 @@ describe('Recordings router (integration)', () => {
249
249
  color: '#ff0000',
250
250
  });
251
251
  (0, chai_1.expect)(ok.status).to.equal(201);
252
- const bad = yield (0, supertest_1.default)(makeApp())
252
+ const bad = yield (0, loopbackRequest_1.default)(makeApp())
253
253
  .post('/xenon/api/recordings/g-1/annotation')
254
254
  .send({ shape: 'RECT' });
255
255
  (0, chai_1.expect)(bad.status).to.equal(400);
@@ -15,20 +15,20 @@ 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 auth_1 = require("../../src/app/routers/auth");
20
20
  const typedi_1 = require("typedi");
21
21
  const UserService_1 = require("../../src/services/UserService");
22
22
  const UserSessionService_1 = require("../../src/services/UserSessionService");
23
23
  const PasswordResetService_1 = require("../../src/services/PasswordResetService");
24
24
  const prisma_1 = require("../../src/prisma");
25
+ const scratch_database_1 = require("../helpers/scratch-database");
25
26
  describe('reset-password revokes all sessions', function () {
26
27
  this.timeout(30000);
28
+ (0, scratch_database_1.useScratchDatabase)({ wholeSuite: true });
27
29
  const email = 'reset-revokes-it@xenon.local';
28
30
  let user;
29
31
  before(() => __awaiter(this, void 0, void 0, function* () {
30
- yield prisma_1.prisma.userSession.deleteMany({ where: { user: { email } } });
31
- yield prisma_1.prisma.user.deleteMany({ where: { email } });
32
32
  user = yield typedi_1.Container.get(UserService_1.UserService).createUser({
33
33
  email,
34
34
  name: 'Reset Revokes IT',
@@ -36,11 +36,6 @@ describe('reset-password revokes all sessions', function () {
36
36
  role: 'MEMBER',
37
37
  });
38
38
  }));
39
- after(() => __awaiter(this, void 0, void 0, function* () {
40
- yield prisma_1.prisma.passwordResetToken.deleteMany({ where: { userId: user.id } });
41
- yield prisma_1.prisma.userSession.deleteMany({ where: { userId: user.id } });
42
- yield prisma_1.prisma.user.delete({ where: { id: user.id } });
43
- }));
44
39
  it('deletes every UserSession for the resetting user', () => __awaiter(this, void 0, void 0, function* () {
45
40
  const sessions = [
46
41
  yield typedi_1.Container.get(UserSessionService_1.UserSessionService).create(user.id),
@@ -51,7 +46,7 @@ describe('reset-password revokes all sessions', function () {
51
46
  const app = (0, express_1.default)();
52
47
  app.use(express_1.default.json());
53
48
  app.use('/auth', (0, auth_1.authPublicRouter)());
54
- const r = yield (0, supertest_1.default)(app)
49
+ const r = yield (0, loopbackRequest_1.default)(app)
55
50
  .post('/auth/reset-password')
56
51
  .send({ token: raw, newPassword: 'BrandNewPw1' });
57
52
  (0, chai_1.expect)(r.status).to.equal(204);
@@ -15,13 +15,13 @@ 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 authMiddleware_1 = require("../../src/middleware/authMiddleware");
20
20
  const apikeys_1 = require("../../src/app/routers/apikeys");
21
21
  const teams_1 = require("../../src/app/routers/teams");
22
22
  const processes_1 = require("../../src/app/routers/processes");
23
23
  const users_1 = require("../../src/app/routers/users");
24
- const prisma_1 = require("../../src/prisma");
24
+ const scratch_database_1 = require("../helpers/scratch-database");
25
25
  const seedUser_1 = require("../helpers/seedUser");
26
26
  // scopesForRole now grants ADMIN the full 'admin,devices,sessions,read' set on
27
27
  // cookie sessions (the user themselves logged in via /login). Token-default
@@ -84,6 +84,7 @@ function buildApp() {
84
84
  }
85
85
  describe('role matrix (integration)', function () {
86
86
  this.timeout(60000);
87
+ (0, scratch_database_1.useScratchDatabase)({ wholeSuite: true });
87
88
  let users;
88
89
  before(() => __awaiter(this, void 0, void 0, function* () {
89
90
  users = {
@@ -92,18 +93,12 @@ describe('role matrix (integration)', function () {
92
93
  MEMBER: yield (0, seedUser_1.seedUser)('MEMBER'),
93
94
  };
94
95
  }));
95
- after(() => __awaiter(this, void 0, void 0, function* () {
96
- yield users.SUPER_ADMIN.cleanup();
97
- yield users.ADMIN.cleanup();
98
- yield users.MEMBER.cleanup();
99
- yield prisma_1.prisma.user.deleteMany({ where: { email: { contains: 'rmcase-' } } });
100
- }));
101
96
  CASES.forEach((c) => {
102
97
  ['SUPER_ADMIN', 'ADMIN', 'MEMBER'].forEach((role) => {
103
98
  it(`${role} ${c.method} ${c.path} → expect ${c.expect[role]}`, () => __awaiter(this, void 0, void 0, function* () {
104
99
  const app = buildApp();
105
100
  const expected = c.expect[role];
106
- const reqBuilder = (0, supertest_1.default)(app)[c.method.toLowerCase()](c.path).set('Cookie', users[role].cookie);
101
+ const reqBuilder = (0, loopbackRequest_1.default)(app)[c.method.toLowerCase()](c.path).set('Cookie', users[role].cookie);
107
102
  const body = typeof c.body === 'function' ? c.body() : c.body;
108
103
  const r = body ? yield reqBuilder.send(body) : yield reqBuilder;
109
104
  if (expected >= 200 && expected < 300) {
@@ -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 sinon_1 = __importDefault(require("sinon"));
20
20
  // Static imports (recordings-router.spec idiom): the previous extensionless
21
21
  // dynamic `await import(...)` fails to resolve when Node's native ESM loader
@@ -53,7 +53,7 @@ describe('POST /xenon/api/sdk/leases', () => {
53
53
  allocatedPorts: { systemPort: 9001 },
54
54
  appiumCapabilities: { 'appium:udid': 'u1', 'xe:options': { leaseId: 'lse_1' } },
55
55
  });
56
- const res = yield (0, supertest_1.default)(app)
56
+ const res = yield (0, loopbackRequest_1.default)(app)
57
57
  .post('/xenon/api/sdk/leases')
58
58
  .send({ filters: { platform: 'android' }, durationMs: 60000, heartbeatSeconds: 30 });
59
59
  (0, chai_1.expect)(res.status).to.equal(201);
@@ -62,7 +62,7 @@ describe('POST /xenon/api/sdk/leases', () => {
62
62
  }));
63
63
  it('records the api-key principal as actor/team on create', () => __awaiter(void 0, void 0, void 0, function* () {
64
64
  leaseSvc.create.resolves({ leaseId: 'lse_1', leaseToken: 't'.repeat(64) });
65
- yield (0, supertest_1.default)(app)
65
+ yield (0, loopbackRequest_1.default)(app)
66
66
  .post('/xenon/api/sdk/leases')
67
67
  .send({ filters: { platform: 'android' } });
68
68
  (0, chai_1.expect)(leaseSvc.create.calledOnce).to.equal(true);
@@ -91,7 +91,7 @@ describe('POST /xenon/api/sdk/leases', () => {
91
91
  });
92
92
  bearerApp.use('/xenon/api/sdk/leases', (0, sdk_leases_1.makeRouter)({ leaseService: leaseSvc }));
93
93
  leaseSvc.create.resolves({ leaseId: 'lse_2', leaseToken: 't'.repeat(64) });
94
- const res = yield (0, supertest_1.default)(bearerApp)
94
+ const res = yield (0, loopbackRequest_1.default)(bearerApp)
95
95
  .post('/xenon/api/sdk/leases')
96
96
  .send({ filters: { platform: 'android' } });
97
97
  (0, chai_1.expect)(res.status).to.equal(201);
@@ -103,7 +103,7 @@ describe('POST /xenon/api/sdk/leases', () => {
103
103
  }));
104
104
  it('returns 404 when no device matches', () => __awaiter(void 0, void 0, void 0, function* () {
105
105
  leaseSvc.create.rejects(new LeaseService_1.NoMatchingDevice('no device'));
106
- const res = yield (0, supertest_1.default)(app)
106
+ const res = yield (0, loopbackRequest_1.default)(app)
107
107
  .post('/xenon/api/sdk/leases')
108
108
  .send({ filters: { platform: 'android', udid: 'absent' }, durationMs: 60000 });
109
109
  (0, chai_1.expect)(res.status).to.equal(404);
@@ -111,23 +111,23 @@ describe('POST /xenon/api/sdk/leases', () => {
111
111
  }));
112
112
  it('heartbeat: 200 with valid token; 403 without', () => __awaiter(void 0, void 0, void 0, function* () {
113
113
  leaseSvc.heartbeat.resolves({ heartbeatedAt: Date.now(), expiresAt: Date.now() + 60000 });
114
- const res1 = yield (0, supertest_1.default)(app)
114
+ const res1 = yield (0, loopbackRequest_1.default)(app)
115
115
  .post('/xenon/api/sdk/leases/lse_1/heartbeat')
116
116
  .set('x-xenon-lease-token', 'cleartext');
117
117
  (0, chai_1.expect)(res1.status).to.equal(200);
118
- const res2 = yield (0, supertest_1.default)(app).post('/xenon/api/sdk/leases/lse_1/heartbeat');
118
+ const res2 = yield (0, loopbackRequest_1.default)(app).post('/xenon/api/sdk/leases/lse_1/heartbeat');
119
119
  (0, chai_1.expect)(res2.status).to.equal(403);
120
120
  }));
121
121
  it('heartbeat: 403 on token mismatch from service', () => __awaiter(void 0, void 0, void 0, function* () {
122
122
  leaseSvc.heartbeat.rejects(new LeaseService_1.LeaseTokenMismatch());
123
- const res = yield (0, supertest_1.default)(app)
123
+ const res = yield (0, loopbackRequest_1.default)(app)
124
124
  .post('/xenon/api/sdk/leases/lse_1/heartbeat')
125
125
  .set('x-xenon-lease-token', 'wrong');
126
126
  (0, chai_1.expect)(res.status).to.equal(403);
127
127
  }));
128
128
  it('heartbeat: 410 Gone on expired lease', () => __awaiter(void 0, void 0, void 0, function* () {
129
129
  leaseSvc.heartbeat.rejects(new LeaseService_1.LeaseGone('expired'));
130
- const res = yield (0, supertest_1.default)(app)
130
+ const res = yield (0, loopbackRequest_1.default)(app)
131
131
  .post('/xenon/api/sdk/leases/lse_1/heartbeat')
132
132
  .set('x-xenon-lease-token', 'whatever');
133
133
  (0, chai_1.expect)(res.status).to.equal(410);
@@ -48,7 +48,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
48
48
  require("reflect-metadata");
49
49
  const chai_1 = require("chai");
50
50
  const express_1 = __importStar(require("express"));
51
- const supertest_1 = __importDefault(require("supertest"));
51
+ const loopbackRequest_1 = __importDefault(require("../helpers/loopbackRequest"));
52
52
  describe('GET /xenon/api/sdk/version', () => {
53
53
  it('returns pluginVersion + supports array', () => __awaiter(void 0, void 0, void 0, function* () {
54
54
  const register = (yield Promise.resolve().then(() => __importStar(require('../../src/app/routers/sdk-version')))).default;
@@ -61,7 +61,7 @@ describe('GET /xenon/api/sdk/version', () => {
61
61
  const apiRouter = (0, express_1.Router)();
62
62
  register(apiRouter);
63
63
  app.use('/xenon/api', apiRouter);
64
- const res = yield (0, supertest_1.default)(app).get('/xenon/api/sdk/version');
64
+ const res = yield (0, loopbackRequest_1.default)(app).get('/xenon/api/sdk/version');
65
65
  (0, chai_1.expect)(res.status).to.equal(200);
66
66
  (0, chai_1.expect)(res.body.pluginVersion).to.be.a('string');
67
67
  (0, chai_1.expect)(res.body.supports).to.include.members(['leases', 'ports', 'heartbeat']);
@@ -16,7 +16,7 @@ require("reflect-metadata");
16
16
  const chai_1 = require("chai");
17
17
  const sinon_1 = __importDefault(require("sinon"));
18
18
  const express_1 = __importDefault(require("express"));
19
- const supertest_1 = __importDefault(require("supertest"));
19
+ const loopbackRequest_1 = __importDefault(require("../helpers/loopbackRequest"));
20
20
  const dashboard_1 = require("../../src/app/routers/dashboard");
21
21
  const roleGuard_1 = require("../../src/middleware/roleGuard");
22
22
  const prisma_1 = require("../../src/prisma");
@@ -209,23 +209,23 @@ describe('GET /healing/selector-health — MEMBER guard (router-level)', () => {
209
209
  });
210
210
  afterEach(() => sinon_1.default.restore());
211
211
  it('401s when unauthenticated', () => __awaiter(void 0, void 0, void 0, function* () {
212
- const r = yield (0, supertest_1.default)(makeApp()).get('/xenon/api/healing/selector-health');
212
+ const r = yield (0, loopbackRequest_1.default)(makeApp()).get('/xenon/api/healing/selector-health');
213
213
  (0, chai_1.expect)(r.status).to.equal(401);
214
214
  }));
215
215
  it('403s for a role below MEMBER', () => __awaiter(void 0, void 0, void 0, function* () {
216
- const r = yield (0, supertest_1.default)(makeApp())
216
+ const r = yield (0, loopbackRequest_1.default)(makeApp())
217
217
  .get('/xenon/api/healing/selector-health')
218
218
  .set('x-test-role', 'BOGUS');
219
219
  (0, chai_1.expect)(r.status).to.equal(403);
220
220
  }));
221
221
  it('200s for MEMBER', () => __awaiter(void 0, void 0, void 0, function* () {
222
- const r = yield (0, supertest_1.default)(makeApp())
222
+ const r = yield (0, loopbackRequest_1.default)(makeApp())
223
223
  .get('/xenon/api/healing/selector-health')
224
224
  .set('x-test-role', 'MEMBER');
225
225
  (0, chai_1.expect)(r.status).to.equal(200);
226
226
  }));
227
227
  it('200s for ADMIN', () => __awaiter(void 0, void 0, void 0, function* () {
228
- const r = yield (0, supertest_1.default)(makeApp())
228
+ const r = yield (0, loopbackRequest_1.default)(makeApp())
229
229
  .get('/xenon/api/healing/selector-health')
230
230
  .set('x-test-role', 'ADMIN');
231
231
  (0, chai_1.expect)(r.status).to.equal(200);
@@ -105,7 +105,7 @@ describe('Selector state full flow (integration)', function () {
105
105
  stdio: 'pipe',
106
106
  });
107
107
  prisma = new client_1.PrismaClient({ datasources: { db: { url } } });
108
- socketStub = { emitToDashboard: sinon_1.default.stub() };
108
+ socketStub = { emitToDashboardForSelector: sinon_1.default.stub().resolves() };
109
109
  stateService = new SelectorStateService_1.SelectorStateService(prisma, socketStub);
110
110
  verifier = new SelectorVerificationJob_1.SelectorVerificationJob(prisma, socketStub);
111
111
  }));
@@ -116,7 +116,7 @@ describe('Selector state full flow (integration)', function () {
116
116
  fs.unlinkSync(dbPath);
117
117
  }));
118
118
  beforeEach(() => __awaiter(this, void 0, void 0, function* () {
119
- socketStub.emitToDashboard.resetHistory();
119
+ socketStub.emitToDashboardForSelector.resetHistory();
120
120
  // SessionLog → Session → Build — delete children before parents.
121
121
  yield prisma.sessionLog.deleteMany({});
122
122
  yield prisma.session.deleteMany({});
@@ -156,7 +156,7 @@ describe('Selector state full flow (integration)', function () {
156
156
  });
157
157
  (0, chai_1.expect)(fixed.status).to.equal('pending');
158
158
  (0, chai_1.expect)(fixed.fixed_at).to.be.instanceOf(Date);
159
- (0, chai_1.expect)(socketStub.emitToDashboard.lastCall.args[0]).to.equal(SocketEvents_1.SocketEvents.SELECTOR_FIXED);
159
+ (0, chai_1.expect)(socketStub.emitToDashboardForSelector.lastCall.args[0]).to.equal(SocketEvents_1.SocketEvents.SELECTOR_FIXED);
160
160
  // Three distinct CI builds, each with one un-healed findElement on the tuple.
161
161
  for (const buildId of ['b-1', 'b-2', 'b-3']) {
162
162
  const sid = `s-${buildId}`;
@@ -190,7 +190,7 @@ describe('Selector state full flow (integration)', function () {
190
190
  (0, chai_1.expect)(final.status).to.equal('resolved');
191
191
  (0, chai_1.expect)(final.clean_builds_count).to.equal(3);
192
192
  (0, chai_1.expect)(final.resolved_at).to.be.instanceOf(Date);
193
- const resolvedEmits = socketStub.emitToDashboard
193
+ const resolvedEmits = socketStub.emitToDashboardForSelector
194
194
  .getCalls()
195
195
  .filter((c) => c.args[0] === SocketEvents_1.SocketEvents.SELECTOR_RESOLVED);
196
196
  (0, chai_1.expect)(resolvedEmits.length).to.equal(1);
@@ -226,6 +226,6 @@ describe('Selector state full flow (integration)', function () {
226
226
  (0, chai_1.expect)(after.regression_count).to.equal(1);
227
227
  (0, chai_1.expect)(after.resolved_at).to.be.null;
228
228
  (0, chai_1.expect)(after.fixed_at).to.be.null;
229
- (0, chai_1.expect)(socketStub.emitToDashboard.lastCall.args[0]).to.equal(SocketEvents_1.SocketEvents.SELECTOR_REGRESSED);
229
+ (0, chai_1.expect)(socketStub.emitToDashboardForSelector.lastCall.args[0]).to.equal(SocketEvents_1.SocketEvents.SELECTOR_REGRESSED);
230
230
  }));
231
231
  });
@@ -8,28 +8,89 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
8
8
  step((generator = generator.apply(thisArg, _arguments || [])).next());
9
9
  });
10
10
  };
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
11
14
  Object.defineProperty(exports, "__esModule", { value: true });
12
15
  require("reflect-metadata");
13
16
  const chai_1 = require("chai");
17
+ const child_process_1 = require("child_process");
18
+ const fs_1 = __importDefault(require("fs"));
19
+ const os_1 = __importDefault(require("os"));
20
+ const path_1 = __importDefault(require("path"));
14
21
  const typedi_1 = require("typedi");
22
+ const client_1 = require("../../src/generated/client");
15
23
  const UserService_1 = require("../../src/services/UserService");
16
24
  const TeamService_1 = require("../../src/services/TeamService");
17
25
  const prisma_1 = require("../../src/prisma");
26
+ const scratch_database_1 = require("../helpers/scratch-database");
27
+ const ROOT = path_1.default.resolve(__dirname, '../..');
28
+ const MIGRATIONS = path_1.default.join(ROOT, 'prisma', 'migrations');
29
+ /** The migration that backfills TeamMember from ApiKey.teamId. */
30
+ const BACKFILL = '20260430001747_phase_3_team_members';
31
+ /**
32
+ * `prisma migrate deploy` into `url` of the repository's migrations that
33
+ * `include` names, from a copy of the schema in `dir`. Run again with more
34
+ * of them, it applies only the ones not yet applied.
35
+ */
36
+ function deploy(dir, url, include) {
37
+ const target = path_1.default.join(dir, 'migrations');
38
+ fs_1.default.mkdirSync(target, { recursive: true });
39
+ fs_1.default.copyFileSync(path_1.default.join(MIGRATIONS, 'migration_lock.toml'), path_1.default.join(target, 'migration_lock.toml'));
40
+ for (const name of fs_1.default.readdirSync(MIGRATIONS)) {
41
+ const sql = path_1.default.join(MIGRATIONS, name, 'migration.sql');
42
+ if (!include(name) || !fs_1.default.existsSync(sql))
43
+ continue;
44
+ fs_1.default.mkdirSync(path_1.default.join(target, name), { recursive: true });
45
+ fs_1.default.copyFileSync(sql, path_1.default.join(target, name, 'migration.sql'));
46
+ }
47
+ fs_1.default.copyFileSync(path_1.default.join(ROOT, 'prisma', 'schema.prisma'), path_1.default.join(dir, 'schema.prisma'));
48
+ (0, child_process_1.execSync)(`npx prisma migrate deploy --schema "${path_1.default.join(dir, 'schema.prisma')}"`, {
49
+ cwd: ROOT,
50
+ env: Object.assign(Object.assign({}, process.env), { DATABASE_URL: url }),
51
+ stdio: 'pipe',
52
+ });
53
+ }
18
54
  describe('team-membership migration backfill (integration)', function () {
19
55
  this.timeout(60000);
20
- it('no TeamMember row points at Legacy Admin', () => __awaiter(this, void 0, void 0, function* () {
21
- const legacy = yield prisma_1.prisma.user.findFirst({
22
- where: { email: 'legacy-admin@xenon.local' },
23
- });
24
- if (!legacy) {
25
- // Legacy Admin only exists if Phase 1's adoption migration actually ran
26
- // against a non-empty ApiKey table. Skip if absent.
27
- // eslint-disable-next-line no-console
28
- console.log('skipped: no Legacy Admin in this DB');
29
- return;
56
+ (0, scratch_database_1.useScratchDatabase)({ wholeSuite: true });
57
+ // Through 2.14 this read the developer's own database, and returned early
58
+ // unless an old Legacy Admin happened to be in it. It runs the migration
59
+ // itself now, on a database migrated up to it and seeded as such a lab was.
60
+ it('backfills TeamMember from team keys, and gives Legacy Admin no membership', () => __awaiter(this, void 0, void 0, function* () {
61
+ const dir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'xenon-team-backfill-'));
62
+ const url = `file:${path_1.default.join(dir, 'xenon.db')}`;
63
+ try {
64
+ deploy(dir, url, (name) => name < BACKFILL);
65
+ const before = new client_1.PrismaClient({ datasources: { db: { url } } });
66
+ try {
67
+ yield before.$executeRawUnsafe('INSERT INTO "Team" ("id", "name") VALUES (?, ?)', 'team-1', 'Team 1');
68
+ for (const [id, email] of [
69
+ ['legacy', 'legacy-admin@xenon.local'],
70
+ ['alice', 'alice@xenon.local'],
71
+ ]) {
72
+ yield before.$executeRawUnsafe(`INSERT INTO "User" ("id", "email", "name", "passwordHash", "accessKey", "updatedAt")
73
+ VALUES (?, ?, ?, 'x', ?, CURRENT_TIMESTAMP)`, id, email, id, `ak-${id}`);
74
+ yield before.$executeRawUnsafe(`INSERT INTO "ApiKey" ("id", "name", "keyHash", "scopes", "teamId", "userId")
75
+ VALUES (?, ?, ?, 'devices', 'team-1', ?)`, `key-${id}`, id, `hash-${id}`, id);
76
+ }
77
+ }
78
+ finally {
79
+ yield before.$disconnect();
80
+ }
81
+ deploy(dir, url, () => true);
82
+ const after = new client_1.PrismaClient({ datasources: { db: { url } } });
83
+ try {
84
+ const members = yield after.teamMember.findMany({ select: { teamId: true, userId: true } });
85
+ (0, chai_1.expect)(members).to.deep.equal([{ teamId: 'team-1', userId: 'alice' }]);
86
+ }
87
+ finally {
88
+ yield after.$disconnect();
89
+ }
90
+ }
91
+ finally {
92
+ fs_1.default.rmSync(dir, { recursive: true, force: true });
30
93
  }
31
- const polluted = yield prisma_1.prisma.teamMember.count({ where: { userId: legacy.id } });
32
- (0, chai_1.expect)(polluted).to.equal(0);
33
94
  }));
34
95
  it('addMember / removeMember round-trip an arbitrary user', () => __awaiter(this, void 0, void 0, function* () {
35
96
  const u = yield typedi_1.Container.get(UserService_1.UserService).createUser({