@xenon-device-management/xenon 2.13.2 → 2.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (350) hide show
  1. package/README.md +17 -8
  2. package/lib/package.json +5 -5
  3. package/lib/public/assets/{AnnotationOverlay-BXruClDk.js → AnnotationOverlay-Btq3dZRI.js} +1 -1
  4. package/lib/public/assets/{ApiKeyGate-qTS4LPLC.js → ApiKeyGate-C9OoAgg_.js} +1 -1
  5. package/lib/public/assets/DeviceMosaicView-DBC_xczB.js +2 -0
  6. package/lib/public/assets/{EmptyState-Cr17PdDv.js → EmptyState-DqWU2-vM.js} +1 -1
  7. package/lib/public/assets/{FieldGroup-Bvn08svv.js → FieldGroup-CveQ3xrA.js} +1 -1
  8. package/lib/public/assets/{FilterMenu-DDSO8aDf.js → FilterMenu-BfKDSfHz.js} +1 -1
  9. package/lib/public/assets/{Menu-B-Qn2X3T.js → Menu-CtdCybzA.js} +1 -1
  10. package/lib/public/assets/{Modal-Cg9V5JYU.js → Modal-Bsvj1tm2.js} +1 -1
  11. package/lib/public/assets/{RecordingPage-BTUs9VTW.js → RecordingPage-CRofOiyx.js} +1 -1
  12. package/lib/public/assets/{RecordingsPage-DYcfxITK.js → RecordingsPage-jNQHb2IR.js} +2 -2
  13. package/lib/public/assets/{SegmentedControl-Zy7suhT5.js → SegmentedControl-C8HtTha-.js} +1 -1
  14. package/lib/public/assets/{SettingCard-CWYmycoe.js → SettingCard-D8K1LZNo.js} +1 -1
  15. package/lib/public/assets/{Table-io9fwa98.js → Table-zNTX92Pn.js} +1 -1
  16. package/lib/public/assets/{activity-Bdf_uIWl.js → activity-CJ8LwZq9.js} +1 -1
  17. package/lib/public/assets/ai-settings-CgxQZqrc.js +16 -0
  18. package/lib/public/assets/{api-keys-CPqtVpm3.js → api-keys-ECpo-zTo.js} +1 -1
  19. package/lib/public/assets/{apps-BgZan0H9.js → apps-BjGymyf3.js} +2 -2
  20. package/lib/public/assets/{arrow-left-B27ckB31.js → arrow-left-BWfCbeHP.js} +1 -1
  21. package/lib/public/assets/{arrow-right-DOF8ITCz.js → arrow-right-DC7UGf_n.js} +1 -1
  22. package/lib/public/assets/{arrow-up-right-eFG5KTFS.js → arrow-up-right-BOjwybDk.js} +1 -1
  23. package/lib/public/assets/{auth-shell-DlC3_27j.js → auth-shell-CxMTo9aH.js} +2 -2
  24. package/lib/public/assets/{builds-page-BuXpPfKo.js → builds-page-DOCm4HkA.js} +1 -1
  25. package/lib/public/assets/{button-fIW8owMI.js → button-Cg08-EMT.js} +1 -1
  26. package/lib/public/assets/{calendar-WNjmrF-a.js → calendar-CA1ikLJs.js} +1 -1
  27. package/lib/public/assets/{check-BdTh1Sq7.js → check-BMTaH72W.js} +1 -1
  28. package/lib/public/assets/{chevron-right-B4BKlsqk.js → chevron-right-B2QOv9Nx.js} +1 -1
  29. package/lib/public/assets/{circle-check-zJU80_EF.js → circle-check-CYXBoUqT.js} +1 -1
  30. package/lib/public/assets/{circle-x-5Uwquo0h.js → circle-x-DTwnSAnl.js} +1 -1
  31. package/lib/public/assets/{clock-BtUprDsZ.js → clock-C8_HViQP.js} +1 -1
  32. package/lib/public/assets/{copy-DGSbZI3u.js → copy-Be7LNMXV.js} +1 -1
  33. package/lib/public/assets/derive-UnLRp4r3.js +1 -0
  34. package/lib/public/assets/device-explorer-BOqtfGXz.js +282 -0
  35. package/lib/public/assets/{download-WG6l7BgD.js → download-B56lwxWP.js} +1 -1
  36. package/lib/public/assets/forgot-password-CCU6bzLn.js +1 -0
  37. package/lib/public/assets/h264Stream-C-OtmIuk.js +26 -0
  38. package/lib/public/assets/index-B9KQ6LsY.js +15 -0
  39. package/lib/public/assets/{index-DI1fGIyj.js → index-BOxe43p1.js} +5 -5
  40. package/lib/public/assets/{index-HDQVe1gi.js → index-CfxsUslp.js} +1 -1
  41. package/lib/public/assets/{input-BjDRKCea.js → input-CZHFjbSg.js} +1 -1
  42. package/lib/public/assets/{line-chart-C0zdgd0v.js → line-chart-Gl_Dz3X_.js} +1 -1
  43. package/lib/public/assets/{list-checks-BdW5CjM8.js → list-checks-CCgjeyGY.js} +1 -1
  44. package/lib/public/assets/{lock-Dy3L4I8E.js → lock-CkOwgDcx.js} +1 -1
  45. package/lib/public/assets/{login-BHbY9CLG.js → login-D5cc4w7M.js} +1 -1
  46. package/lib/public/assets/maintenance-settings-t7qr3fvX.js +6 -0
  47. package/lib/public/assets/{monitor-8xjkeHHi.js → monitor-DEJixKQO.js} +1 -1
  48. package/lib/public/assets/{mouse-pointer-2-CXdKMXQ8.js → mouse-pointer-2-CtOrk4M8.js} +1 -1
  49. package/lib/public/assets/{network-Cj8JOKON.js → network-Dii9qdvk.js} +1 -1
  50. package/lib/public/assets/{overview-CTg7F0im.js → overview-rolAACrO.js} +1 -1
  51. package/lib/public/assets/{page-header--aTyJXq6.js → page-header-COQZxJfR.js} +1 -1
  52. package/lib/public/assets/{play-B-FodgHV.js → play-DrN1Se_R.js} +1 -1
  53. package/lib/public/assets/{plus-CsIk10Cq.js → plus-5aq-9uRx.js} +1 -1
  54. package/lib/public/assets/{profile-page-DkFKkMfl.js → profile-page-6i5kY6kF.js} +1 -1
  55. package/lib/public/assets/{reset-password-I2-zqLlI.js → reset-password-sp2IBrc7.js} +1 -1
  56. package/lib/public/assets/runbook-page-DJDjeopU.js +253 -0
  57. package/lib/public/assets/{select-DvCSv25z.js → select-Bpyn7rWO.js} +1 -1
  58. package/lib/public/assets/{selector-detail-redirect-BLVfMJs_.js → selector-detail-redirect-BH74vmKv.js} +1 -1
  59. package/lib/public/assets/{selector-health-page-oqadRokn.js → selector-health-page-Cw_g218H.js} +2 -2
  60. package/lib/public/assets/session-detail-page-AZJE6XXE.js +4 -0
  61. package/lib/public/assets/settings-C3uyGhyc.js +6 -0
  62. package/lib/public/assets/{settings-Dep_3kbA.css → settings-K8UHWMdI.css} +1 -1
  63. package/lib/public/assets/{stat-tile-DV9Ix-4O.js → stat-tile-xVqAKIlI.js} +1 -1
  64. package/lib/public/assets/{tablet-6b_SEN2X.js → tablet-BxUScAeO.js} +1 -1
  65. package/lib/public/assets/{teams-BjZyfuHh.js → teams-B5ljGkY2.js} +1 -1
  66. package/lib/public/assets/{trash-2-CTZpPSdd.js → trash-2-B701i8sy.js} +1 -1
  67. package/lib/public/assets/{upload-P3XzwQUb.js → upload-C2gr44RS.js} +1 -1
  68. package/lib/public/assets/{use-builds-data-1cOCMf-z.js → use-builds-data-NeQOFEoy.js} +1 -1
  69. package/lib/public/assets/{use-password-reset-mode-DgF9w088.js → use-password-reset-mode-DEzJq5ef.js} +1 -1
  70. package/lib/public/assets/{users-DMF9Mk5H.js → users-D_vv_yz4.js} +1 -1
  71. package/lib/public/assets/{users-CiNTKUiz.js → users-MY6eNe6s.js} +1 -1
  72. package/lib/public/assets/{video-off-CHp1KU_T.js → video-off-OK1lNZQn.js} +1 -1
  73. package/lib/public/assets/webhook-settings-BU-C9lBf.js +1 -0
  74. package/lib/public/assets/{zap-DRz0tDq5.js → zap-DO9ry1iz.js} +1 -1
  75. package/lib/public/index.html +1 -1
  76. package/lib/schema.json +23 -25
  77. package/lib/src/XenonCapabilityManager.js +22 -5
  78. package/lib/src/app/dashboardPluginLink.js +117 -0
  79. package/lib/src/app/index.js +13 -34
  80. package/lib/src/app/openapi/control.yaml +73 -20
  81. package/lib/src/app/openapi/grid.yaml +31 -7
  82. package/lib/src/app/openapi/identity.yaml +103 -31
  83. package/lib/src/app/openapi/platform.yaml +165 -39
  84. package/lib/src/app/openapi/sessions.yaml +10 -3
  85. package/lib/src/app/routers/apikeys.js +14 -2
  86. package/lib/src/app/routers/auth.js +29 -5
  87. package/lib/src/app/routers/control.js +20 -2
  88. package/lib/src/app/routers/dashboard.js +69 -26
  89. package/lib/src/app/routers/grid.js +37 -13
  90. package/lib/src/app/routers/profile.js +21 -0
  91. package/lib/src/app/routers/streamType.js +7 -2
  92. package/lib/src/app/routers/users.js +10 -5
  93. package/lib/src/app/routers/webhook.js +8 -4
  94. package/lib/src/app/swagger.js +2 -0
  95. package/lib/src/app/ws/h264StreamWs.js +10 -1
  96. package/lib/src/config.js +51 -5
  97. package/lib/src/dashboard/commands.js +185 -141
  98. package/lib/src/dashboard/event-manager.js +88 -67
  99. package/lib/src/dashboard/services/failure-analysis-service.js +136 -45
  100. package/lib/src/dashboard/sessionCommandSummary.js +39 -0
  101. package/lib/src/data-service/appleFamily.js +43 -0
  102. package/lib/src/data-service/db.js +11 -0
  103. package/lib/src/data-service/device-service.js +6 -1
  104. package/lib/src/data-service/device-store.js +76 -3
  105. package/lib/src/data-service/deviceClaims.js +37 -0
  106. package/lib/src/data-service/deviceFieldOwners.js +25 -11
  107. package/lib/src/data-service/deviceSettings.js +79 -0
  108. package/lib/src/data-service/prisma-store.js +169 -15
  109. package/lib/src/data-service/web-config-service.js +50 -92
  110. package/lib/src/device-managers/AndroidDeviceManager.js +9 -31
  111. package/lib/src/device-managers/android/AndroidH264StreamService.js +69 -20
  112. package/lib/src/device-managers/android/H264Multiplexer.js +38 -4
  113. package/lib/src/device-managers/ios/WDAClient.js +44 -43
  114. package/lib/src/device-managers/shellCommands.js +96 -0
  115. package/lib/src/device-utils.js +114 -40
  116. package/lib/src/gateway/defaultGateway.js +1 -1
  117. package/lib/src/gateway/forwardToNode.js +2 -0
  118. package/lib/src/gateway/healReport.js +87 -0
  119. package/lib/src/gateway/sessionGateway.js +12 -2
  120. package/lib/src/generated/client/edge.js +18 -3
  121. package/lib/src/generated/client/index-browser.js +15 -0
  122. package/lib/src/generated/client/index.d.ts +2086 -379
  123. package/lib/src/generated/client/index.js +18 -3
  124. package/lib/src/generated/client/package.json +1 -1
  125. package/lib/src/generated/client/schema.prisma +38 -7
  126. package/lib/src/generated/client/wasm.js +15 -0
  127. package/lib/src/interceptors/CommandInterceptor.js +271 -188
  128. package/lib/src/interceptors/xenonScripts.js +55 -0
  129. package/lib/src/interfaces/IPluginArgs.js +1 -2
  130. package/lib/src/middleware/authMiddleware.js +15 -3
  131. package/lib/src/middleware/commandCaller.js +3 -3
  132. package/lib/src/middleware/verifyCredential.js +58 -1
  133. package/lib/src/plugin.js +23 -4
  134. package/lib/src/prisma.js +1 -1
  135. package/lib/src/scripts/database-check.js +124 -0
  136. package/lib/src/services/AICommandService.js +9 -2
  137. package/lib/src/services/AIService.js +281 -32
  138. package/lib/src/services/CleanupService.js +6 -1
  139. package/lib/src/services/EmailService.js +29 -0
  140. package/lib/src/services/InterceptorService.js +70 -13
  141. package/lib/src/services/NetworkConditioningService.js +85 -88
  142. package/lib/src/services/NotificationService.js +36 -29
  143. package/lib/src/services/OrphanSweeper.js +7 -0
  144. package/lib/src/services/PortAllocator.js +5 -0
  145. package/lib/src/services/SelectorVerificationJob.js +15 -4
  146. package/lib/src/services/ServerManager.js +105 -9
  147. package/lib/src/services/SessionLifecycleService.js +133 -75
  148. package/lib/src/services/ShutdownCoordinator.js +33 -0
  149. package/lib/src/services/SocketServer.js +19 -3
  150. package/lib/src/services/TeamService.js +20 -3
  151. package/lib/src/services/bug-report/video-slice.js +4 -1
  152. package/lib/src/services/device-access/leaseOverride.js +7 -5
  153. package/lib/src/services/healing/FuzzyXmlHealingProvider.js +51 -0
  154. package/lib/src/services/healing/HealEtalonService.js +3 -2
  155. package/lib/src/services/healing/HealingOrchestrator.js +85 -71
  156. package/lib/src/services/healing/OcrHealingProvider.js +41 -64
  157. package/lib/src/services/healing/ResilioTreeHealingProvider.js +41 -50
  158. package/lib/src/services/healing/VisualAiHealingProvider.js +1 -1
  159. package/lib/src/services/healing/fingerprintIdentity.js +126 -0
  160. package/lib/src/services/healing/resilioPath.js +274 -0
  161. package/lib/src/services/interceptor/AndroidProxyAdapter.js +18 -0
  162. package/lib/src/services/interceptor/interceptorOptions.js +44 -0
  163. package/lib/src/services/logcat/SessionDeviceLogs.js +271 -0
  164. package/lib/src/services/logcat/deviceLogBook.js +163 -0
  165. package/lib/src/services/network/AndroidRadios.js +71 -0
  166. package/lib/src/services/network/PhoneNetworkLedger.js +184 -0
  167. package/lib/src/services/network/PhoneNetworkRestore.js +330 -0
  168. package/lib/src/services/network/adbForPhone.js +60 -0
  169. package/lib/src/services/network/phoneNetworkLock.js +20 -0
  170. package/lib/src/services/network/xenonProxy.js +107 -0
  171. package/lib/src/services/ocr/ocrData.js +118 -0
  172. package/lib/src/services/ocr/vendor/LICENSE-tessdata.txt +202 -0
  173. package/lib/src/services/ocr/vendor/eng.traineddata.gz +0 -0
  174. package/lib/src/services/omni-vision/OmniVisionService.js +132 -95
  175. package/lib/src/services/omni-vision/VisionAssertionService.js +28 -25
  176. package/lib/src/services/omni-vision/ocrTextMatch.js +105 -0
  177. package/lib/src/services/omni-vision/screenScale.js +68 -0
  178. package/lib/src/services/passwordResetLink.js +50 -9
  179. package/lib/src/services/passwordResetMode.js +3 -1
  180. package/lib/src/services/publicUrl.js +45 -0
  181. package/lib/src/services/recording/concurrency-gate.js +12 -3
  182. package/lib/src/services/session/sessionIdentity.js +5 -3
  183. package/lib/src/services/session/shutdownReason.js +26 -0
  184. package/lib/src/services/settings/SelfHealingSwitch.js +70 -0
  185. package/lib/src/services/settings/aiEngineSettings.js +219 -0
  186. package/lib/src/services/settings/labSettings.js +149 -0
  187. package/lib/src/services/token/mintedLifetime.js +51 -0
  188. package/lib/src/services/webhookEvents.js +173 -0
  189. package/lib/src/sessions/SessionManager.js +8 -2
  190. package/lib/src/sessions/sessionMemory.js +25 -0
  191. package/lib/test/helpers/fake-adb.js +58 -0
  192. package/lib/test/helpers/fake-ai-provider.js +188 -0
  193. package/lib/test/helpers/fake-device-table.js +63 -24
  194. package/lib/test/helpers/loki-stores.js +33 -0
  195. package/lib/test/helpers/page-sources.js +164 -0
  196. package/lib/test/helpers/scratch-database.js +58 -13
  197. package/lib/test/integration/audit-ingest.spec.js +10 -10
  198. package/lib/test/integration/auth-flow.spec.js +9 -14
  199. package/lib/test/integration/auth-rate-limit.spec.js +6 -3
  200. package/lib/test/integration/bug-report-route.spec.js +22 -2
  201. package/lib/test/integration/forgot-password-rate-limit.spec.js +6 -3
  202. package/lib/test/integration/forgot-password.spec.js +17 -18
  203. package/lib/test/integration/legacy-deprecation-headers.spec.js +5 -2
  204. package/lib/test/integration/migration.spec.js +6 -1
  205. package/lib/test/integration/node-pair-auth-rest.spec.js +5 -9
  206. package/lib/test/integration/ports-router.spec.js +4 -4
  207. package/lib/test/integration/prisma-device-store-add-devices.spec.js +2 -3
  208. package/lib/test/integration/prisma-device-store-team-filter.spec.js +2 -5
  209. package/lib/test/integration/profile-tokens.spec.js +10 -16
  210. package/lib/test/integration/recordings-router.spec.js +16 -16
  211. package/lib/test/integration/reset-revokes-sessions.spec.js +4 -9
  212. package/lib/test/integration/role-matrix.spec.js +4 -9
  213. package/lib/test/integration/sdk-leases-router.spec.js +9 -9
  214. package/lib/test/integration/sdk-version.spec.js +2 -2
  215. package/lib/test/integration/selector-health-endpoint.spec.js +5 -5
  216. package/lib/test/integration/team-membership-migration.spec.js +73 -12
  217. package/lib/test/integration/team-visibility-apps.spec.js +15 -22
  218. package/lib/test/integration/team-visibility-control.spec.js +96 -69
  219. package/lib/test/integration/team-visibility-dashboard.spec.js +7 -23
  220. package/lib/test/integration/team-visibility-grid.spec.js +16 -15
  221. package/lib/test/integration/team-visibility-leases.spec.js +8 -11
  222. package/lib/test/integration/team-visibility-recordings.spec.js +6 -19
  223. package/lib/test/integration/team-visibility-sessions.spec.js +6 -19
  224. package/lib/test/integration/users-flow.spec.js +7 -12
  225. package/lib/test/integration/users-lockout.spec.js +5 -3
  226. package/lib/test/unit/EmailService.test.js +66 -0
  227. package/lib/test/unit/ResilioTreeHealingProvider.test.js +46 -19
  228. package/lib/test/unit/TeamService.test.js +3 -0
  229. package/lib/test/unit/XenonCapabilityManager.spec.js +4 -2
  230. package/lib/test/unit/ai-call-time-limit.spec.js +130 -0
  231. package/lib/test/unit/ai-engine-settings.spec.js +338 -0
  232. package/lib/test/unit/ai-provider-switch.spec.js +130 -0
  233. package/lib/test/unit/ai-rate-limit.spec.js +126 -0
  234. package/lib/test/unit/android-h264-viewers.spec.js +272 -0
  235. package/lib/test/unit/android-preview-hold.spec.js +3 -2
  236. package/lib/test/unit/apikeys-router.spec.js +84 -0
  237. package/lib/test/unit/apple-family.spec.js +36 -0
  238. package/lib/test/unit/authMiddleware.bearer.spec.js +9 -0
  239. package/lib/test/unit/authMiddleware.credentialLifetime.spec.js +149 -0
  240. package/lib/test/unit/bug-report/video-slice.spec.js +19 -0
  241. package/lib/test/unit/cleanup-settings-db.spec.js +103 -0
  242. package/lib/test/unit/cleanup-settings.spec.js +244 -0
  243. package/lib/test/unit/dashboard-api-paths.spec.js +101 -0
  244. package/lib/test/unit/dashboard-commands.spec.js +249 -0
  245. package/lib/test/unit/dashboard-events-device-logs.spec.js +207 -0
  246. package/lib/test/unit/dashboard-events-team-scope.spec.js +9 -6
  247. package/lib/test/unit/dashboard-plugin-link.spec.js +213 -0
  248. package/lib/test/unit/database-check.spec.js +108 -0
  249. package/lib/test/unit/default-plugin-args.spec.js +33 -0
  250. package/lib/test/unit/device-log-book.spec.js +180 -0
  251. package/lib/test/unit/device-offline-webhook.spec.js +81 -0
  252. package/lib/test/unit/device-setting-columns.spec.js +66 -0
  253. package/lib/test/unit/device-settings-loki.spec.js +87 -0
  254. package/lib/test/unit/device-settings-survive.spec.js +555 -0
  255. package/lib/test/unit/device-sync-lost-update.spec.js +5 -0
  256. package/lib/test/unit/emulator-boot.spec.js +91 -0
  257. package/lib/test/unit/enable-dashboard-option.spec.js +57 -0
  258. package/lib/test/unit/event-log-session-data.spec.js +244 -0
  259. package/lib/test/unit/failure-analysis.spec.js +182 -0
  260. package/lib/test/unit/failure-categories.spec.js +128 -0
  261. package/lib/test/unit/fingerprint-identity.spec.js +433 -0
  262. package/lib/test/unit/h264-multiplexer.spec.js +46 -0
  263. package/lib/test/unit/heal-report.spec.js +71 -0
  264. package/lib/test/unit/healing-instrumentation.spec.js +6 -1
  265. package/lib/test/unit/healing-tier-gate.spec.js +73 -29
  266. package/lib/test/unit/healing-tiers-every-session.spec.js +219 -0
  267. package/lib/test/unit/hub-node-gateway-appium-server.spec.js +219 -0
  268. package/lib/test/unit/hub-node-same-host.spec.js +5 -0
  269. package/lib/test/unit/interceptor-visual-elements.spec.js +192 -0
  270. package/lib/test/unit/iphone-ipad-only.spec.js +132 -0
  271. package/lib/test/unit/json-logging-option.spec.js +74 -0
  272. package/lib/test/unit/lab-settings.spec.js +218 -0
  273. package/lib/test/unit/lease/lease-override.spec.js +23 -6
  274. package/lib/test/unit/lease/session-lease-proof.spec.js +16 -4
  275. package/lib/test/unit/max-sessions.spec.js +240 -0
  276. package/lib/test/unit/ocr-healing-tier-real-ocr.spec.js +113 -0
  277. package/lib/test/unit/ocr-healing-tier.spec.js +122 -0
  278. package/lib/test/unit/ocr-offline.spec.js +213 -0
  279. package/lib/test/unit/ocr-text-match.spec.js +44 -0
  280. package/lib/test/unit/omni-virtual-element-store.spec.js +301 -0
  281. package/lib/test/unit/omni-vision-analyze-screen.spec.js +72 -0
  282. package/lib/test/unit/omni-vision-ios-points.spec.js +159 -0
  283. package/lib/test/unit/omni-vision-tap-text.spec.js +129 -0
  284. package/lib/test/unit/omni-vision.spec.js +6 -8
  285. package/lib/test/unit/package-engines.spec.js +27 -0
  286. package/lib/test/unit/partial-config-file.spec.js +14 -0
  287. package/lib/test/unit/passwordResetMode.test.js +10 -2
  288. package/lib/test/unit/phone-network/boot-clean-up.spec.js +64 -0
  289. package/lib/test/unit/phone-network/interceptor-options.spec.js +77 -0
  290. package/lib/test/unit/phone-network/interceptor-proxy-restore.spec.js +145 -0
  291. package/lib/test/unit/phone-network/network-conditioning.spec.js +145 -0
  292. package/lib/test/unit/phone-network/phone-network-restore.spec.js +246 -0
  293. package/lib/test/unit/phone-network/session-end-paths.spec.js +182 -0
  294. package/lib/test/unit/phone-network/session-start.spec.js +110 -0
  295. package/lib/test/unit/phone-network/xenon-proxy.spec.js +69 -0
  296. package/lib/test/unit/plugin.spec.js +3 -3
  297. package/lib/test/unit/profile-router.test.js +122 -1
  298. package/lib/test/unit/queue-requester.spec.js +1 -1
  299. package/lib/test/unit/recording-options.spec.js +181 -0
  300. package/lib/test/unit/reset-link-address.spec.js +266 -0
  301. package/lib/test/unit/reset-link.test.js +10 -4
  302. package/lib/test/unit/resilio-path-healing.spec.js +270 -0
  303. package/lib/test/unit/selector-learning-every-session.spec.js +179 -0
  304. package/lib/test/unit/selector-verification-clean-build.spec.js +130 -0
  305. package/lib/test/unit/selector-verification-job.spec.js +13 -13
  306. package/lib/test/unit/self-healing-boot.spec.js +80 -0
  307. package/lib/test/unit/self-healing-setting.spec.js +273 -0
  308. package/lib/test/unit/session-app-resolution.spec.js +5 -1
  309. package/lib/test/unit/session-attribution-wiring.spec.js +41 -13
  310. package/lib/test/unit/session-create-credential-owner.spec.js +242 -0
  311. package/lib/test/unit/session-credential-strip.spec.js +1 -1
  312. package/lib/test/unit/session-device-logs.spec.js +347 -0
  313. package/lib/test/unit/session-end-failure-analysis.spec.js +133 -0
  314. package/lib/test/unit/session-failed-webhook.spec.js +170 -0
  315. package/lib/test/unit/session-log-indexes.spec.js +64 -3
  316. package/lib/test/unit/shell-commands.spec.js +133 -0
  317. package/lib/test/unit/socketServer.scopedEmit.spec.js +2 -0
  318. package/lib/test/unit/socketServer.sessionCookie.spec.js +2 -1
  319. package/lib/test/unit/stream-start-pipelines.spec.js +264 -0
  320. package/lib/test/unit/stream-status-type.spec.js +4 -0
  321. package/lib/test/unit/token-mint-limits.spec.js +165 -0
  322. package/lib/test/unit/unexpected-shutdown-reason.spec.js +163 -0
  323. package/lib/test/unit/verify-credential.spec.js +33 -0
  324. package/lib/test/unit/vision-assertion.spec.js +119 -0
  325. package/lib/test/unit/wda-client-shell.spec.js +133 -0
  326. package/lib/test/unit/web-config-service.spec.js +98 -0
  327. package/lib/test/unit/webhook-delivery.spec.js +24 -0
  328. package/lib/test/unit/webhook-payloads.spec.js +273 -0
  329. package/lib/test/unit/xenon-execute-scripts.spec.js +138 -0
  330. package/lib/tsconfig.tsbuildinfo +1 -1
  331. package/lib/web/src/components/webhook-settings/webhookEventVariables.json +30 -0
  332. package/package.json +5 -5
  333. package/prisma/migrations/20261004140000_keep_device_settings/migration.sql +19 -0
  334. package/prisma/migrations/20261004174413_locator_etalon_path/migration.sql +2 -0
  335. package/prisma/schema.prisma +38 -7
  336. package/schema.json +23 -25
  337. package/scripts/generate-types-from-schema.js +1 -2
  338. package/scripts/lib/always-present-plugin-args.js +4 -1
  339. package/lib/public/assets/BugReportButton-BoGhlI7F.js +0 -15
  340. package/lib/public/assets/DeviceMosaicView-8J0oYqMM.js +0 -2
  341. package/lib/public/assets/ai-settings-Bce5SJob.js +0 -21
  342. package/lib/public/assets/device-explorer-C-KnhMHI.js +0 -279
  343. package/lib/public/assets/forgot-password-DnudqFFo.js +0 -1
  344. package/lib/public/assets/maintenance-settings-zDVlOMVK.js +0 -6
  345. package/lib/public/assets/recording-group-store-WVM3hoXn.js +0 -26
  346. package/lib/public/assets/runbook-page-DJTVCJX7.js +0 -124
  347. package/lib/public/assets/session-detail-page-CGn55Pvl.js +0 -4
  348. package/lib/public/assets/settings-MsVU2ukE.js +0 -6
  349. package/lib/public/assets/webhook-settings-DiS8geIm.js +0 -1
  350. /package/lib/public/assets/{BugReportButton-CPBjfiv4.css → index-CPBjfiv4.css} +0 -0
