@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
@@ -51,54 +51,85 @@ describe('filterProvidersByTier (§2.7 healing-tier gate)', () => {
51
51
  const result = (0, HealingOrchestrator_1.filterProvidersByTier)(providers, []);
52
52
  (0, chai_1.expect)(result).to.deep.equal([]);
53
53
  });
54
- // MINOR fix (Phase 2a review): the capability accessor must fail OPEN on a
55
- // malformed healingTiers cap. Pre-fix, an all-non-numeric array (e.g.
56
- // ["1","2"]) filtered to [] and silently DISABLED healing; the contract is
57
- // malformed → run all tiers. The cap is meant to RESTRICT healing, never to
58
- // disable it — so an explicitly-empty [] also coerces to run-all (opting out
59
- // of healing entirely is not a supported use case).
60
- describe('coerceHealingTiersCap (malformed cap → fail open)', () => {
61
- it('passes a numeric array through unchanged', () => {
54
+ // The value is a privacy control: a session that leaves out Visual AI (4)
55
+ // and the LLM (5) keeps its screen away from the AI provider. So a value
56
+ // that can't be read fails closed for those two, and open for the rest:
57
+ // the tiers that stay on the server (1, 2, 3) still run. Through 2.14 an
58
+ // empty list, or a value that wasn't a list or had no numbers in it, ran
59
+ // every tier, the AI ones included; a list that held some numbers ran those.
60
+ describe('coerceHealingTiersCap', () => {
61
+ it('passes a list of tier numbers through unchanged', () => {
62
62
  (0, chai_1.expect)((0, HealingOrchestrator_1.coerceHealingTiersCap)([1, 3])).to.deep.equal([1, 3]);
63
+ (0, chai_1.expect)((0, HealingOrchestrator_1.coerceHealingTiersCap)([5, 4, 1])).to.deep.equal([5, 4, 1]);
63
64
  });
64
- it('keeps only the numeric entries of a mixed array', () => {
65
- (0, chai_1.expect)((0, HealingOrchestrator_1.coerceHealingTiersCap)([1, '2'])).to.deep.equal([1]);
66
- });
67
- it('non-array (string/undefined/null) → undefined (run all)', () => {
68
- (0, chai_1.expect)((0, HealingOrchestrator_1.coerceHealingTiersCap)('1,2')).to.equal(undefined);
65
+ it('runs every tier when the session sets nothing', () => {
69
66
  (0, chai_1.expect)((0, HealingOrchestrator_1.coerceHealingTiersCap)(undefined)).to.equal(undefined);
70
67
  (0, chai_1.expect)((0, HealingOrchestrator_1.coerceHealingTiersCap)(null)).to.equal(undefined);
71
68
  });
72
- it('all-non-numeric array ["1","2"] → undefined → ALL providers run (fail open)', () => {
73
- const coerced = (0, HealingOrchestrator_1.coerceHealingTiersCap)(['1', '2']);
74
- (0, chai_1.expect)(coerced).to.equal(undefined);
75
- (0, chai_1.expect)((0, HealingOrchestrator_1.filterProvidersByTier)(providers, coerced)).to.deep.equal(providers);
76
- });
77
- it('explicitly-empty [] also → undefined (run all; the cap restricts, it cannot disable)', () => {
78
- (0, chai_1.expect)((0, HealingOrchestrator_1.coerceHealingTiersCap)([])).to.equal(undefined);
69
+ it('runs no tier for an empty list', () => {
70
+ (0, chai_1.expect)((0, HealingOrchestrator_1.coerceHealingTiersCap)([])).to.deep.equal([]);
79
71
  });
72
+ for (const [label, value] of [
73
+ ['a string', '1,2'],
74
+ ['numbers as strings', ['1', '2']],
75
+ ['a list with a string in it', [1, '2']],
76
+ ['a number that is no tier', [1, 6]],
77
+ ['tier 0', [0]],
78
+ ['a fraction', [1.5]],
79
+ ['a lone number', 4],
80
+ ['an object', { llm: false }],
81
+ ['true', true],
82
+ ]) {
83
+ it(`runs only the tiers that stay on the server for ${label}`, () => {
84
+ (0, chai_1.expect)((0, HealingOrchestrator_1.coerceHealingTiersCap)(value)).to.deep.equal([1, 2, 3]);
85
+ (0, chai_1.expect)((0, HealingOrchestrator_1.filterProvidersByTier)(providers, (0, HealingOrchestrator_1.coerceHealingTiersCap)(value))).to.deep.equal(providers.slice(0, 3));
86
+ });
87
+ }
80
88
  });
81
89
  // What CommandInterceptor hands attemptHealing, read from the session's
82
90
  // capabilities.
83
91
  describe('healingTiersFromCaps', () => {
84
92
  it('reads xe:options.healingTiers', () => {
85
- (0, chai_1.expect)((0, HealingOrchestrator_1.healingTiersFromCaps)({ 'xe:options': { healingTiers: [1, 2] } })).to.deep.equal([
86
- 1, 2,
87
- ]);
93
+ (0, chai_1.expect)((0, HealingOrchestrator_1.healingTiersFromCaps)({ 'xe:options': { healingTiers: [1, 2] } })).to.deep.equal({
94
+ tiers: [1, 2],
95
+ });
88
96
  });
89
97
  it('still reads the xenon:options alias', () => {
90
- (0, chai_1.expect)((0, HealingOrchestrator_1.healingTiersFromCaps)({ 'xenon:options': { healingTiers: [3] } })).to.deep.equal([3]);
98
+ (0, chai_1.expect)((0, HealingOrchestrator_1.healingTiersFromCaps)({ 'xenon:options': { healingTiers: [3] } })).to.deep.equal({
99
+ tiers: [3],
100
+ });
91
101
  });
92
102
  it('prefers xe:options when both set it', () => {
93
103
  (0, chai_1.expect)((0, HealingOrchestrator_1.healingTiersFromCaps)({
94
104
  'xenon:options': { healingTiers: [5] },
95
105
  'xe:options': { healingTiers: [1] },
96
- })).to.deep.equal([1]);
106
+ })).to.deep.equal({ tiers: [1] });
107
+ });
108
+ it('runs every tier with no capabilities, or none set', () => {
109
+ (0, chai_1.expect)((0, HealingOrchestrator_1.healingTiersFromCaps)(undefined)).to.deep.equal({ tiers: undefined });
110
+ (0, chai_1.expect)((0, HealingOrchestrator_1.healingTiersFromCaps)({})).to.deep.equal({ tiers: undefined });
111
+ (0, chai_1.expect)((0, HealingOrchestrator_1.healingTiersFromCaps)({ 'xe:options': {} })).to.deep.equal({ tiers: undefined });
97
112
  });
98
- it('fails open with no capabilities or a malformed value', () => {
99
- (0, chai_1.expect)((0, HealingOrchestrator_1.healingTiersFromCaps)(undefined)).to.equal(undefined);
100
- (0, chai_1.expect)((0, HealingOrchestrator_1.healingTiersFromCaps)({})).to.equal(undefined);
101
- (0, chai_1.expect)((0, HealingOrchestrator_1.healingTiersFromCaps)({ 'xe:options': { healingTiers: ['1'] } })).to.equal(undefined);
113
+ it('names a value it could not read, for the log', () => {
114
+ (0, chai_1.expect)((0, HealingOrchestrator_1.healingTiersFromCaps)({ 'xe:options': { healingTiers: ['1'] } })).to.deep.equal({
115
+ tiers: [1, 2, 3],
116
+ unreadable: 'healingTiers is ["1"], not a list of tier numbers from 1 to 5',
117
+ });
118
+ });
119
+ // xenonOptionsIn skips a namespace that isn't an object, so this one used
120
+ // to read as "nothing set", and ran every tier.
121
+ it('runs only the tiers that stay on the server when xe:options is not an object', () => {
122
+ (0, chai_1.expect)((0, HealingOrchestrator_1.healingTiersFromCaps)({ 'xe:options': '{"healingTiers":[1]}' })).to.deep.equal({
123
+ tiers: [1, 2, 3],
124
+ unreadable: 'xe:options is "{\\"healingTiers\\":[1]}", not an object',
125
+ });
126
+ (0, chai_1.expect)((0, HealingOrchestrator_1.healingTiersFromCaps)({
127
+ 'xenon:options': ['healingTiers'],
128
+ 'xe:options': { healingTiers: [1, 2, 3, 4, 5] },
129
+ })).to.deep.equal({
130
+ tiers: [1, 2, 3],
131
+ unreadable: 'xenon:options is ["healingTiers"], not an object',
132
+ });
102
133
  });
103
134
  });
104
135
  });
@@ -136,6 +167,20 @@ describe('HealingOrchestrator.attemptHealing allowedTiers dispatch gate', () =>
136
167
  yield orchestrator.attemptHealing('sess-1', mockDriver, 'xpath', '//broken', [1]);
137
168
  (0, chai_1.expect)(dispatched).to.deep.equal(['ResilioTree Provider']);
138
169
  }));
170
+ it('collects nothing and dispatches nothing when no tier is allowed', () => __awaiter(void 0, void 0, void 0, function* () {
171
+ let collected = 0;
172
+ const driver = Object.assign(Object.assign({}, mockDriver), { getPageSource: () => __awaiter(void 0, void 0, void 0, function* () {
173
+ collected++;
174
+ return '<xml/>';
175
+ }), getScreenshot: () => __awaiter(void 0, void 0, void 0, function* () {
176
+ collected++;
177
+ return 'fake-screenshot';
178
+ }) });
179
+ const healed = yield orchestrator.attemptHealing('sess-3', driver, 'xpath', '//broken', []);
180
+ (0, chai_1.expect)(healed).to.equal(null);
181
+ (0, chai_1.expect)(dispatched).to.deep.equal([]);
182
+ (0, chai_1.expect)(collected).to.equal(0);
183
+ }));
139
184
  it('dispatches to the full provider set when allowedTiers is omitted (regression)', () => __awaiter(void 0, void 0, void 0, function* () {
140
185
  yield orchestrator.attemptHealing('sess-2', mockDriver, 'xpath', '//broken');
141
186
  (0, chai_1.expect)(dispatched).to.deep.equal([
@@ -0,0 +1,219 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ require("reflect-metadata");
16
+ const chai_1 = require("chai");
17
+ const sinon_1 = __importDefault(require("sinon"));
18
+ const typedi_1 = require("typedi");
19
+ const base_driver_1 = require("@appium/base-driver");
20
+ const CommandInterceptor_1 = require("../../src/interceptors/CommandInterceptor");
21
+ const HealingOrchestrator_1 = require("../../src/services/healing/HealingOrchestrator");
22
+ const AIService_1 = require("../../src/services/AIService");
23
+ const SelfHealingSwitch_1 = require("../../src/services/settings/SelfHealingSwitch");
24
+ const SessionManager_1 = require("../../src/sessions/SessionManager");
25
+ const IPluginArgs_1 = require("../../src/interfaces/IPluginArgs");
26
+ const container_registration_1 = require("../helpers/container-registration");
27
+ const scratch_database_1 = require("../helpers/scratch-database");
28
+ /**
29
+ * A session's `xe:options.healingTiers` keeps its screen away from the AI
30
+ * provider: with `[1, 2, 3]`, Visual AI (4) and the LLM (5) never run, so the
31
+ * screenshot and page source never leave the server.
32
+ *
33
+ * Through 2.14 the interceptor read the tiers from SESSION_MANAGER, which holds
34
+ * a local session only with the dashboard on or a video recorded. A session
35
+ * with `record_video: false` on a server with the dashboard off (the default),
36
+ * a node's for its hub included, wasn't there: the interceptor found no
37
+ * capabilities and ran every tier. The tiers are now read from the session's
38
+ * own driver, which Appium hands the plugin with every command. These sessions
39
+ * are registered nowhere, as such a session isn't.
40
+ */
41
+ describe("a session's healing tiers, on a session the dashboard doesn't track", () => {
42
+ (0, scratch_database_1.useScratchDatabase)();
43
+ let restoreContainer;
44
+ let dispatched;
45
+ let visualFind;
46
+ let healLocator;
47
+ const drivers = [];
48
+ const pluginArgs = Object.assign(Object.assign({}, IPluginArgs_1.DefaultPluginArgs), { enableDashboard: false });
49
+ beforeEach(() => {
50
+ restoreContainer = (0, container_registration_1.saveRegistrations)(HealingOrchestrator_1.HealingOrchestrator, SelfHealingSwitch_1.SelfHealingSwitch);
51
+ // The switch as at boot with nothing saved: healing on.
52
+ typedi_1.Container.set(SelfHealingSwitch_1.SelfHealingSwitch, new SelfHealingSwitch_1.SelfHealingSwitch());
53
+ const etalons = {
54
+ getSignature: () => __awaiter(void 0, void 0, void 0, function* () { return null; }),
55
+ saveSignature: () => __awaiter(void 0, void 0, void 0, function* () { }),
56
+ };
57
+ const orchestrator = new HealingOrchestrator_1.HealingOrchestrator(etalons);
58
+ dispatched = [];
59
+ const providers = orchestrator.providers;
60
+ // The three local tiers are only recorded. Visual AI and the LLM run as
61
+ // they are, up to the AI service, which is stubbed: what reaches it is
62
+ // what would have gone to the provider.
63
+ for (const provider of providers.slice(0, 3)) {
64
+ provider.heal = () => __awaiter(void 0, void 0, void 0, function* () {
65
+ dispatched.push(provider.name);
66
+ return null;
67
+ });
68
+ }
69
+ for (const provider of providers.slice(3)) {
70
+ const heal = provider.heal.bind(provider);
71
+ provider.heal = (context) => __awaiter(void 0, void 0, void 0, function* () {
72
+ dispatched.push(provider.name);
73
+ return heal(context);
74
+ });
75
+ }
76
+ typedi_1.Container.set(HealingOrchestrator_1.HealingOrchestrator, orchestrator);
77
+ sinon_1.default.stub(AIService_1.AI_SERVICE, 'isEnabled').returns(true);
78
+ visualFind = sinon_1.default.stub(AIService_1.AI_SERVICE, 'visualFind').resolves(null);
79
+ healLocator = sinon_1.default.stub(AIService_1.AI_SERVICE, 'healLocator').resolves(null);
80
+ });
81
+ afterEach(() => __awaiter(void 0, void 0, void 0, function* () {
82
+ sinon_1.default.restore();
83
+ restoreContainer();
84
+ for (const driver of drivers.splice(0))
85
+ yield driver.deleteSession();
86
+ }));
87
+ /**
88
+ * The driver Appium hands the plugin for a session's commands: a session
89
+ * created from the capabilities the client sent, as Appium creates it.
90
+ */
91
+ function sessionWith(xenonOptions) {
92
+ return __awaiter(this, void 0, void 0, function* () {
93
+ const driver = new base_driver_1.BaseDriver({});
94
+ // Every driver warns that it doesn't know `xe:options`; not this spec's business.
95
+ sinon_1.default.stub(driver.log, 'warn');
96
+ const alwaysMatch = {
97
+ platformName: 'Android',
98
+ 'appium:automationName': 'UiAutomator2',
99
+ };
100
+ if (xenonOptions)
101
+ Object.assign(alwaysMatch, xenonOptions);
102
+ yield driver.createSession({ alwaysMatch, firstMatch: [{}] });
103
+ drivers.push(driver);
104
+ Object.assign(driver, {
105
+ getPageSource: () => __awaiter(this, void 0, void 0, function* () { return '<hierarchy><node text="Pay now"/></hierarchy>'; }),
106
+ getScreenshot: () => __awaiter(this, void 0, void 0, function* () { return 'iVBORw0KGgo='; }),
107
+ findElements: () => __awaiter(this, void 0, void 0, function* () { return []; }),
108
+ });
109
+ return driver;
110
+ });
111
+ }
112
+ /** A findElement the driver answers with NoSuchElement, as a broken selector does. */
113
+ function findMissing(driver_1, _a) {
114
+ return __awaiter(this, arguments, void 0, function* (driver, { isHub }) {
115
+ const sessionId = driver.sessionId;
116
+ return typedi_1.Container.get(CommandInterceptor_1.CommandInterceptor)
117
+ .handle(() => __awaiter(this, void 0, void 0, function* () {
118
+ throw Object.assign(new Error('NoSuchElement: An element could not be located'), {
119
+ name: 'NoSuchElementError',
120
+ });
121
+ }), driver, 'findElement', ['xpath', '//android.widget.Button[@text="Pay"]', sessionId], pluginArgs, isHub)
122
+ .then(() => undefined, (err) => err);
123
+ });
124
+ }
125
+ for (const [where, isHub] of [
126
+ ['on a standalone server', true],
127
+ ["on a node running its hub's session", false],
128
+ ]) {
129
+ describe(where, () => {
130
+ it('never calls the AI provider for a session that allows tiers 1, 2 and 3', () => __awaiter(void 0, void 0, void 0, function* () {
131
+ const driver = yield sessionWith({ 'xe:options': { healingTiers: [1, 2, 3] } });
132
+ // The case: with the dashboard off and no video, nothing registers it.
133
+ (0, chai_1.expect)(SessionManager_1.SESSION_MANAGER.getSession(driver.sessionId)).to.equal(undefined);
134
+ const thrown = yield findMissing(driver, { isHub });
135
+ (0, chai_1.expect)(visualFind.called, 'Visual AI sent the screenshot').to.equal(false);
136
+ (0, chai_1.expect)(healLocator.called, 'the LLM was sent the page source').to.equal(false);
137
+ (0, chai_1.expect)(dispatched).to.deep.equal([
138
+ 'ResilioTree Provider',
139
+ 'Fuzzy XML Provider',
140
+ 'OCR Text Provider',
141
+ ]);
142
+ // Nothing healed it, so the test sees its own failure.
143
+ (0, chai_1.expect)(thrown === null || thrown === void 0 ? void 0 : thrown.name).to.equal('NoSuchElementError');
144
+ }));
145
+ it('reads the xenon:options alias the same way', () => __awaiter(void 0, void 0, void 0, function* () {
146
+ const driver = yield sessionWith({ 'xenon:options': { healingTiers: [2] } });
147
+ yield findMissing(driver, { isHub });
148
+ (0, chai_1.expect)(visualFind.called).to.equal(false);
149
+ (0, chai_1.expect)(healLocator.called).to.equal(false);
150
+ (0, chai_1.expect)(dispatched).to.deep.equal(['Fuzzy XML Provider']);
151
+ }));
152
+ it('still runs every tier for a session that asks for none in particular', () => __awaiter(void 0, void 0, void 0, function* () {
153
+ const driver = yield sessionWith(null);
154
+ yield findMissing(driver, { isHub });
155
+ (0, chai_1.expect)(dispatched).to.deep.equal([
156
+ 'ResilioTree Provider',
157
+ 'Fuzzy XML Provider',
158
+ 'OCR Text Provider',
159
+ 'Visual AI Provider',
160
+ 'LLM Reasoning Provider',
161
+ ]);
162
+ (0, chai_1.expect)(visualFind.calledOnce).to.equal(true);
163
+ (0, chai_1.expect)(healLocator.calledOnce).to.equal(true);
164
+ }));
165
+ });
166
+ }
167
+ describe('a value that is not a list of tier numbers', () => {
168
+ for (const [label, value] of [
169
+ ['a string', '1,2,3'],
170
+ ['numbers as strings', ['1', '2']],
171
+ ['a list with a string in it', [4, 'llm']],
172
+ ['a number that is no tier', [1, 6]],
173
+ ['a lone number', 4],
174
+ ['an object', { visualAi: false }],
175
+ ]) {
176
+ it(`runs only the tiers that stay on the server: ${label}`, () => __awaiter(void 0, void 0, void 0, function* () {
177
+ const driver = yield sessionWith({ 'xe:options': { healingTiers: value } });
178
+ yield findMissing(driver, { isHub: true });
179
+ (0, chai_1.expect)(visualFind.called).to.equal(false);
180
+ (0, chai_1.expect)(healLocator.called).to.equal(false);
181
+ (0, chai_1.expect)(dispatched).to.deep.equal([
182
+ 'ResilioTree Provider',
183
+ 'Fuzzy XML Provider',
184
+ 'OCR Text Provider',
185
+ ]);
186
+ }));
187
+ }
188
+ it('runs only the tiers that stay on the server when xe:options is not an object', () => __awaiter(void 0, void 0, void 0, function* () {
189
+ const driver = yield sessionWith({ 'xe:options': '{"healingTiers":[1,2,3]}' });
190
+ yield findMissing(driver, { isHub: true });
191
+ (0, chai_1.expect)(visualFind.called).to.equal(false);
192
+ (0, chai_1.expect)(healLocator.called).to.equal(false);
193
+ (0, chai_1.expect)(dispatched).to.deep.equal([
194
+ 'ResilioTree Provider',
195
+ 'Fuzzy XML Provider',
196
+ 'OCR Text Provider',
197
+ ]);
198
+ }));
199
+ it('says so in the log, once per session', () => __awaiter(void 0, void 0, void 0, function* () {
200
+ const warn = sinon_1.default.stub(typedi_1.Container.get(CommandInterceptor_1.CommandInterceptor).log, 'warn');
201
+ const driver = yield sessionWith({ 'xe:options': { healingTiers: ['1', '2'] } });
202
+ yield findMissing(driver, { isHub: true });
203
+ yield findMissing(driver, { isHub: true });
204
+ const said = warn.getCalls().filter((c) => /healingTiers/.test(String(c.args[0])));
205
+ (0, chai_1.expect)(said).to.have.length(1);
206
+ (0, chai_1.expect)(String(said[0].args[0])).to.contain('["1","2"]');
207
+ }));
208
+ });
209
+ it('heals with no tier, and takes no screenshot, for a session that lists none', () => __awaiter(void 0, void 0, void 0, function* () {
210
+ const driver = yield sessionWith({ 'xe:options': { healingTiers: [] } });
211
+ const screenshot = sinon_1.default.spy(driver, 'getScreenshot');
212
+ const source = sinon_1.default.spy(driver, 'getPageSource');
213
+ const thrown = yield findMissing(driver, { isHub: true });
214
+ (0, chai_1.expect)(dispatched).to.deep.equal([]);
215
+ (0, chai_1.expect)(screenshot.called).to.equal(false);
216
+ (0, chai_1.expect)(source.called).to.equal(false);
217
+ (0, chai_1.expect)(thrown === null || thrown === void 0 ? void 0 : thrown.name).to.equal('NoSuchElementError');
218
+ }));
219
+ });
@@ -71,6 +71,11 @@ const hubSessionToken_1 = require("../../src/gateway/hubSessionToken");
71
71
  const internalCall_1 = require("../../src/gateway/internalCall");
72
72
  const nodeWebDriverUrl_1 = require("../../src/gateway/nodeWebDriverUrl");
73
73
  const defaultGateway_1 = require("../../src/gateway/defaultGateway");
74
+ const CommandInterceptor_1 = require("../../src/interceptors/CommandInterceptor");
75
+ const HealingOrchestrator_1 = require("../../src/services/healing/HealingOrchestrator");
76
+ const types_1 = require("../../src/services/healing/types");
77
+ const SelfHealingSwitch_1 = require("../../src/services/settings/SelfHealingSwitch");
78
+ const IPluginArgs_1 = require("../../src/interfaces/IPluginArgs");
74
79
  const container_registration_1 = require("../helpers/container-registration");
75
80
  const scratch_database_1 = require("../helpers/scratch-database");
76
81
  /**
@@ -104,6 +109,15 @@ const DRIVER_ROUTES = {
104
109
  POST: { command: 'stopRecordingScreen', payloadParams: { optional: ['options'] } },
105
110
  },
106
111
  };
112
+ /**
113
+ * The node's findElement as its plugin runs it, when a test sets it: the
114
+ * driver's own find goes through Xenon's CommandInterceptor.
115
+ */
116
+ let nodeFind;
117
+ /** The hub's findElement as its plugin runs it, for a session the hub runs itself. */
118
+ let hubFind;
119
+ /** The node's answer to an execute script, when a test sets it. */
120
+ let nodeExecute;
107
121
  function fakeDriver(name) {
108
122
  const { errors } = appiumBaseDriver();
109
123
  const commands = [];
@@ -120,14 +134,21 @@ function fakeDriver(name) {
120
134
  if (command === 'getUrl')
121
135
  return `https://${name}.example/`;
122
136
  if (command === 'findElement') {
137
+ if (name === 'node' && nodeFind)
138
+ return nodeFind(args);
139
+ if (name === 'hub' && hubFind)
140
+ return hubFind(args);
123
141
  if (args[1] === 'missing')
124
142
  throw new errors.NoSuchElementError();
125
143
  return { 'element-6066-11e4-a52e-4f735466cecf': `${name}-el` };
126
144
  }
127
145
  if (command === 'stopRecordingScreen')
128
146
  return 'node-video.mp4';
129
- if (command === 'execute')
147
+ if (command === 'execute') {
148
+ if (name === 'node' && nodeExecute)
149
+ return nodeExecute(args);
130
150
  return { ranOn: name, script: args[0] };
151
+ }
131
152
  if (command === 'deleteSession') {
132
153
  sessions.delete(args[0]);
133
154
  return null;
@@ -458,6 +479,72 @@ describe('the session gateway between a hub and a node, in Appium 3’s own serv
458
479
  const row = yield scratch.db.session.findUnique({ where: { id } });
459
480
  (0, chai_1.expect)(row === null || row === void 0 ? void 0 : row.name).to.equal('Checkout');
460
481
  }));
482
+ describe('a network-capture script', () => {
483
+ // A captured request's headers and bodies can carry the app's sign-in
484
+ // tokens. They are for admins only (the /interceptor routes, the Network
485
+ // panel, the live events), but the hub's command log is read by anyone
486
+ // who can see the session (`session_log`, bug reports).
487
+ const SECRET = 'Bearer eyJ-app-access-token';
488
+ const HAR = {
489
+ log: {
490
+ entries: [{ request: { headers: [{ name: 'authorization', value: SECRET }] } }],
491
+ },
492
+ };
493
+ afterEach(() => {
494
+ nodeExecute = undefined;
495
+ });
496
+ function loggedRows(id, count) {
497
+ return __awaiter(this, void 0, void 0, function* () {
498
+ const deadline = Date.now() + 5000;
499
+ let rows = yield scratch.db.sessionLog.findMany({ where: { session_id: id } });
500
+ while (rows.length < count && Date.now() < deadline) {
501
+ yield new Promise((r) => setTimeout(r, 10));
502
+ rows = yield scratch.db.sessionLog.findMany({ where: { session_id: id } });
503
+ }
504
+ return rows;
505
+ });
506
+ }
507
+ it('reaches the test whole, and the hub’s command log keeps neither its arguments nor its answer', () => __awaiter(this, void 0, void 0, function* () {
508
+ var _a;
509
+ yield boot({ dashboard: true });
510
+ const { id } = yield remoteSession();
511
+ nodeExecute = (_a) => __awaiter(this, [_a], void 0, function* ([script]) { return /addMock/.test(script) ? { id: 'mock-1' } : script.includes('exportHar') ? HAR : [HAR]; });
512
+ const har = yield execute(id, 'xenon: exportHar', []);
513
+ (0, chai_1.expect)(har.status).to.equal(200);
514
+ (0, chai_1.expect)(har.body.value, 'the test gets its capture').to.deep.equal(HAR);
515
+ yield execute(id, 'xe:getRequests', []);
516
+ yield execute(id, 'getMocks', []);
517
+ yield execute(id, 'xenon: addMock', [
518
+ { match: { url: '**/login' }, respond: { body: { token: SECRET } } },
519
+ ]);
520
+ const rows = yield loggedRows(id, 4);
521
+ (0, chai_1.expect)(rows.map((r) => r.command_name)).to.deep.equal([
522
+ 'execute',
523
+ 'execute',
524
+ 'execute',
525
+ 'execute',
526
+ ]);
527
+ for (const row of rows) {
528
+ (0, chai_1.expect)(`${row.body}${row.response}`, (_a = row.body) !== null && _a !== void 0 ? _a : '').to.not.include('eyJ-app-access-token');
529
+ (0, chai_1.expect)(row.is_success).to.equal(true);
530
+ }
531
+ // Which script ran stays on record.
532
+ (0, chai_1.expect)(rows.map((r) => { var _a; return JSON.parse((_a = r.body) !== null && _a !== void 0 ? _a : '{}').script; })).to.deep.equal([
533
+ 'xenon: exportHar',
534
+ 'xe:getRequests',
535
+ 'getMocks',
536
+ 'xenon: addMock',
537
+ ]);
538
+ }));
539
+ it('any other script is logged whole, as before', () => __awaiter(this, void 0, void 0, function* () {
540
+ yield boot({ dashboard: true });
541
+ const { id } = yield remoteSession();
542
+ nodeExecute = () => __awaiter(this, void 0, void 0, function* () { return ({ output: SECRET }); });
543
+ yield execute(id, 'mobile: shell', [{ command: 'echo' }]);
544
+ const [row] = yield loggedRows(id, 1);
545
+ (0, chai_1.expect)(row.response).to.include('eyJ-app-access-token');
546
+ }));
547
+ });
461
548
  it('with the hub’s dashboard off, sends a session-details command on to the node', () => __awaiter(this, void 0, void 0, function* () {
462
549
  yield boot({ dashboard: false });
463
550
  const { id } = yield remoteSession();
@@ -639,4 +726,159 @@ describe('the session gateway between a hub and a node, in Appium 3’s own serv
639
726
  yield (0, loopbackRequest_1.default)(nodeOrigin).get(`${nodeBase}/session/${id}/url`).expect(200);
640
727
  }));
641
728
  });
729
+ /**
730
+ * A find on a node's phone runs on the node, so the node's interceptor
731
+ * heals it. Through 2.14 the heal was recorded nowhere: the node keeps no
732
+ * record of a session the hub created, and the hub saw only the element the
733
+ * node answered with, so the find was logged as found (and counted toward
734
+ * the selector's verification as a clean find). The node now hands the heal
735
+ * back on its answer, and the hub, which owns the session record, records
736
+ * it as it records a local session's.
737
+ */
738
+ describe("a heal on a node's phone", () => {
739
+ const W3C = 'element-6066-11e4-a52e-4f735466cecf';
740
+ let attemptHealing;
741
+ let nodeLogged;
742
+ let restoreHealing;
743
+ beforeEach(() => {
744
+ restoreHealing = (0, container_registration_1.saveRegistrations)(HealingOrchestrator_1.HealingOrchestrator, SelfHealingSwitch_1.SelfHealingSwitch);
745
+ typedi_1.Container.set(SelfHealingSwitch_1.SelfHealingSwitch, new SelfHealingSwitch_1.SelfHealingSwitch());
746
+ attemptHealing = sinon_1.default.stub().resolves({
747
+ id: 'node-healed-el',
748
+ tier: types_1.HealingTier.TIER_2_FUZZY_XML,
749
+ confidence: 0.82,
750
+ originalSelector: 'broken',
751
+ originalStrategy: 'xpath',
752
+ recommendedSelector: "//*[@resource-id='com.example:id/login']",
753
+ recommendedStrategy: 'xpath',
754
+ });
755
+ typedi_1.Container.set(HealingOrchestrator_1.HealingOrchestrator, { attemptHealing });
756
+ // Hub and node share this process; the node's own record of a heal is
757
+ // the node's (on a real node it has no Session row to write it to).
758
+ nodeLogged = sinon_1.default.stub(CommandInterceptor_1.CommandInterceptor.prototype, 'logHealingEvent').resolves();
759
+ const { errors } = appiumBaseDriver();
760
+ nodeFind = hubFind = (args) => typedi_1.Container.get(CommandInterceptor_1.CommandInterceptor).handle(() => __awaiter(this, void 0, void 0, function* () {
761
+ if (args[1] === 'broken')
762
+ throw new errors.NoSuchElementError();
763
+ return { [W3C]: 'node-el' };
764
+ }), { sessionId: args[2] }, 'findElement', args, Object.assign({}, IPluginArgs_1.DefaultPluginArgs), false);
765
+ });
766
+ afterEach(() => {
767
+ nodeFind = hubFind = undefined;
768
+ restoreHealing();
769
+ });
770
+ /** The session's logged finds; the hub logs a command after it has answered it. */
771
+ function logsOf(id) {
772
+ return __awaiter(this, void 0, void 0, function* () {
773
+ for (let i = 0; i < 100; i++) {
774
+ const rows = yield scratch.db.sessionLog.findMany({
775
+ where: { session_id: id, command_name: 'findElement' },
776
+ });
777
+ if (rows.length)
778
+ return rows;
779
+ yield new Promise((r) => setTimeout(r, 20));
780
+ }
781
+ return [];
782
+ });
783
+ }
784
+ it('is recorded on the hub, against the session, with what it healed to', () => __awaiter(this, void 0, void 0, function* () {
785
+ yield boot({ dashboard: true });
786
+ const { id } = yield remoteSession();
787
+ const res = yield (0, loopbackRequest_1.default)(hubUrl)
788
+ .post(`/wd/hub/session/${id}/element`)
789
+ .send({ using: 'xpath', value: 'broken' });
790
+ (0, chai_1.expect)(res.status).to.equal(200);
791
+ (0, chai_1.expect)(res.body.value[W3C]).to.equal('node-healed-el');
792
+ const [row] = yield logsOf(id);
793
+ (0, chai_1.expect)(row, 'the find was logged').to.not.equal(undefined);
794
+ (0, chai_1.expect)(row).to.deep.include({
795
+ is_healed: true,
796
+ original_strategy: 'xpath',
797
+ original_selector: 'broken',
798
+ healed_strategy: 'xpath',
799
+ healed_selector: "//*[@resource-id='com.example:id/login']",
800
+ healing_confidence: 0.82,
801
+ healing_tier: 'Fuzzy XML',
802
+ });
803
+ (0, chai_1.expect)(nodeLogged.called, 'recorded on the node as well').to.equal(false);
804
+ }));
805
+ it("is Xenon's own: the client's answer carries nothing about it", () => __awaiter(this, void 0, void 0, function* () {
806
+ yield boot({ dashboard: true });
807
+ const { id } = yield remoteSession();
808
+ const res = yield (0, loopbackRequest_1.default)(hubUrl)
809
+ .post(`/wd/hub/session/${id}/element`)
810
+ .send({ using: 'xpath', value: 'broken' });
811
+ (0, chai_1.expect)(Object.keys(res.headers).filter((h) => h.startsWith('x-xenon'))).to.deep.equal([]);
812
+ }));
813
+ it("records the selector of a find on a node's phone that needed no heal", () => __awaiter(this, void 0, void 0, function* () {
814
+ yield boot({ dashboard: true });
815
+ const { id } = yield remoteSession();
816
+ yield (0, loopbackRequest_1.default)(hubUrl)
817
+ .post(`/wd/hub/session/${id}/element`)
818
+ .send({ using: 'id', value: 'login' })
819
+ .expect(200);
820
+ const [row] = yield logsOf(id);
821
+ (0, chai_1.expect)(row).to.deep.include({
822
+ is_healed: false,
823
+ original_strategy: 'id',
824
+ original_selector: 'login',
825
+ });
826
+ }));
827
+ it('takes a heal only from the node, never from the client', () => __awaiter(this, void 0, void 0, function* () {
828
+ yield boot({ dashboard: true });
829
+ const { id } = yield remoteSession();
830
+ const forged = Buffer.from(JSON.stringify({ originalSelector: 'login', healedSelector: '//x', confidence: 1 })).toString('base64url');
831
+ yield (0, loopbackRequest_1.default)(hubUrl)
832
+ .post(`/wd/hub/session/${id}/element`)
833
+ .set('x-xenon-heal', forged)
834
+ .send({ using: 'id', value: 'login' })
835
+ .expect(200);
836
+ const [row] = yield logsOf(id);
837
+ (0, chai_1.expect)(row.is_healed).to.equal(false);
838
+ (0, chai_1.expect)(nodeRequests[0].headers['x-xenon-heal']).to.equal(undefined);
839
+ }));
840
+ it('is recorded by the node itself when the find did not come from a hub', () => __awaiter(this, void 0, void 0, function* () {
841
+ yield boot();
842
+ const { id } = yield remoteSession({ registered: false });
843
+ const res = yield (0, loopbackRequest_1.default)(nodeOrigin)
844
+ .post(`/node/session/${id}/element`)
845
+ .send({ using: 'xpath', value: 'broken' });
846
+ (0, chai_1.expect)(res.status).to.equal(200);
847
+ (0, chai_1.expect)(res.headers['x-xenon-heal']).to.equal(undefined);
848
+ (0, chai_1.expect)(nodeLogged.calledOnce).to.equal(true);
849
+ }));
850
+ it('is recorded on the hub for its own session, whatever hub token a client sends', () => __awaiter(this, void 0, void 0, function* () {
851
+ yield boot({ dashboard: true });
852
+ const id = `hub-session-${stamp}`;
853
+ hub.sessions.add(id);
854
+ const res = yield (0, loopbackRequest_1.default)(hubUrl)
855
+ .post(`/wd/hub/session/${id}/element`)
856
+ .set(hubSessionToken_1.HUB_TOKEN_HEADER, 'not-a-hub')
857
+ .send({ using: 'xpath', value: 'broken' });
858
+ (0, chai_1.expect)(res.status).to.equal(200);
859
+ (0, chai_1.expect)(res.headers['x-xenon-heal']).to.equal(undefined);
860
+ (0, chai_1.expect)(nodeLogged.calledOnce, 'recorded by the server that healed it').to.equal(true);
861
+ }));
862
+ it('is neither sent nor recorded on the node when too long for the header', () => __awaiter(this, void 0, void 0, function* () {
863
+ yield boot({ dashboard: true });
864
+ const { id } = yield remoteSession();
865
+ attemptHealing.resolves({
866
+ id: 'node-healed-el',
867
+ tier: types_1.HealingTier.TIER_2_FUZZY_XML,
868
+ confidence: 0.82,
869
+ originalSelector: 'broken',
870
+ originalStrategy: 'xpath',
871
+ recommendedSelector: `//*[@text='${'x'.repeat(10000)}']`,
872
+ recommendedStrategy: 'xpath',
873
+ });
874
+ const res = yield (0, loopbackRequest_1.default)(hubUrl)
875
+ .post(`/wd/hub/session/${id}/element`)
876
+ .send({ using: 'xpath', value: 'broken' });
877
+ (0, chai_1.expect)(res.status).to.equal(200);
878
+ (0, chai_1.expect)(res.body.value[W3C]).to.equal('node-healed-el');
879
+ const [row] = yield logsOf(id);
880
+ (0, chai_1.expect)(row.is_healed).to.equal(false);
881
+ (0, chai_1.expect)(nodeLogged.called, 'recorded on the node, which has no row for it').to.equal(false);
882
+ }));
883
+ });
642
884
  });