@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
@@ -0,0 +1,158 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
36
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
37
+ return new (P || (P = Promise))(function (resolve, reject) {
38
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
39
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
40
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
41
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
42
+ });
43
+ };
44
+ var __importDefault = (this && this.__importDefault) || function (mod) {
45
+ return (mod && mod.__esModule) ? mod : { "default": mod };
46
+ };
47
+ Object.defineProperty(exports, "__esModule", { value: true });
48
+ require("reflect-metadata");
49
+ const chai_1 = require("chai");
50
+ const sinon_1 = __importDefault(require("sinon"));
51
+ const typedi_1 = require("typedi");
52
+ const DeviceService = __importStar(require("../../src/data-service/device-service"));
53
+ const IOSStreamService_1 = __importDefault(require("../../src/device-managers/ios/IOSStreamService"));
54
+ const event_manager_1 = require("../../src/dashboard/event-manager");
55
+ const SessionLifecycleService_1 = require("../../src/services/SessionLifecycleService");
56
+ const SessionMetricsService_1 = require("../../src/services/metrics/SessionMetricsService");
57
+ const PhoneNetworkRestore_1 = require("../../src/services/network/PhoneNetworkRestore");
58
+ const VideoPipelineService_1 = require("../../src/services/VideoPipelineService");
59
+ const LocalSession_1 = require("../../src/sessions/LocalSession");
60
+ const SessionManager_1 = require("../../src/sessions/SessionManager");
61
+ const container_registration_1 = require("../helpers/container-registration");
62
+ const scratch_database_1 = require("../helpers/scratch-database");
63
+ /**
64
+ * What Xenon does around a session on an iOS simulator, whose WebDriverAgent
65
+ * the XCUITest driver runs on this Mac.
66
+ *
67
+ * A session that passed every command ended failed: its end stopped a
68
+ * performance recording that Xenon starts only on an iPhone, the driver
69
+ * refuses that on a simulator without relaxed security, and the refused stop
70
+ * counted as a failed command of the session. Its video was never written:
71
+ * it was read from the iPhone stream's USB forward (iproxy), which on a
72
+ * simulator forwards nothing, while the simulator's own WebDriverAgent serves
73
+ * the picture on this Mac at the session's mjpegServerPort.
74
+ */
75
+ const SIMULATOR = '35429994-B029-4AF4-9634-880B89DAC782';
76
+ const IPHONE = '00008150-000E78612168C01C';
77
+ const device = (udid, realDevice) => ({
78
+ udid,
79
+ name: 'iPhone 16 Pro',
80
+ platform: 'ios',
81
+ sdk: '18.4',
82
+ host: 'http://127.0.0.1:4723',
83
+ realDevice,
84
+ deviceType: realDevice ? 'real' : 'simulator',
85
+ screenWidth: '402',
86
+ screenHeight: '874',
87
+ });
88
+ describe('iOS simulator sessions: around the session', function () {
89
+ this.timeout(30000);
90
+ afterEach(() => sinon_1.default.restore());
91
+ describe('the end of a session', () => {
92
+ (0, scratch_database_1.useScratchDatabase)();
93
+ let restoreRegs;
94
+ beforeEach(() => {
95
+ restoreRegs = (0, container_registration_1.saveRegistrations)(PhoneNetworkRestore_1.PhoneNetworkRestore, SessionMetricsService_1.SessionMetricsService);
96
+ typedi_1.Container.set(PhoneNetworkRestore_1.PhoneNetworkRestore, { restoreSession: sinon_1.default.stub().resolves() });
97
+ typedi_1.Container.set(SessionMetricsService_1.SessionMetricsService, { stop: sinon_1.default.stub().resolves() });
98
+ sinon_1.default.stub(DeviceService, 'releaseSessionDevices').resolves();
99
+ sinon_1.default.stub(event_manager_1.DASHBORD_EVENT_MANAGER, 'onSessionStopped').resolves();
100
+ sinon_1.default.stub(SessionManager_1.SESSION_MANAGER, 'removeSession');
101
+ });
102
+ afterEach(() => restoreRegs());
103
+ /** Deletes a session on `phone`; resolves to whether its end stopped a performance recording. */
104
+ function endSessionOn(phone) {
105
+ return __awaiter(this, void 0, void 0, function* () {
106
+ const stopPerformanceRecording = sinon_1.default.stub().resolves(null);
107
+ const session = {
108
+ isStopping: false,
109
+ getId: () => 's-1',
110
+ getDevice: () => phone,
111
+ stopPerformanceRecording,
112
+ isVideoRecordingInProgress: () => false,
113
+ };
114
+ sinon_1.default.stub(SessionManager_1.SESSION_MANAGER, 'getSession').returns(session);
115
+ yield typedi_1.Container.get(SessionLifecycleService_1.SessionLifecycleService).deleteSession(sinon_1.default.stub().resolves(), 's-1');
116
+ return stopPerformanceRecording.called;
117
+ });
118
+ }
119
+ it('stops no performance recording on a simulator, where none was started', () => __awaiter(this, void 0, void 0, function* () {
120
+ (0, chai_1.expect)(yield endSessionOn(device(SIMULATOR, false))).to.equal(false);
121
+ }));
122
+ it("still stops an iPhone's, which Xenon started", () => __awaiter(this, void 0, void 0, function* () {
123
+ (0, chai_1.expect)(yield endSessionOn(device(IPHONE, true))).to.equal(true);
124
+ }));
125
+ });
126
+ describe('the session video', () => {
127
+ let startRecording;
128
+ let startStream;
129
+ beforeEach(() => {
130
+ startRecording = sinon_1.default.stub(VideoPipelineService_1.VideoPipelineService.prototype, 'startRecording').resolves();
131
+ startStream = sinon_1.default
132
+ .stub(IOSStreamService_1.default.prototype, 'startStream')
133
+ .resolves({ mjpegPort: 9150 });
134
+ });
135
+ const local = (phone) => new LocalSession_1.LocalSession({
136
+ sessionId: 's-video',
137
+ device: phone,
138
+ // The capabilities the driver answered the create with.
139
+ sessionResponse: { udid: phone.udid, wdaLocalPort: 8101, mjpegServerPort: 9101 },
140
+ xenonOption: {},
141
+ driver: { opts: { address: '127.0.0.1', port: 4723, basePath: '/wd/hub' }, sessions: {} },
142
+ });
143
+ it("is read from the simulator's own WebDriverAgent, at the session's MJPEG port", () => __awaiter(this, void 0, void 0, function* () {
144
+ yield local(device(SIMULATOR, false)).startVideoRecording();
145
+ (0, chai_1.expect)(startStream.called, 'started the iPhone stream').to.equal(false);
146
+ (0, chai_1.expect)(startRecording.calledOnce).to.equal(true);
147
+ (0, chai_1.expect)(startRecording.firstCall.args[0]).to.deep.include({
148
+ udid: SIMULATOR,
149
+ mjpegPort: 9101,
150
+ });
151
+ }));
152
+ it("is still read from an iPhone's stream", () => __awaiter(this, void 0, void 0, function* () {
153
+ yield local(device(IPHONE, true)).startVideoRecording();
154
+ (0, chai_1.expect)(startStream.calledOnceWith(IPHONE)).to.equal(true);
155
+ (0, chai_1.expect)(startRecording.firstCall.args[0]).to.deep.include({ udid: IPHONE, mjpegPort: 9150 });
156
+ }));
157
+ });
158
+ });
@@ -26,14 +26,31 @@ describe('canOverrideLease — who may use a lease they did not create', () => {
26
26
  (0, chai_1.expect)((0, leaseOverride_1.canOverrideLease)({ kind: 'api-key', scopes: 'admin', user: null })).to.equal(false);
27
27
  });