@@ -0,0 +1,51 @@
1
+ "use strict";
2
+ /**
3
+ * Nothing a credential mints outlives it.
4
+ *
5
+ * `credentialExpiresAt` is req.auth.credentialExpiresAt: when the credential
6
+ * making the request stops working (a Bearer token's `exp`, an API key's
7
+ * `expiresAt`), as epoch ms, or undefined for one that doesn't expire (a key
8
+ * without an expiry, a dashboard sign-in, which renews itself).
9
+ *
10
+ * Through 2.14 a 1-hour Bearer token could make itself an API key that never
11
+ * expired (POST /profile/tokens, POST /apikeys), or mint a fresh 1-hour token
12
+ * before it expired, and so on for ever (POST /auth/token).
13
+ */
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.mintedLifetimeSec = mintedLifetimeSec;
16
+ exports.keyExpiryWithin = keyExpiryWithin;
17
+ /**
18
+ * A minted token's lifetime in seconds: `defaultSec`, cut to what is left of
19
+ * the credential minting it, less a second so the minted token's `exp`
20
+ * can't round past it. Throws when nothing is left.
21
+ */
22
+ function mintedLifetimeSec(defaultSec, credentialExpiresAt) {
23
+ if (credentialExpiresAt === undefined)
24
+ return defaultSec;
25
+ const left = Math.floor((credentialExpiresAt - Date.now()) / 1000) - 1;
26
+ if (left < 1)
27
+ throw new Error('the credential used to mint this token has expired');
28
+ return Math.min(defaultSec, left);
29
+ }
30
+ /**
31
+ * An API key's expiry, made with a credential that may itself expire: the
32
+ * expiry asked for, or the credential's when none was asked for; an error
33
+ * when the one asked for is later than the credential's, or the credential
34
+ * has already expired.
35
+ */
36
+ function keyExpiryWithin(requested, credentialExpiresAt) {
37
+ if (credentialExpiresAt === undefined)
38
+ return { expiresAt: requested };
39
+ // Token verification allows 60 s of clock skew, so a request can arrive on
40
+ // a credential just past its exp: it must not make a key already expired.
41
+ if (credentialExpiresAt <= Date.now()) {
42
+ return { error: 'the credential you are using has expired' };
43
+ }
44
+ if (requested && requested.getTime() > credentialExpiresAt) {
45
+ return {
46
+ error: 'expiresAt can be no later than the credential you are using, which expires at ' +
47
+ new Date(credentialExpiresAt).toISOString(),
48
+ };
49
+ }
50
+ return { expiresAt: requested !== null && requested !== void 0 ? requested : new Date(credentialExpiresAt) };
51
+ }
@@ -0,0 +1,173 @@
1
+ "use strict";
2
+ /**
3
+ * What each webhook event carries.
4
+ *
5
+ * One shape per event, documented here and nowhere else. A webhook turns it
6
+ * into a message in three ways, and all three read these names:
7
+ *
8
+ * - the built-in Slack message (`type: slack`, `NotificationService`);
9
+ * - the generic JSON body, `{ "event": "<event>", "payload": { ... } }`;
10
+ * - a custom template, where `{{name}}` stands for a name below, `{{eventType}}`
11
+ * for the event itself, and `{{a.b}}` or `{{list.0.b}}` reaches inside.
12
+ *
13
+ * The dashboard offers the names below as template chips
14
+ * (`web/src/components/webhook-settings/webhookEvents.ts`; a spec keeps the two
15
+ * lists equal). A device event also carries the device's other fields, which
16
+ * may change: only the names listed are promised.
17
+ */
18
+ Object.defineProperty(exports, "__esModule", { value: true });
19
+ exports.EVENT_TYPES = exports.WEBHOOK_EVENTS = void 0;
20
+ exports.isEventType = isEventType;
21
+ exports.sessionFailedPayload = sessionFailedPayload;
22
+ exports.renderTemplate = renderTemplate;
23
+ const DEVICE_VARIABLES = [
24
+ { name: 'udid', description: "The device's id" },
25
+ { name: 'name', description: "The device's name" },
26
+ { name: 'host', description: 'The machine it is plugged into' },
27
+ { name: 'platform', description: 'android or ios' },
28
+ ];
29
+ const sampleDevice = () => ({
30
+ udid: 'test-device-udid',
31
+ name: 'Test Device',
32
+ host: '127.0.0.1',
33
+ platform: 'android',
34
+ });
35
+ exports.WEBHOOK_EVENTS = {
36
+ device_offline: {
37
+ when: 'A device is no longer reported by its machine (unplugged, or its machine stopped).',
38
+ variables: DEVICE_VARIABLES,
39
+ sample: sampleDevice,
40
+ },
41
+ device_new: {
42
+ when: 'A device is detected for the first time.',
43
+ variables: DEVICE_VARIABLES,
44
+ sample: sampleDevice,
45
+ },
46
+ session_failed: {
47
+ when: 'A session ends as failed, once per session: the test marked it failed, a command failed, ' +
48
+ 'it timed out for inactivity, its driver crashed, or its heartbeat stopped. Not when the ' +
49
+ 'server itself shuts down, and not for a session that was already failed when the server ' +
50
+ 'started.',
51
+ variables: [
52
+ { name: 'sessionId', description: "The session's id" },
53
+ { name: 'sessionName', description: "The session's name, if the test gave it one" },
54
+ { name: 'failureReason', description: 'Why it failed' },
55
+ { name: 'udid', description: "The device's id" },
56
+ { name: 'deviceName', description: "The device's name" },
57
+ { name: 'platform', description: 'android or ios' },
58
+ { name: 'osVersion', description: "The device's OS version" },
59
+ { name: 'startTime', description: 'When it started (ISO 8601)' },
60
+ { name: 'endTime', description: 'When it ended (ISO 8601)' },
61
+ ],
62
+ sample: () => ({
63
+ sessionId: 'test-session-id',
64
+ sessionName: 'Checkout flow',
65
+ failureReason: 'Test message: element not found',
66
+ udid: 'test-device-udid',
67
+ deviceName: 'Test Device',
68
+ platform: 'android',
69
+ osVersion: '14',
70
+ startTime: new Date(Date.now() - 150000).toISOString(),
71
+ endTime: new Date().toISOString(),
72
+ }),
73
+ },
74
+ selector_health_digest: {
75
+ when: 'The Selector Health digest is sent. `hotspots` lists the most healed selectors, each ' +
76
+ 'with `healCount`, `originalSelector` and, when there is one, `suggestedRewrite`; reach ' +
77
+ 'into it with `{{hotspots.0.originalSelector}}`.',
78
+ variables: [
79
+ { name: 'windowDays', description: 'How many days the digest covers' },
80
+ { name: 'totalHeals', description: 'Heals in that time' },
81
+ { name: 'distinctSelectors', description: 'Selectors that were healed' },
82
+ ],
83
+ sample: () => ({
84
+ windowDays: 30,
85
+ totalHeals: 12,
86
+ distinctSelectors: 3,
87
+ hotspots: [
88
+ {
89
+ healCount: 7,
90
+ originalSelector: "//*[@text='Log in']",
91
+ suggestedRewrite: '~login-button',
92
+ },
93
+ ],
94
+ }),
95
+ },
96
+ };
97
+ exports.EVENT_TYPES = Object.keys(exports.WEBHOOK_EVENTS);
98
+ function isEventType(value) {
99
+ return typeof value === 'string' && exports.EVENT_TYPES.includes(value);
100
+ }
101
+ /**
102
+ * `session_failed`'s payload. Not the row: a row has the session's
103
+ * capabilities, the keys that created it and its AI analysis, and its names
104
+ * are the database's (`failure_reason`). Unknown values are empty text, never
105
+ * missing, so a template never shows "undefined" or "null".
106
+ */
107
+ function sessionFailedPayload(session) {
108
+ var _a, _b, _c;
109
+ const iso = (value) => value ? new Date(value).toISOString() : '';
110
+ return {
111
+ sessionId: session.id,
112
+ sessionName: (_a = session.name) !== null && _a !== void 0 ? _a : '',
113
+ failureReason: (_b = session.failure_reason) !== null && _b !== void 0 ? _b : '',
114
+ udid: session.device_udid,
115
+ deviceName: (_c = session.device_name) !== null && _c !== void 0 ? _c : '',
116
+ platform: session.device_platform,
117
+ osVersion: session.device_version,
118
+ startTime: iso(session.startTime),
119
+ endTime: iso(session.endTime),
120
+ };
121
+ }
122
+ function lookup(data, path) {
123
+ let value = data;
124
+ for (const key of path.trim().split('.')) {
125
+ value = value === null || value === undefined ? undefined : value[key];
126
+ }
127
+ return value;
128
+ }
129
+ const asText = (value) => typeof value === 'object' && value !== null ? JSON.stringify(value) : String(value);
130
+ /** `{{name}}` filled in from `data` as text; a name `data` doesn't have stays as written. */
131
+ function fill(text, data) {
132
+ return text.replace(/\{\{([^}]+)\}\}/g, (match, path) => {
133
+ const value = lookup(data, path);
134
+ return value === undefined ? match : asText(value);
135
+ });
136
+ }
137
+ function fillStrings(node, data) {
138
+ if (typeof node === 'string')
139
+ return fill(node, data);
140
+ if (Array.isArray(node))
141
+ return node.map((item) => fillStrings(item, data));
142
+ if (node && typeof node === 'object') {
143
+ return Object.fromEntries(Object.entries(node).map(([key, value]) => [key, fillStrings(value, data)]));
144
+ }
145
+ return node;
146
+ }
147
+ /**
148
+ * The body a custom template sends for an event.
149
+ *
150
+ * A template that is JSON as written is filled in string by string, so a
151
+ * value with a quote, a backslash or a line break (an error message, say)
152
+ * cannot break it. Any other template, such as `{"n": {{totalHeals}}}` or
153
+ * plain text, is filled in as text, then sent as JSON if the result parses,
154
+ * and as `{ "text": "<result>" }` if not.
155
+ */
156
+ function renderTemplate(template, data) {
157
+ let written;
158
+ try {
159
+ written = JSON.parse(template);
160
+ }
161
+ catch (_a) {
162
+ written = undefined;
163
+ }
164
+ if (written !== null && typeof written === 'object')
165
+ return fillStrings(written, data);
166
+ const text = fill(template, data);
167
+ try {
168
+ return JSON.parse(text);
169
+ }
170
+ catch (_b) {
171
+ return { text };
172
+ }
173
+ }
@@ -51,7 +51,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
51
51
  return (mod && mod.__esModule) ? mod : { "default": mod };
52
52
  };
53
53
  Object.defineProperty(exports, "__esModule", { value: true });
54
- exports.SESSION_MANAGER = exports.SessionManager = void 0;
54
+ exports.SESSION_MANAGER = exports.SessionManager = exports.HUB_RESTART_CATEGORY = void 0;
55
55
  const os_1 = __importDefault(require("os"));
56
56
  const typedi_1 = require("typedi");
57
57
  const prisma_1 = require("../prisma");
@@ -62,6 +62,12 @@ const device_store_1 = require("../data-service/device-store");
62
62
  const nodeWebDriverUrl_1 = require("../gateway/nodeWebDriverUrl");
63
63
  const XenonCapabilityManager_1 = require("../XenonCapabilityManager");
64
64
  const nodeMetrics_1 = require("../services/metrics/nodeMetrics");
65
+ /**
66
+ * The `failure_category` of a session a restart ended: one still running at
67
+ * boot that couldn't be picked up again. The failure analysis writes every
68
+ * other category (`ANALYSIS_CATEGORIES`).
69
+ */
70
+ exports.HUB_RESTART_CATEGORY = 'HUB_RESTART';
65
71
  /**
66
72
  * SessionManager with persistence and recovery capabilities.
67
73
  *
@@ -306,7 +312,7 @@ let SessionManager = class SessionManager {
306
312
  status: 'failed',
307
313
  endTime: new Date(),
308
314
  failure_reason: reason,
309
- failure_category: 'HUB_RESTART',
315
+ failure_category: exports.HUB_RESTART_CATEGORY,
310
316
  },
311
317
  });
312
318
  }
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.forgetSessionMemory = forgetSessionMemory;
4
+ const typedi_1 = require("typedi");
5
+ const OmniVisionService_1 = require("../services/omni-vision/OmniVisionService");
6
+ const AutowaitService_1 = require("../services/autowait/AutowaitService");
7
+ /**
8
+ * Drop what this server keeps in memory for a session's commands: the
9
+ * elements its finds and heals found in screenshots (OmniVisionService) and
10
+ * its autowait settings (AutowaitService). Called however the session ends
11
+ * here: the test's delete (SessionLifecycleService.deleteSession), Appium's
12
+ * new-command timeout or a crash (XenonPlugin.onUnexpectedShutdown), the idle
13
+ * release (releaseBlockedDevices), a stale heartbeat (OrphanSweeper) and a
14
+ * shutdown's drain (stopSessionForShutdown). Safe to call more than once.
15
+ *
16
+ * Through 2.14 the elements were never dropped, and the autowait settings
17
+ * only by the interceptor on `deleteSession`, a command Appium never hands
18
+ * it: XenonPlugin answers `deleteSession` itself.
19
+ */
20
+ function forgetSessionMemory(sessionId) {
21
+ if (!sessionId)
22
+ return;
23
+ typedi_1.Container.get(OmniVisionService_1.OmniVisionService).forgetSession(sessionId);
24
+ typedi_1.Container.get(AutowaitService_1.AutowaitService).clearSession(sessionId);
25
+ }
@@ -0,0 +1,58 @@
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
+ exports.fakeAdb = fakeAdb;
16
+ exports.useFakeAdb = useFakeAdb;
17
+ const typedi_1 = require("typedi");
18
+ const AndroidDeviceManager_1 = __importDefault(require("../../src/device-managers/AndroidDeviceManager"));
19
+ const container_registration_1 = require("./container-registration");
20
+ function fakeAdb(answers = {}) {
21
+ const fake = {
22
+ calls: [],
23
+ answers,
24
+ commands(udid) {
25
+ return fake.calls
26
+ .filter((args) => udid === undefined || args[1] === udid)
27
+ .map((args) => args.slice(2).join(' '));
28
+ },
29
+ };
30
+ return fake;
31
+ }
32
+ /**
33
+ * Registers `fake` as every phone's adb for the enclosing `describe`. Returns
34
+ * nothing: the registration is put back after each test.
35
+ */
36
+ function useFakeAdb(get) {
37
+ let restore;
38
+ beforeEach(() => {
39
+ restore = (0, container_registration_1.saveRegistrations)(AndroidDeviceManager_1.default);
40
+ typedi_1.Container.set(AndroidDeviceManager_1.default, {
41
+ getAdbForDevice: (udid) => __awaiter(this, void 0, void 0, function* () {
42
+ return ({
43
+ adbExec: (args) => __awaiter(this, void 0, void 0, function* () {
44
+ var _a;
45
+ const fake = get();
46
+ fake.calls.push(args);
47
+ const key = args.slice(args[0] === '-s' ? 2 : 0).join(' ');
48
+ const answer = (_a = fake.answers[`${udid} ${key}`]) !== null && _a !== void 0 ? _a : fake.answers[key];
49
+ if (answer instanceof Error)
50
+ throw answer;
51
+ return answer !== null && answer !== void 0 ? answer : '';
52
+ }),
53
+ });
54
+ }),
55
+ });
56
+ });
57
+ afterEach(() => restore());
58
+ }
@@ -0,0 +1,188 @@
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
+ exports.OLLAMA_URL = exports.PROVIDERS = void 0;
16
+ exports.useFakeAiProviders = useFakeAiProviders;
17
+ const axios_1 = __importDefault(require("axios"));
18
+ const sinon_1 = __importDefault(require("sinon"));
19
+ const config_1 = require("../../src/config");
20
+ exports.PROVIDERS = ['gemini', 'openai', 'anthropic', 'ollama'];
21
+ const AI_SETTINGS = [
22
+ 'aiProvider',
23
+ 'aiModel',
24
+ 'aiBaseUrl',
25
+ 'geminiApiKey',
26
+ 'openaiApiKey',
27
+ 'anthropicApiKey',
28
+ 'geminiModel',
29
+ 'openaiModel',
30
+ 'anthropicModel',
31
+ 'ollamaModel',
32
+ ];
33
+ exports.OLLAMA_URL = 'http://ollama.test:11434';
34
+ function json(status, body) {
35
+ return new Response(JSON.stringify(body), {
36
+ status,
37
+ // The OpenAI and Anthropic SDKs retry a 429 after a pause unless told not to.
38
+ headers: { 'content-type': 'application/json', 'x-should-retry': 'false' },
39
+ });
40
+ }
41
+ function okBody(provider, text) {
42
+ switch (provider) {
43
+ case 'gemini':
44
+ return {
45
+ candidates: [
46
+ { content: { role: 'model', parts: [{ text }] }, finishReason: 'STOP', index: 0 },
47
+ ],
48
+ };
49
+ case 'openai':
50
+ return {
51
+ id: 'chatcmpl-test',
52
+ object: 'chat.completion',
53
+ created: 0,
54
+ model: 'test',
55
+ choices: [
56
+ { index: 0, message: { role: 'assistant', content: text }, finish_reason: 'stop' },
57
+ ],
58
+ };
59
+ case 'anthropic':
60
+ return {
61
+ id: 'msg_test',
62
+ type: 'message',
63
+ role: 'assistant',
64
+ model: 'test',
65
+ content: [{ type: 'text', text }],
66
+ stop_reason: 'end_turn',
67
+ usage: { input_tokens: 1, output_tokens: 1 },
68
+ };
69
+ default:
70
+ return { response: text };
71
+ }
72
+ }
73
+ function errorBody(provider, status, said) {
74
+ const message = said !== null && said !== void 0 ? said : (status === 429 ? 'Resource has been exhausted (e.g. check quota).' : `HTTP ${status}`);
75
+ switch (provider) {
76
+ case 'gemini':
77
+ return { error: { code: status, message, status: 'RESOURCE_EXHAUSTED' } };
78
+ case 'anthropic':
79
+ return { type: 'error', error: { type: 'rate_limit_error', message } };
80
+ default:
81
+ return { error: { message, type: 'requests', code: 'rate_limit_exceeded' } };
82
+ }
83
+ }
84
+ function aborted() {
85
+ const err = new Error('This operation was aborted');
86
+ err.name = 'AbortError';
87
+ return err;
88
+ }
89
+ /** Resolves never, and rejects as fetch does when the request's signal aborts. */
90
+ function hangUntilAborted(signal) {
91
+ return new Promise((_, reject) => {
92
+ if (signal === null || signal === void 0 ? void 0 : signal.aborted)
93
+ return reject(aborted());
94
+ signal === null || signal === void 0 ? void 0 : signal.addEventListener('abort', () => reject(aborted()));
95
+ });
96
+ }
97
+ function useFakeAiProviders() {
98
+ let saved = {};
99
+ let savedMockEnv;
100
+ let provider = 'gemini';
101
+ let current = { status: 500 };
102
+ let ollamaUp = true;
103
+ const calls = [];
104
+ const sandbox = sinon_1.default.createSandbox();
105
+ beforeEach(() => {
106
+ saved = Object.fromEntries(AI_SETTINGS.map((k) => [k, config_1.config[k]]));
107
+ savedMockEnv = process.env.GEMINI_API_KEY;
108
+ // 'mock' would put the service in its built-in mock mode.
109
+ if (process.env.GEMINI_API_KEY === 'mock')
110
+ delete process.env.GEMINI_API_KEY;
111
+ for (const k of AI_SETTINGS)
112
+ config_1.config[k] = undefined;
113
+ config_1.config.aiProvider = 'gemini';
114
+ calls.length = 0;
115
+ current = { status: 500 };
116
+ ollamaUp = true;
117
+ sandbox.stub(globalThis, 'fetch').callsFake(((_url, init) => __awaiter(this, void 0, void 0, function* () {
118
+ var _a, _b;
119
+ const signal = (_a = init === null || init === void 0 ? void 0 : init.signal) !== null && _a !== void 0 ? _a : undefined;
120
+ calls.push({ provider, body: String((_b = init === null || init === void 0 ? void 0 : init.body) !== null && _b !== void 0 ? _b : ''), signal });
121
+ if (current === 'hang')
122
+ return hangUntilAborted(signal);
123
+ if ('text' in current)
124
+ return json(200, okBody(provider, current.text));
125
+ return json(current.status, errorBody(provider, current.status, current.message));
126
+ })));
127
+ // Ollama: its health check answers, then the generate call as told.
128
+ sandbox.stub(axios_1.default, 'get').callsFake(((url) => __awaiter(this, void 0, void 0, function* () {
129
+ if (url.startsWith(exports.OLLAMA_URL)) {
130
+ if (ollamaUp)
131
+ return { status: 200, data: { models: [] } };
132
+ throw Object.assign(new Error(`connect ECONNREFUSED ${exports.OLLAMA_URL}`), {
133
+ code: 'ECONNREFUSED',
134
+ });
135
+ }
136
+ throw new Error(`no network in tests: GET ${url}`);
137
+ })));
138
+ sandbox.stub(axios_1.default, 'post').callsFake(((url, body, cfg) => __awaiter(this, void 0, void 0, function* () {
139
+ if (!url.startsWith(exports.OLLAMA_URL))
140
+ throw new Error(`no network in tests: POST ${url}`);
141
+ const signal = cfg === null || cfg === void 0 ? void 0 : cfg.signal;
142
+ calls.push({ provider: 'ollama', body: JSON.stringify(body), signal });
143
+ if (current === 'hang')
144
+ return hangUntilAborted(signal);
145
+ if ('text' in current)
146
+ return { status: 200, data: okBody('ollama', current.text) };
147
+ const err = new Error(`Request failed with status code ${current.status}`);
148
+ err.response = {
149
+ status: current.status,
150
+ data: errorBody('ollama', current.status, current.message),
151
+ };
152
+ throw err;
153
+ })));
154
+ });
155
+ afterEach(() => {
156
+ sandbox.restore();
157
+ for (const k of AI_SETTINGS)
158
+ config_1.config[k] = saved[k];
159
+ if (savedMockEnv === undefined)
160
+ delete process.env.GEMINI_API_KEY;
161
+ else
162
+ process.env.GEMINI_API_KEY = savedMockEnv;
163
+ });
164
+ return {
165
+ calls,
166
+ use(name, opts = {}) {
167
+ provider = name;
168
+ const model = `test-${name}-${Math.random().toString(36).slice(2)}`;
169
+ const key = opts.key === false ? undefined : 'test-key';
170
+ config_1.config.aiProvider = name;
171
+ if (name === 'gemini')
172
+ Object.assign(config_1.config, { geminiApiKey: key, geminiModel: model });
173
+ if (name === 'openai')
174
+ Object.assign(config_1.config, { openaiApiKey: key, openaiModel: model });
175
+ if (name === 'anthropic')
176
+ Object.assign(config_1.config, { anthropicApiKey: key, anthropicModel: model });
177
+ if (name === 'ollama')
178
+ Object.assign(config_1.config, { aiBaseUrl: exports.OLLAMA_URL, ollamaModel: model });
179
+ return { model };
180
+ },
181
+ answer(answer) {
182
+ current = answer;
183
+ },
184
+ ollamaUp(up) {
185
+ ollamaUp = up;
186
+ },
187
+ };
188
+ }
@@ -1,7 +1,8 @@
1
1
  "use strict";