28
28
  });
29
- describe('a session token (the user themselves, like a dashboard cookie)', () => {
30
- it('is allowed for an ADMIN or SUPER_ADMIN', () => {
31
- (0, chai_1.expect)((0, leaseOverride_1.canOverrideLease)({ kind: 'session-token', user: user('ADMIN') })).to.equal(true);
32
- (0, chai_1.expect)((0, leaseOverride_1.canOverrideLease)({ kind: 'session-token', user: user('SUPER_ADMIN') })).to.equal(true);
29
+ // A session token carries the scopes of the credential that minted it, and
30
+ // is judged by them like a key. By role alone, an ADMIN's key without the
31
+ // admin scope minted itself a token that could override.
32
+ describe("a session token (judged by its scopes and its user's role now)", () => {
33
+ // resolveActor's rule (a SUPER_ADMIN, or a credential with `admin`), on
34
+ // the user's role now: the scope is fixed at mint, so an ADMIN's token
35
+ // also needs the role to still be ADMIN, which notices a demotion.
36
+ it("is allowed for an ADMIN's or SUPER_ADMIN's token carrying the admin scope", () => {
37
+ for (const role of ['ADMIN', 'SUPER_ADMIN']) {
38
+ (0, chai_1.expect)((0, leaseOverride_1.canOverrideLease)({ kind: 'session-token', scopes: 'admin,sessions', user: user(role) })).to.equal(true);
39
+ }
40
+ });
41
+ // As their key pair overrides, and their Bearer token in per-command auth.
42
+ it("is allowed for a SUPER_ADMIN's token without the admin scope", () => {
43
+ (0, chai_1.expect)((0, leaseOverride_1.canOverrideLease)({ kind: 'session-token', scopes: 'sessions', user: user('SUPER_ADMIN') })).to.equal(true);
44
+ });
45
+ it('is refused for a token with the admin scope whose user is now a member', () => {
46
+ (0, chai_1.expect)((0, leaseOverride_1.canOverrideLease)({ kind: 'session-token', scopes: 'admin,sessions', user: user('MEMBER') })).to.equal(false);
47
+ });
48
+ it("is refused for an ADMIN's token without the admin scope", () => {
49
+ (0, chai_1.expect)((0, leaseOverride_1.canOverrideLease)({ kind: 'session-token', scopes: 'sessions', user: user('ADMIN') })).to.equal(false);
33
50
  });
34
51
  it('is refused for a member, or a user who is not active', () => {
35
- (0, chai_1.expect)((0, leaseOverride_1.canOverrideLease)({ kind: 'session-token', user: user('MEMBER') })).to.equal(false);
36
- (0, chai_1.expect)((0, leaseOverride_1.canOverrideLease)({ kind: 'session-token', user: null })).to.equal(false);
52
+ (0, chai_1.expect)((0, leaseOverride_1.canOverrideLease)({ kind: 'session-token', scopes: 'sessions', user: user('MEMBER') })).to.equal(false);
53
+ (0, chai_1.expect)((0, leaseOverride_1.canOverrideLease)({ kind: 'session-token', scopes: 'admin', user: null })).to.equal(false);
37
54
  });
38
55
  });
39
56
  it('is allowed with auth disabled, where every caller is a SUPER_ADMIN', () => {
@@ -83,8 +83,11 @@ const sessionCaps = (xenonOptions, extra = {}) => ({
83
83
  const key = (accessKey) => ({ accessKey, token: 'tk' });
84
84
  const ownerKey = key('ak_owner');
85
85
  const otherKey = key('ak_other');
86
- // A session token names its user in `sub`; the fake verifier echoes it back.
87
- const sessionToken = (sub) => ({ sessionToken: `jwt:${sub}` });
86
+ // A session token names its user in `sub` and carries the scopes of the
87
+ // credential that minted it; the fake verifier echoes both back.
88
+ const sessionToken = (sub, scopes = 'sessions') => ({
89
+ sessionToken: `jwt:${sub}|${scopes}`,
90
+ });
88
91
  const keys = {
89
92
  ak_owner: { id: 'key_owner', userId: 'usr_owner', scopes: 'sessions', teamId: null },
90
93
  ak_other: { id: 'key_other', userId: 'usr_other', scopes: 'sessions', teamId: null },
@@ -174,7 +177,10 @@ describe('createSession — a lease-bound session proves it holds the lease', ()
174
177
  findById: (id) => __awaiter(void 0, void 0, void 0, function* () { return roles[id] ? { id, role: roles[id], status: 'ACTIVE' } : null; }),
175
178
  });
176
179
  typedi_1.Container.set(JwtKeyService_1.JwtKeyService, {
177
- verify: (t) => __awaiter(void 0, void 0, void 0, function* () { return ({ sub: t.replace(/^jwt:/, ''), teamId: null }); }),
180
+ verify: (t) => __awaiter(void 0, void 0, void 0, function* () {
181
+ const [sub, scopes] = t.replace(/^jwt:/, '').split('|');
182
+ return { sub, teamId: null, scopes };
183
+ }),
178
184
  });
179
185
  teamRows = sinon_1.default.stub(prisma_1.prisma.teamMember, 'findMany').resolves([]);
180
186
  sinon_1.default.stub(device_store_1.DeviceStoreFactory, 'getStore').returns({
@@ -331,8 +337,14 @@ describe('createSession — a lease-bound session proves it holds the lease', ()
331
337
  it("is allowed to a SUPER_ADMIN's key", () => __awaiter(void 0, void 0, void 0, function* () {
332
338
  (0, chai_1.expect)(yield allowed(sessionCaps(key('ak_super')))).to.equal(true);
333
339
  }));
340
+ // A session token minted from the dashboard carries the admin's admin
341
+ // scope; one minted with an ADMIN's narrower key doesn't, and overrides
342
+ // no more than that key does.
334
343
  it('is allowed to an ADMIN presenting a session token, as on the dashboard', () => __awaiter(void 0, void 0, void 0, function* () {
335
- (0, chai_1.expect)(yield allowed(sessionCaps(sessionToken('usr_admin')))).to.equal(true);
344
+ (0, chai_1.expect)(yield allowed(sessionCaps(sessionToken('usr_admin', 'admin,sessions')))).to.equal(true);
345
+ }));
346
+ it("is refused to an ADMIN's session token minted without the admin scope", () => __awaiter(void 0, void 0, void 0, function* () {
347
+ (0, chai_1.expect)(yield refusal(sessionCaps(sessionToken('usr_admin', 'sessions')))).to.equal(REFUSED);
336
348
  }));
337
349
  it('is refused to a member presenting a session token', () => __awaiter(void 0, void 0, void 0, function* () {
338
350
  (0, chai_1.expect)(yield refusal(sessionCaps(sessionToken('usr_other')))).to.equal(REFUSED);
@@ -73,6 +73,10 @@ function harness(over) {
73
73
  const mux = new LogcatMultiplexer_1.LogcatMultiplexer();
74
74
  const state = { startCalls: 0, authorizeCalls: 0, authorizeActors: [] };
75
75
  const server = http_1.default.createServer();
76
+ const connectionsClosed = [];
77
+ server.on('connection', (socket) => {
78
+ connectionsClosed.push(new Promise((r) => socket.once('close', () => r())));
79
+ });
76
80
  const userAuthorize = (_a = over.authorize) !== null && _a !== void 0 ? _a : (() => __awaiter(this, void 0, void 0, function* () { return true; }));
77
81
  const deps = {
78
82
  redeem: (_b = over.redeem) !== null && _b !== void 0 ? _b : (() => __awaiter(this, void 0, void 0, function* () { return ({ actorId: 'usr_alice' }); })),
@@ -109,6 +113,13 @@ function harness(over) {
109
113
  get authorizeActors() {
110
114
  return state.authorizeActors;
111
115
  },
116
+ serverSawClose: () => __awaiter(this, void 0, void 0, function* () {
117
+ yield Promise.all(connectionsClosed);
118
+ // ws emits the server WebSocket's 'close' from the TCP socket's 'close'
119
+ // listener, or from a process.nextTick after it (its receiver's
120
+ // 'finish'). Either has run by the event loop's next check phase.
121
+ yield new Promise((r) => setImmediate(r));
122
+ }),
112
123
  close: () => new Promise((r) => {
113
124
  var _a;
114
125
  // http.Server#close only fires its callback once every connection has
@@ -159,6 +170,50 @@ function connectCollecting(h_1) {
159
170
  return { code, got };
160
171
  });
161
172
  }
173
+ /**
174
+ * Holds one handshake step until the test releases it, and says when the
175
+ * server has reached it, so a test acts while that step is in flight instead
176
+ * of guessing how long the steps before it take. Under load a fixed 50 ms
177
+ * wasn't enough: the client was still CONNECTING, and terminate() there
178
+ * raised an uncaught error.
179
+ */
180
+ function heldStep() {
181
+ let enter = () => undefined;
182
+ let release = () => undefined;
183
+ const entered = new Promise((r) => (enter = r));
184
+ const released = new Promise((r) => (release = r));
185
+ return {
186
+ entered,
187
+ release,
188
+ /** For the stubbed step: marks it entered, then waits for the release. */
189
+ hold: () => __awaiter(this, void 0, void 0, function* () {
190
+ enter();
191
+ yield released;
192
+ }),
193
+ };
194
+ }
195
+ /**
196
+ * Connects, drops the connection once it is open and `step` is in flight,
197
+ * and resolves when the server has seen it go.
198
+ */
199
+ function dropDuring(h, step) {
200
+ return __awaiter(this, void 0, void 0, function* () {
201
+ const ws = new ws_1.WebSocket(url(h.port));
202
+ // terminate() waits for 'open', so it never raises the 'error' it raises on
203
+ // a CONNECTING socket. Any other unhandled 'error' would be an uncaught
204
+ // exception, failing whichever test is running when it lands.
205
+ ws.on('error', () => undefined);
206
+ yield Promise.all([new Promise((r) => ws.once('open', r)), step.entered]);
207
+ ws.terminate();
208
+ yield h.serverSawClose();
209
+ });
210
+ }
211
+ /**
212
+ * Lets the handshake run on after a held step is released. Everything after
213
+ * a step is promise continuations (the harness's steps do no I/O), so it has
214
+ * all run by the event loop's next check phase.
215
+ */
216
+ const handshakeSettled = () => new Promise((r) => setImmediate(r));
162
217
  describe('attachLogcatWs handshake', () => {
163
218
  it('accepts a valid ticket for a device the caller may use', () => __awaiter(void 0, void 0, void 0, function* () {
164
219
  const h = yield harness({});
@@ -265,26 +320,21 @@ describe('attachLogcatWs handshake', () => {
265
320
  // The trap documented in h264StreamWs: without cleanup registered before the
266
321
  // awaits, clientCount stays inflated and the idle watchdog never fires.
267
322
  it('leaves no client registered when the socket drops mid-handshake during redeem', () => __awaiter(void 0, void 0, void 0, function* () {
268
- let release = () => undefined;
269
- const gate = new Promise((r) => (release = r));
323
+ const step = heldStep();
270
324
  const h = yield harness({
271
325
  redeem: () => __awaiter(void 0, void 0, void 0, function* () {
272
- yield gate;
326
+ yield step.hold();
273
327
  return { actorId: 'usr_alice' };
274
328
  }),
275
329
  });
276
- const ws = new ws_1.WebSocket(url(h.port));
277
- yield new Promise((r) => setTimeout(r, 50));
278
- ws.terminate(); // disconnect while redeem is still pending
279
- // Let the server actually observe the close (a real socket teardown is a
280
- // macrotask; resolving `gate` below is a same-tick microtask chain that
281
- // would otherwise race ahead of it) BEFORE unblocking redeem. Otherwise
282
- // this proves nothing about the post-redeem guard: the client could get
283
- // registered first and only be torn down afterward by the 'close'
284
- // listener firing late — same end state, different (unguarded) mechanism.
285
- yield new Promise((r) => setTimeout(r, 100));
286
- release();
287
- yield new Promise((r) => setTimeout(r, 150));
330
+ // Disconnect while redeem is still pending, and let the server see the
331
+ // close BEFORE redeem resolves. Otherwise this proves nothing about the
332
+ // post-redeem guard: the client could get registered first and only be
333
+ // torn down afterward by the 'close' listener firing late — same end
334
+ // state, different (unguarded) mechanism.
335
+ yield dropDuring(h, step);
336
+ step.release();
337
+ yield handshakeSettled();
288
338
  (0, chai_1.expect)(h.mux.clientCount).to.equal(0);
289
339
  // The final clientCount alone can't distinguish "the post-redeem guard
290
340
  // short-circuited" from "a later guard cleaned up after the fact" — both
@@ -301,20 +351,16 @@ describe('attachLogcatWs handshake', () => {
301
351
  // session owner) when the caller disconnects. Same reasoning as the two
302
352
  // neighbours — the observable is the adb spawn that must not happen.
303
353
  it('leaves no client registered when the socket drops mid-handshake during authorize', () => __awaiter(void 0, void 0, void 0, function* () {
304
- let release = () => undefined;
305
- const gate = new Promise((r) => (release = r));
354
+ const step = heldStep();
306
355
  const h = yield harness({
307
356
  authorize: () => __awaiter(void 0, void 0, void 0, function* () {
308
- yield gate;
357
+ yield step.hold();
309
358
  return true;
310
359
  }),
311
360
  });
312
- const ws = new ws_1.WebSocket(url(h.port));
313
- yield new Promise((r) => setTimeout(r, 50));
314
- ws.terminate(); // disconnect while authorize is still pending
315
- yield new Promise((r) => setTimeout(r, 100));
316
- release();
317
- yield new Promise((r) => setTimeout(r, 150));
361
+ yield dropDuring(h, step); // disconnect while authorize is still pending
362
+ step.release();
363
+ yield handshakeSettled();
318
364
  (0, chai_1.expect)(h.authorizeCalls, 'authorize was already in flight').to.equal(1);
319
365
  (0, chai_1.expect)(h.startCalls, 'must not start a logcat process once the caller is gone').to.equal(0);
320
366
  (0, chai_1.expect)(h.mux.clientCount).to.equal(0);
@@ -324,25 +370,20 @@ describe('attachLogcatWs handshake', () => {
324
370
  // but startStream (which can shell out to adb) is still in flight when the
325
371
  // caller disconnects.
326
372
  it('leaves no client registered when the socket drops mid-handshake during startStream', () => __awaiter(void 0, void 0, void 0, function* () {
327
- let release = () => undefined;
328
- const gate = new Promise((r) => (release = r));
373
+ const step = heldStep();
329
374
  const gatedMux = new LogcatMultiplexer_1.LogcatMultiplexer();
330
375
  const h = yield harness({
331
376
  startStream: () => __awaiter(void 0, void 0, void 0, function* () {
332
- yield gate;
377
+ yield step.hold();
333
378
  return { mux: gatedMux };
334
379
  }),
335
380
  });
336
- const ws = new ws_1.WebSocket(url(h.port));
337
- yield new Promise((r) => setTimeout(r, 50));
338
- ws.terminate(); // disconnect while startStream is still pending
339
- // See the matching comment in the redeem-timing test above: give the
340
- // server's close listener time to actually run before startStream
341
- // resolves, or this races the wrong way and proves nothing about the
342
- // post-startStream guard.
343
- yield new Promise((r) => setTimeout(r, 100));
344
- release();
345
- yield new Promise((r) => setTimeout(r, 150));
381
+ // As in the redeem test above: the server sees the close before
382
+ // startStream resolves, or this proves nothing about the post-startStream
383
+ // guard.
384
+ yield dropDuring(h, step);
385
+ step.release();
386
+ yield handshakeSettled();
346
387
  (0, chai_1.expect)(gatedMux.clientCount).to.equal(0);
347
388
  yield h.close();
348
389
  }));
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const chai_1 = require("chai");
7
+ const sinon_1 = __importDefault(require("sinon"));
8
+ const logger_1 = require("../../src/logger");
9
+ /**
10
+ * In JSON log mode every line was written through Appium's `info`, whatever
11
+ * its level: a debug line printed at `--log-level info`, and an error went to
12
+ * stdout, since Appium's console sends only its `error` level to stderr. Each
13
+ * line now goes out at its own level, as in text mode.
14
+ */
15
+ describe('XenonLogger in JSON mode writes each line at its own level', () => {
16
+ let wasJson;
17
+ let base;
18
+ let logger;
19
+ beforeEach(() => {
20
+ wasJson = logger_1.XenonLogger.isJsonLogging;
21
+ logger_1.XenonLogger.configure({ enableJsonLogging: true });
22
+ base = {
23
+ debug: sinon_1.default.stub(),
24
+ info: sinon_1.default.stub(),
25
+ warn: sinon_1.default.stub(),
26
+ error: sinon_1.default.stub(),
27
+ };
28
+ logger = new logger_1.XenonLogger('probe');
29
+ logger.baseLogger = base;
30
+ });
31
+ afterEach(() => {
32
+ logger_1.XenonLogger.configure({ enableJsonLogging: wasJson });
33
+ });
34
+ for (const level of ['debug', 'info', 'warn', 'error']) {
35
+ it(level, () => {
36
+ logger[level](`a ${level} line`);
37
+ const written = Object.keys(base).filter((l) => base[l].called);
38
+ (0, chai_1.expect)(written).to.deep.equal([level]);
39
+ const entry = JSON.parse(base[level].firstCall.args[0]);
40
+ (0, chai_1.expect)(entry.level).to.equal(level);
41
+ (0, chai_1.expect)(entry.message).to.equal(`a ${level} line`);
42
+ });
43
+ }
44
+ });
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const chai_1 = require("chai");
7
+ const sinon_1 = __importDefault(require("sinon"));
8
+ const mcpTokenTtl_1 = require("../../src/services/token/mcpTokenTtl");
9
+ /**
10
+ * XENON_MCP_TOKEN_TTL_SEC=abc made every xenon-mcp token's lifetime NaN.
11
+ * A value that isn't a whole number of seconds above 0 now warns and keeps
12
+ * the default.
13
+ */
14
+ describe('XENON_MCP_TOKEN_TTL_SEC', () => {
15
+ it('is the default (a day) when unset or empty, without a warning', () => {
16
+ const warn = sinon_1.default.stub();
17
+ (0, chai_1.expect)((0, mcpTokenTtl_1.mcpTokenTtlSec)(undefined, warn)).to.equal(86400);
18
+ (0, chai_1.expect)((0, mcpTokenTtl_1.mcpTokenTtlSec)('', warn)).to.equal(86400);
19
+ (0, chai_1.expect)((0, mcpTokenTtl_1.mcpTokenTtlSec)(' ', warn)).to.equal(86400);
20
+ (0, chai_1.expect)(mcpTokenTtl_1.DEFAULT_MCP_TOKEN_TTL_SEC).to.equal(86400);
21
+ (0, chai_1.expect)(warn.called).to.equal(false);
22
+ });
23
+ it('is the number of seconds it gives', () => {
24
+ const warn = sinon_1.default.stub();
25
+ (0, chai_1.expect)((0, mcpTokenTtl_1.mcpTokenTtlSec)('3600', warn)).to.equal(3600);
26
+ (0, chai_1.expect)((0, mcpTokenTtl_1.mcpTokenTtlSec)(' 120 ', warn)).to.equal(120);
27
+ (0, chai_1.expect)(warn.called).to.equal(false);
28
+ });
29
+ for (const bad of ['abc', '0', '-60', '1.5', '1e400', 'Infinity', '12s']) {
30
+ it(`warns and keeps the default for "${bad}"`, () => {
31
+ const warn = sinon_1.default.stub();
32
+ (0, chai_1.expect)((0, mcpTokenTtl_1.mcpTokenTtlSec)(bad, warn)).to.equal(86400);
33
+ (0, chai_1.expect)(warn.calledOnce).to.equal(true);
34
+ (0, chai_1.expect)(warn.firstCall.args[0]).to.include('XENON_MCP_TOKEN_TTL_SEC');
35
+ (0, chai_1.expect)(warn.firstCall.args[0]).to.include(bad);
36
+ });
37
+ }
38
+ });
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const chai_1 = require("chai");
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const js_yaml_1 = __importDefault(require("js-yaml"));
10
+ /**
11
+ * The example Loki config set `limits_config.retention_period: 24h`, but Loki
12
+ * deletes nothing unless its compactor runs retention, so the logs it kept
13
+ * grew for ever. Loki 3 also refuses to start with retention enabled and no
14
+ * store for delete requests.
15
+ */
16
+ describe('examples/observability/loki.yml', () => {
17
+ const config = js_yaml_1.default.load(fs_1.default.readFileSync(path_1.default.join(__dirname, '../../examples/observability/loki.yml'), 'utf8'));
18
+ it('keeps logs for the retention period it sets', () => {
19
+ var _a, _b, _c;
20
+ (0, chai_1.expect)(config.limits_config.retention_period).to.equal('24h');
21
+ (0, chai_1.expect)((_a = config.compactor) === null || _a === void 0 ? void 0 : _a.retention_enabled, 'compactor.retention_enabled').to.equal(true);
22
+ (0, chai_1.expect)((_b = config.compactor) === null || _b === void 0 ? void 0 : _b.delete_request_store, 'compactor.delete_request_store').to.equal(config.schema_config.configs[0].object_store);
23
+ (0, chai_1.expect)((_c = config.compactor) === null || _c === void 0 ? void 0 : _c.working_directory, 'compactor.working_directory').to.be.a('string');
24
+ });
25
+ });
@@ -0,0 +1,113 @@
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 sharp_1 = __importDefault(require("sharp"));
18
+ const typedi_1 = require("typedi");
19
+ const OcrHealingProvider_1 = require("../../src/services/healing/OcrHealingProvider");
20
+ const OmniVisionService_1 = require("../../src/services/omni-vision/OmniVisionService");
21
+ const container_registration_1 = require("../helpers/container-registration");
22
+ /**
23
+ * The OCR healing tier, on a real screenshot read by a real Tesseract worker
24
+ * (the language data the plugin ships), with nothing stubbed between them.
25
+ *
26
+ * Through 2.14 the tier read `result.data.words` from tesseract.js, which
27
+ * since tesseract.js 6 returns text only unless asked: `words` was undefined,
28
+ * and the tier healed nothing, ever. Its tests stubbed the OCR with words, so
29
+ * none of them could see it. ocr-healing-tier.spec.ts covers the matching
30
+ * rules on stubbed words; this checks the whole tier reads a screen.
31
+ */
32
+ describe('The OCR healing tier, reading a real screenshot', () => {
33
+ let restoreContainer;
34
+ let screenshot;
35
+ // A phone-like screen: four lines, 200 px apart, each at x 60. "Log out"
36
+ // and "Check in" come first, so a tier that took a lone "Log" for "Login",
37
+ // or a lone "in" for "Sign in", answers with the wrong line.
38
+ const WIDTH = 720;
39
+ const LINES = { 'Log out': 150, 'Check in': 350, 'Sign in': 550, Login: 750 };
40
+ const FONT_SIZE = 56;
41
+ /** Where a line's text is: from its cap height above the baseline to just below it. */
42
+ const band = (line) => ({
43
+ top: LINES[line] - FONT_SIZE,
44
+ bottom: LINES[line] + FONT_SIZE / 4,
45
+ });
46
+ before(() => __awaiter(void 0, void 0, void 0, function* () {
47
+ const text = Object.entries(LINES)
48
+ .map(([words, y]) => `<text x="60" y="${y}" font-family="Helvetica, Arial, sans-serif" font-size="${FONT_SIZE}" fill="#111">${words}</text>`)
49
+ .join('');
50
+ const png = yield (0, sharp_1.default)({
51
+ create: { width: WIDTH, height: 900, channels: 3, background: '#ffffff' },
52
+ })
53
+ .composite([
54
+ {
55
+ input: Buffer.from(`<svg xmlns="http://www.w3.org/2000/svg" width="${WIDTH}" height="900">${text}</svg>`),
56
+ },
57
+ ])
58
+ .png()
59
+ .toBuffer();
60
+ screenshot = png.toString('base64');
61
+ // An Omni-Vision of its own, whose worker this suite starts and ends.
62
+ restoreContainer = (0, container_registration_1.saveRegistrations)(OmniVisionService_1.OmniVisionService);
63
+ typedi_1.Container.set(OmniVisionService_1.OmniVisionService, new OmniVisionService_1.OmniVisionService());
64
+ }));
65
+ after(() => __awaiter(void 0, void 0, void 0, function* () {
66
+ const worker = typedi_1.Container.get(OmniVisionService_1.OmniVisionService).sharedWorker;
67
+ yield (worker === null || worker === void 0 ? void 0 : worker.terminate());
68
+ restoreContainer();
69
+ }));
70
+ /** An Android driver: no element answers an iOS predicate, so the tier says where the text is. */
71
+ const android = { findElement: () => __awaiter(void 0, void 0, void 0, function* () { return Promise.reject(new Error('unsupported locator')); }) };
72
+ const heal = (selector) => new OcrHealingProvider_1.OcrHealingProvider().heal({
73
+ sessionId: 'real-ocr-session',
74
+ driver: android,
75
+ strategy: 'xpath',
76
+ selector,
77
+ screenshotBase64: screenshot,
78
+ });
79
+ /** The healed element's box lies on `line`, starting where its text starts. */
80
+ const expectOnLine = (rect, line) => {
81
+ const { top, bottom } = band(line);
82
+ (0, chai_1.expect)(rect, 'rect').to.be.an('object');
83
+ (0, chai_1.expect)(rect.y, `top of the box, on "${line}"`).to.be.within(top, bottom);
84
+ (0, chai_1.expect)(rect.y + rect.height, `bottom of the box, on "${line}"`).to.be.within(top, bottom);
85
+ (0, chai_1.expect)(rect.x, 'left of the box').to.be.within(40, 80);
86
+ };
87
+ it('finds a two-word text on its line, not a lone word of it read earlier', () => __awaiter(void 0, void 0, void 0, function* () {
88
+ const healed = yield heal("//*[@text='Sign in']");
89
+ (0, chai_1.expect)(healed, 'healed').to.not.equal(null);
90
+ (0, chai_1.expect)(healed.id).to.match(/^healed_ocr_/);
91
+ (0, chai_1.expect)(healed.recommendedSelector).to.equal('ocr:text="Sign in"');
92
+ (0, chai_1.expect)(healed.text).to.equal('Sign in');
93
+ expectOnLine(healed.rect, 'Sign in');
94
+ // Both words: wider than "Sign" alone at this size.
95
+ (0, chai_1.expect)(healed.rect.width).to.be.above(150);
96
+ (0, chai_1.expect)(healed.confidence).to.be.within(0.6, 1);
97
+ }));
98
+ it('does not take a word that is only part of the text', () => __awaiter(void 0, void 0, void 0, function* () {
99
+ const healed = yield heal("//*[@text='Login']");
100
+ (0, chai_1.expect)(healed, 'healed').to.not.equal(null);
101
+ (0, chai_1.expect)(healed.text).to.equal('Login');
102
+ expectOnLine(healed.rect, 'Login');
103
+ }));
104
+ it('reads the text from the attribute the selector names, whatever its case', () => __awaiter(void 0, void 0, void 0, function* () {
105
+ const healed = yield heal("//android.widget.Button[@content-desc='check IN']");
106
+ (0, chai_1.expect)(healed, 'healed').to.not.equal(null);
107
+ (0, chai_1.expect)(healed.text).to.equal('Check in');
108
+ expectOnLine(healed.rect, 'Check in');
109
+ }));
110
+ it('heals nothing when the text is not on the screen', () => __awaiter(void 0, void 0, void 0, function* () {
111
+ (0, chai_1.expect)(yield heal("//*[@text='Sign up']")).to.equal(null);
112
+ }));
113
+ });