2
2
  /**
3
- * An in-memory device table to stub `prisma.device` with, so a spec runs the
4
- * real PrismaDeviceStore. It follows Prisma's rules: `undefined` leaves a
3
+ * In-memory tables to stub `prisma.device` (fakeDeviceTable) and
4
+ * `prisma.deviceSetting` (fakeDeviceSettingTable) with, so a spec runs the
5
+ * real PrismaDeviceStore. They follow Prisma's rules: `undefined` leaves a
5
6
  * column alone, `null` clears it, an upsert takes `create` for a new row and
6
7
  * `update` for a known one, and `{ contains }` matches a substring (the store
7
8
  * uses it for a host that isn't a URL).
@@ -16,31 +17,69 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
16
17
  });
17
18
  };
18
19
  Object.defineProperty(exports, "__esModule", { value: true });
20
+ exports.fakeDeviceSettingTable = fakeDeviceSettingTable;
19
21
  exports.fakeDeviceTable = fakeDeviceTable;
20
- function fakeDeviceTable(seed) {
22
+ const key = (udid, host) => `${udid}|${host}`;
23
+ const matches = (row, where) => {
24
+ if (!where)
25
+ return true;
26
+ return Object.entries(where).every(([field, cond]) => {
27
+ var _a;
28
+ if (field === 'OR')
29
+ return cond.some((w) => matches(row, w));
30
+ if (field === 'AND')
31
+ return cond.every((w) => matches(row, w));
32
+ if (cond && typeof cond === 'object' && 'in' in cond)
33
+ return cond.in.includes(row[field]);
34
+ if (cond && typeof cond === 'object' && 'contains' in cond)
35
+ return String((_a = row[field]) !== null && _a !== void 0 ? _a : '').includes(cond.contains);
36
+ return row[field] === cond;
37
+ });
38
+ };
39
+ const write = (row, data) => {
40
+ for (const [field, value] of Object.entries(data))
41
+ if (value !== undefined)
42
+ row[field] = value;
43
+ };
44
+ /**
45
+ * The saved settings of phones (DeviceSetting), to stub `prisma.deviceSetting`
46
+ * with beside fakeDeviceTable, so the store's settings don't reach a database.
47
+ */
48
+ function fakeDeviceSettingTable(seed = []) {
21
49
  const rows = new Map();
22
- const key = (udid, host) => `${udid}|${host}`;
23
- const matches = (row, where) => {
24
- if (!where)
25
- return true;
26
- return Object.entries(where).every(([field, cond]) => {
27
- var _a;
28
- if (field === 'OR')
29
- return cond.some((w) => matches(row, w));
30
- if (field === 'AND')
31
- return cond.every((w) => matches(row, w));
32
- if (cond && typeof cond === 'object' && 'in' in cond)
33
- return cond.in.includes(row[field]);
34
- if (cond && typeof cond === 'object' && 'contains' in cond)
35
- return String((_a = row[field]) !== null && _a !== void 0 ? _a : '').includes(cond.contains);
36
- return row[field] === cond;
37
- });
38
- };
39
- const write = (row, data) => {
40
- for (const [field, value] of Object.entries(data))
41
- if (value !== undefined)
42
- row[field] = value;
50
+ for (const r of seed)
51
+ rows.set(key(r.udid, r.host), Object.assign({ userBlocked: false }, r));
52
+ return {
53
+ row: (udid, host) => [...rows.values()].find((r) => r.udid === udid && (host === undefined || r.host === host)),
54
+ delegate: {
55
+ findMany: (...args_1) => __awaiter(this, [...args_1], void 0, function* (args = {}) { return [...rows.values()].filter((r) => matches(r, args.where)).map((r) => (Object.assign({}, r))); }),
56
+ upsert: (_a) => __awaiter(this, [_a], void 0, function* ({ where, create, update, }) {
57
+ const k = key(where.udid_host.udid, where.udid_host.host);
58
+ const found = rows.get(k);
59
+ if (found) {
60
+ write(found, update);
61
+ return Object.assign({}, found);
62
+ }
63
+ const created = { userBlocked: false };
64
+ write(created, create);
65
+ rows.set(k, created);
66
+ return Object.assign({}, created);
67
+ }),
68
+ deleteMany: (...args_1) => __awaiter(this, [...args_1], void 0, function* (args = {}) {
69
+ let count = 0;
70
+ for (const [k, r] of rows) {
71
+ if (matches(r, args.where)) {
72
+ rows.delete(k);
73
+ count++;
74
+ }
75
+ }
76
+ return { count };
77
+ }),
78
+ },
43
79
  };
80
+ }
81
+ function fakeDeviceTable(seed) {
82
+ const rows = new Map();
44
83
  const defaults = () => ({
45
84
  busy: false,
46
85
  userBlocked: false,