@xenon-device-management/xenon 2.14.0 → 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 (259) hide show
  1. package/README.md +7 -4
  2. package/lib/package.json +5 -5
  3. package/lib/public/assets/{AnnotationOverlay-B_VNtKBH.js → AnnotationOverlay-Btq3dZRI.js} +1 -1
  4. package/lib/public/assets/{ApiKeyGate-BwuaMjkX.js → ApiKeyGate-C9OoAgg_.js} +1 -1
  5. package/lib/public/assets/DeviceMosaicView-DBC_xczB.js +2 -0
  6. package/lib/public/assets/{EmptyState-BSjhk_IV.js → EmptyState-DqWU2-vM.js} +1 -1
  7. package/lib/public/assets/{FieldGroup-Cb5QZsE1.js → FieldGroup-CveQ3xrA.js} +1 -1
  8. package/lib/public/assets/{FilterMenu-BAaSlWbW.js → FilterMenu-BfKDSfHz.js} +1 -1
  9. package/lib/public/assets/{Menu-JGGYrSQQ.js → Menu-CtdCybzA.js} +1 -1
  10. package/lib/public/assets/{Modal-CFCp3n9d.js → Modal-Bsvj1tm2.js} +1 -1
  11. package/lib/public/assets/{RecordingPage-CbJj_EBT.js → RecordingPage-CRofOiyx.js} +1 -1
  12. package/lib/public/assets/{RecordingsPage-BJeYnygR.js → RecordingsPage-jNQHb2IR.js} +1 -1
  13. package/lib/public/assets/{SegmentedControl-Bg42Atjy.js → SegmentedControl-C8HtTha-.js} +1 -1
  14. package/lib/public/assets/{SettingCard-Njz1pUIJ.js → SettingCard-D8K1LZNo.js} +1 -1
  15. package/lib/public/assets/{Table-BcTm5xV2.js → Table-zNTX92Pn.js} +1 -1
  16. package/lib/public/assets/{activity-DOVdsfMQ.js → activity-CJ8LwZq9.js} +1 -1
  17. package/lib/public/assets/ai-settings-CgxQZqrc.js +16 -0
  18. package/lib/public/assets/{api-keys-BKnNSTw9.js → api-keys-ECpo-zTo.js} +1 -1
  19. package/lib/public/assets/{apps-D0SC6uw6.js → apps-BjGymyf3.js} +1 -1
  20. package/lib/public/assets/{arrow-left-MMbTmgoE.js → arrow-left-BWfCbeHP.js} +1 -1
  21. package/lib/public/assets/{arrow-right-BMfnGmxW.js → arrow-right-DC7UGf_n.js} +1 -1
  22. package/lib/public/assets/{arrow-up-right-DuAftMWB.js → arrow-up-right-BOjwybDk.js} +1 -1
  23. package/lib/public/assets/{auth-shell-nZC2JNDO.js → auth-shell-CxMTo9aH.js} +2 -2
  24. package/lib/public/assets/{builds-page-hDCgTwkS.js → builds-page-DOCm4HkA.js} +1 -1
  25. package/lib/public/assets/{button-Dda4irn_.js → button-Cg08-EMT.js} +1 -1
  26. package/lib/public/assets/{calendar-BMr0Q62v.js → calendar-CA1ikLJs.js} +1 -1
  27. package/lib/public/assets/{check-CZ9iw9-e.js → check-BMTaH72W.js} +1 -1
  28. package/lib/public/assets/{chevron-right-BRYJJVC9.js → chevron-right-B2QOv9Nx.js} +1 -1
  29. package/lib/public/assets/{circle-check-CqAJjQ5O.js → circle-check-CYXBoUqT.js} +1 -1
  30. package/lib/public/assets/{circle-x-DlDJtnVz.js → circle-x-DTwnSAnl.js} +1 -1
  31. package/lib/public/assets/{clock-CpuDKysU.js → clock-C8_HViQP.js} +1 -1
  32. package/lib/public/assets/{copy-CAQydNFe.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-C9XG6lLv.js → download-B56lwxWP.js} +1 -1
  36. package/lib/public/assets/forgot-password-CCU6bzLn.js +1 -0
  37. package/lib/public/assets/{h264Stream-VQBphYhg.js → h264Stream-C-OtmIuk.js} +6 -6
  38. package/lib/public/assets/index-B9KQ6LsY.js +15 -0
  39. package/lib/public/assets/{index-BbKEXUFH.js → index-BOxe43p1.js} +2 -2
  40. package/lib/public/assets/{index-DJGc1M2f.js → index-CfxsUslp.js} +1 -1
  41. package/lib/public/assets/{input-Cm0tmSoc.js → input-CZHFjbSg.js} +1 -1
  42. package/lib/public/assets/{line-chart-D2DyJCml.js → line-chart-Gl_Dz3X_.js} +1 -1
  43. package/lib/public/assets/{list-checks-UWf1GY99.js → list-checks-CCgjeyGY.js} +1 -1
  44. package/lib/public/assets/{lock-CCJv9m_I.js → lock-CkOwgDcx.js} +1 -1
  45. package/lib/public/assets/{login-f8vOVW-U.js → login-D5cc4w7M.js} +1 -1
  46. package/lib/public/assets/{maintenance-settings-C1PrkuAH.js → maintenance-settings-t7qr3fvX.js} +1 -1
  47. package/lib/public/assets/{monitor-B2jDgwm7.js → monitor-DEJixKQO.js} +1 -1
  48. package/lib/public/assets/{mouse-pointer-2-DLI1RtzU.js → mouse-pointer-2-CtOrk4M8.js} +1 -1
  49. package/lib/public/assets/{network-ebT86qoi.js → network-Dii9qdvk.js} +1 -1
  50. package/lib/public/assets/{overview-C0aykVrd.js → overview-rolAACrO.js} +1 -1
  51. package/lib/public/assets/{page-header-BjzgWz-T.js → page-header-COQZxJfR.js} +1 -1
  52. package/lib/public/assets/{play-DLG9t8FU.js → play-DrN1Se_R.js} +1 -1
  53. package/lib/public/assets/{plus-nBFSMu_X.js → plus-5aq-9uRx.js} +1 -1
  54. package/lib/public/assets/{profile-page-CSl0UYlv.js → profile-page-6i5kY6kF.js} +1 -1
  55. package/lib/public/assets/{reset-password-LeOuHqmB.js → reset-password-sp2IBrc7.js} +1 -1
  56. package/lib/public/assets/runbook-page-DJDjeopU.js +253 -0
  57. package/lib/public/assets/{select-88jHcl-Z.js → select-Bpyn7rWO.js} +1 -1
  58. package/lib/public/assets/{selector-detail-redirect-BbXKJjZM.js → selector-detail-redirect-BH74vmKv.js} +1 -1
  59. package/lib/public/assets/{selector-health-page-B4rA1Xkv.js → selector-health-page-Cw_g218H.js} +1 -1
  60. package/lib/public/assets/session-detail-page-AZJE6XXE.js +4 -0
  61. package/lib/public/assets/{settings-GF6ZMSQ6.js → settings-C3uyGhyc.js} +1 -1
  62. package/lib/public/assets/{settings-Dep_3kbA.css → settings-K8UHWMdI.css} +1 -1
  63. package/lib/public/assets/{stat-tile-BV98e_Sv.js → stat-tile-xVqAKIlI.js} +1 -1
  64. package/lib/public/assets/{tablet-BiAsnFDj.js → tablet-BxUScAeO.js} +1 -1
  65. package/lib/public/assets/{teams-B4-jrr-1.js → teams-B5ljGkY2.js} +1 -1
  66. package/lib/public/assets/{trash-2-C5RSuX0e.js → trash-2-B701i8sy.js} +1 -1
  67. package/lib/public/assets/{upload-BG7_rMXK.js → upload-C2gr44RS.js} +1 -1
  68. package/lib/public/assets/{use-builds-data-J-riQYyC.js → use-builds-data-NeQOFEoy.js} +1 -1
  69. package/lib/public/assets/{use-password-reset-mode-C8-eH-UG.js → use-password-reset-mode-DEzJq5ef.js} +1 -1
  70. package/lib/public/assets/{users-CFba5TSy.js → users-D_vv_yz4.js} +1 -1
  71. package/lib/public/assets/{users-Cp0beJm3.js → users-MY6eNe6s.js} +1 -1
  72. package/lib/public/assets/{video-off-DDsEISjV.js → video-off-OK1lNZQn.js} +1 -1
  73. package/lib/public/assets/{webhook-settings-BGu32vSB.js → webhook-settings-BU-C9lBf.js} +1 -1
  74. package/lib/public/assets/{zap-l4VaXzq8.js → zap-DO9ry1iz.js} +1 -1
  75. package/lib/public/index.html +1 -1
  76. package/lib/schema.json +5 -5
  77. package/lib/src/app/dashboardPluginLink.js +117 -0
  78. package/lib/src/app/index.js +13 -34
  79. package/lib/src/app/openapi/control.yaml +6 -4
  80. package/lib/src/app/openapi/grid.yaml +8 -2
  81. package/lib/src/app/openapi/identity.yaml +103 -31
  82. package/lib/src/app/openapi/platform.yaml +33 -15
  83. package/lib/src/app/openapi/sessions.yaml +10 -3
  84. package/lib/src/app/routers/apikeys.js +14 -2
  85. package/lib/src/app/routers/auth.js +29 -5
  86. package/lib/src/app/routers/control.js +13 -5
  87. package/lib/src/app/routers/dashboard.js +42 -27
  88. package/lib/src/app/routers/grid.js +5 -3
  89. package/lib/src/app/routers/profile.js +21 -0
  90. package/lib/src/app/routers/users.js +10 -5
  91. package/lib/src/app/swagger.js +2 -0
  92. package/lib/src/app/ws/h264StreamWs.js +10 -1
  93. package/lib/src/config.js +1 -0
  94. package/lib/src/dashboard/commands.js +5 -5
  95. package/lib/src/dashboard/event-manager.js +49 -61
  96. package/lib/src/dashboard/services/failure-analysis-service.js +136 -45
  97. package/lib/src/dashboard/sessionCommandSummary.js +39 -0
  98. package/lib/src/data-service/device-store.js +14 -1
  99. package/lib/src/data-service/prisma-store.js +19 -7
  100. package/lib/src/data-service/web-config-service.js +7 -0
  101. package/lib/src/device-managers/android/AndroidH264StreamService.js +69 -20
  102. package/lib/src/device-managers/android/H264Multiplexer.js +38 -4
  103. package/lib/src/device-utils.js +20 -2
  104. package/lib/src/gateway/defaultGateway.js +1 -1
  105. package/lib/src/gateway/forwardToNode.js +2 -0
  106. package/lib/src/gateway/healReport.js +87 -0
  107. package/lib/src/gateway/sessionGateway.js +12 -2
  108. package/lib/src/generated/client/edge.js +4 -3
  109. package/lib/src/generated/client/index-browser.js +1 -0
  110. package/lib/src/generated/client/index.d.ts +32 -0
  111. package/lib/src/generated/client/index.js +4 -3
  112. package/lib/src/generated/client/package.json +1 -1
  113. package/lib/src/generated/client/schema.prisma +3 -0
  114. package/lib/src/generated/client/wasm.js +1 -0
  115. package/lib/src/interceptors/CommandInterceptor.js +243 -181
  116. package/lib/src/middleware/authMiddleware.js +15 -3
  117. package/lib/src/middleware/commandCaller.js +3 -3
  118. package/lib/src/middleware/verifyCredential.js +58 -1
  119. package/lib/src/plugin.js +8 -3
  120. package/lib/src/services/AICommandService.js +9 -2
  121. package/lib/src/services/AIService.js +192 -42
  122. package/lib/src/services/EmailService.js +29 -0
  123. package/lib/src/services/OrphanSweeper.js +2 -0
  124. package/lib/src/services/SelectorVerificationJob.js +15 -4
  125. package/lib/src/services/ServerManager.js +11 -1
  126. package/lib/src/services/SessionLifecycleService.js +86 -26
  127. package/lib/src/services/SocketServer.js +19 -3
  128. package/lib/src/services/bug-report/video-slice.js +4 -1
  129. package/lib/src/services/device-access/leaseOverride.js +7 -5
  130. package/lib/src/services/healing/FuzzyXmlHealingProvider.js +51 -0
  131. package/lib/src/services/healing/HealEtalonService.js +3 -2
  132. package/lib/src/services/healing/HealingOrchestrator.js +55 -72
  133. package/lib/src/services/healing/OcrHealingProvider.js +41 -64
  134. package/lib/src/services/healing/ResilioTreeHealingProvider.js +41 -50
  135. package/lib/src/services/healing/VisualAiHealingProvider.js +1 -1
  136. package/lib/src/services/healing/fingerprintIdentity.js +126 -0
  137. package/lib/src/services/healing/resilioPath.js +274 -0
  138. package/lib/src/services/logcat/SessionDeviceLogs.js +271 -0
  139. package/lib/src/services/logcat/deviceLogBook.js +163 -0
  140. package/lib/src/services/ocr/ocrData.js +118 -0
  141. package/lib/src/services/ocr/vendor/LICENSE-tessdata.txt +202 -0
  142. package/lib/src/services/ocr/vendor/eng.traineddata.gz +0 -0
  143. package/lib/src/services/omni-vision/OmniVisionService.js +83 -56
  144. package/lib/src/services/passwordResetLink.js +50 -9
  145. package/lib/src/services/passwordResetMode.js +3 -1
  146. package/lib/src/services/publicUrl.js +45 -0
  147. package/lib/src/services/session/sessionIdentity.js +5 -3
  148. package/lib/src/services/session/shutdownReason.js +26 -0
  149. package/lib/src/services/settings/aiEngineSettings.js +219 -0
  150. package/lib/src/services/token/mintedLifetime.js +51 -0
  151. package/lib/src/sessions/SessionManager.js +8 -2
  152. package/lib/src/sessions/sessionMemory.js +25 -0
  153. package/lib/test/helpers/fake-ai-provider.js +188 -0
  154. package/lib/test/helpers/loki-stores.js +33 -0
  155. package/lib/test/helpers/page-sources.js +164 -0
  156. package/lib/test/helpers/scratch-database.js +57 -13
  157. package/lib/test/integration/audit-ingest.spec.js +10 -10
  158. package/lib/test/integration/auth-flow.spec.js +9 -14
  159. package/lib/test/integration/auth-rate-limit.spec.js +6 -3
  160. package/lib/test/integration/bug-report-route.spec.js +22 -2
  161. package/lib/test/integration/forgot-password-rate-limit.spec.js +6 -3
  162. package/lib/test/integration/forgot-password.spec.js +17 -18
  163. package/lib/test/integration/legacy-deprecation-headers.spec.js +5 -2
  164. package/lib/test/integration/migration.spec.js +6 -1
  165. package/lib/test/integration/node-pair-auth-rest.spec.js +5 -9
  166. package/lib/test/integration/ports-router.spec.js +4 -4
  167. package/lib/test/integration/prisma-device-store-add-devices.spec.js +2 -3
  168. package/lib/test/integration/prisma-device-store-team-filter.spec.js +2 -5
  169. package/lib/test/integration/profile-tokens.spec.js +10 -16
  170. package/lib/test/integration/recordings-router.spec.js +16 -16
  171. package/lib/test/integration/reset-revokes-sessions.spec.js +4 -9
  172. package/lib/test/integration/role-matrix.spec.js +4 -9
  173. package/lib/test/integration/sdk-leases-router.spec.js +9 -9
  174. package/lib/test/integration/sdk-version.spec.js +2 -2
  175. package/lib/test/integration/selector-health-endpoint.spec.js +5 -5
  176. package/lib/test/integration/team-membership-migration.spec.js +73 -12
  177. package/lib/test/integration/team-visibility-apps.spec.js +15 -22
  178. package/lib/test/integration/team-visibility-control.spec.js +96 -69
  179. package/lib/test/integration/team-visibility-dashboard.spec.js +7 -23
  180. package/lib/test/integration/team-visibility-grid.spec.js +16 -15
  181. package/lib/test/integration/team-visibility-leases.spec.js +8 -11
  182. package/lib/test/integration/team-visibility-recordings.spec.js +6 -19
  183. package/lib/test/integration/team-visibility-sessions.spec.js +6 -19
  184. package/lib/test/integration/users-flow.spec.js +7 -12
  185. package/lib/test/integration/users-lockout.spec.js +5 -3
  186. package/lib/test/unit/EmailService.test.js +66 -0
  187. package/lib/test/unit/ResilioTreeHealingProvider.test.js +46 -19
  188. package/lib/test/unit/ai-call-time-limit.spec.js +130 -0
  189. package/lib/test/unit/ai-engine-settings.spec.js +338 -0
  190. package/lib/test/unit/ai-provider-switch.spec.js +130 -0
  191. package/lib/test/unit/ai-rate-limit.spec.js +126 -0
  192. package/lib/test/unit/android-h264-viewers.spec.js +272 -0
  193. package/lib/test/unit/android-preview-hold.spec.js +3 -2
  194. package/lib/test/unit/apikeys-router.spec.js +84 -0
  195. package/lib/test/unit/authMiddleware.bearer.spec.js +9 -0
  196. package/lib/test/unit/authMiddleware.credentialLifetime.spec.js +149 -0
  197. package/lib/test/unit/bug-report/video-slice.spec.js +19 -0
  198. package/lib/test/unit/dashboard-events-device-logs.spec.js +207 -0
  199. package/lib/test/unit/dashboard-plugin-link.spec.js +213 -0
  200. package/lib/test/unit/device-log-book.spec.js +180 -0
  201. package/lib/test/unit/enable-dashboard-option.spec.js +57 -0
  202. package/lib/test/unit/event-log-session-data.spec.js +244 -0
  203. package/lib/test/unit/failure-analysis.spec.js +182 -0
  204. package/lib/test/unit/failure-categories.spec.js +128 -0
  205. package/lib/test/unit/fingerprint-identity.spec.js +433 -0
  206. package/lib/test/unit/h264-multiplexer.spec.js +46 -0
  207. package/lib/test/unit/heal-report.spec.js +71 -0
  208. package/lib/test/unit/healing-instrumentation.spec.js +6 -1
  209. package/lib/test/unit/healing-tier-gate.spec.js +73 -29
  210. package/lib/test/unit/healing-tiers-every-session.spec.js +219 -0
  211. package/lib/test/unit/hub-node-gateway-appium-server.spec.js +171 -0
  212. package/lib/test/unit/interceptor-visual-elements.spec.js +192 -0
  213. package/lib/test/unit/lease/lease-override.spec.js +23 -6
  214. package/lib/test/unit/lease/session-lease-proof.spec.js +16 -4
  215. package/lib/test/unit/ocr-healing-tier-real-ocr.spec.js +113 -0
  216. package/lib/test/unit/ocr-healing-tier.spec.js +122 -0
  217. package/lib/test/unit/ocr-offline.spec.js +213 -0
  218. package/lib/test/unit/omni-virtual-element-store.spec.js +301 -0
  219. package/lib/test/unit/omni-vision-ios-points.spec.js +0 -2
  220. package/lib/test/unit/omni-vision.spec.js +5 -7
  221. package/lib/test/unit/package-engines.spec.js +27 -0
  222. package/lib/test/unit/passwordResetMode.test.js +10 -2
  223. package/lib/test/unit/profile-router.test.js +122 -1
  224. package/lib/test/unit/queue-requester.spec.js +1 -1
  225. package/lib/test/unit/reset-link-address.spec.js +266 -0
  226. package/lib/test/unit/reset-link.test.js +10 -4
  227. package/lib/test/unit/resilio-path-healing.spec.js +270 -0
  228. package/lib/test/unit/selector-learning-every-session.spec.js +179 -0
  229. package/lib/test/unit/selector-verification-clean-build.spec.js +130 -0
  230. package/lib/test/unit/selector-verification-job.spec.js +13 -13
  231. package/lib/test/unit/self-healing-boot.spec.js +10 -5
  232. package/lib/test/unit/self-healing-setting.spec.js +7 -6
  233. package/lib/test/unit/session-app-resolution.spec.js +5 -1
  234. package/lib/test/unit/session-attribution-wiring.spec.js +41 -13
  235. package/lib/test/unit/session-create-credential-owner.spec.js +242 -0
  236. package/lib/test/unit/session-credential-strip.spec.js +1 -1
  237. package/lib/test/unit/session-device-logs.spec.js +347 -0
  238. package/lib/test/unit/session-end-failure-analysis.spec.js +133 -0
  239. package/lib/test/unit/session-log-indexes.spec.js +64 -3
  240. package/lib/test/unit/socketServer.sessionCookie.spec.js +2 -1
  241. package/lib/test/unit/stream-start-pipelines.spec.js +72 -18
  242. package/lib/test/unit/token-mint-limits.spec.js +165 -0
  243. package/lib/test/unit/unexpected-shutdown-reason.spec.js +163 -0
  244. package/lib/test/unit/verify-credential.spec.js +33 -0
  245. package/lib/test/unit/vision-assertion.spec.js +3 -2
  246. package/lib/test/unit/web-config-service.spec.js +19 -0
  247. package/lib/tsconfig.tsbuildinfo +1 -1
  248. package/package.json +5 -5
  249. package/prisma/migrations/20261004174413_locator_etalon_path/migration.sql +2 -0
  250. package/prisma/schema.prisma +3 -0
  251. package/schema.json +5 -5
  252. package/lib/public/assets/BugReportButton-BAt0_pzq.js +0 -15
  253. package/lib/public/assets/DeviceMosaicView-BYaQHhTf.js +0 -2
  254. package/lib/public/assets/ai-settings-CsuJroRf.js +0 -21
  255. package/lib/public/assets/device-explorer-CdO6_nrn.js +0 -282
  256. package/lib/public/assets/forgot-password-CGnkRgE9.js +0 -1
  257. package/lib/public/assets/runbook-page-CMMeQml9.js +0 -124
  258. package/lib/public/assets/session-detail-page-COL_RZxw.js +0 -4
  259. /package/lib/public/assets/{BugReportButton-CPBjfiv4.css → index-CPBjfiv4.css} +0 -0
@@ -15,25 +15,20 @@ Object.defineProperty(exports, "__esModule", { value: true });
15
15
  require("reflect-metadata");
16
16
  const chai_1 = require("chai");
17
17
  const express_1 = __importDefault(require("express"));
18
- const supertest_1 = __importDefault(require("supertest"));
18
+ const loopbackRequest_1 = __importDefault(require("../helpers/loopbackRequest"));
19
19
  const authMiddleware_1 = require("../../src/middleware/authMiddleware");
20
20
  const users_1 = require("../../src/app/routers/users");
21
- const prisma_1 = require("../../src/prisma");
21
+ const scratch_database_1 = require("../helpers/scratch-database");
22
22
  const seedUser_1 = require("../helpers/seedUser");
23
23
  describe('users CRUD flow (integration)', function () {
24
24
  this.timeout(60000);
25
+ (0, scratch_database_1.useScratchDatabase)({ wholeSuite: true });
25
26
  let sa;
26
27
  let admin;
27
28
  before(() => __awaiter(this, void 0, void 0, function* () {
28
29
  sa = yield (0, seedUser_1.seedUser)('SUPER_ADMIN', { name: 'Flow SA' });
29
30
  admin = yield (0, seedUser_1.seedUser)('ADMIN', { name: 'Flow Admin' });
30
31
  }));
31
- after(() => __awaiter(this, void 0, void 0, function* () {
32
- yield sa.cleanup();
33
- yield admin.cleanup();
34
- // Anything created during the flow with the flow-bob email tag.
35
- yield prisma_1.prisma.user.deleteMany({ where: { email: { contains: 'flow-bob' } } });
36
- }));
37
32
  function buildApp() {
38
33
  const app = (0, express_1.default)();
39
34
  app.use(express_1.default.json());
@@ -44,27 +39,27 @@ describe('users CRUD flow (integration)', function () {
44
39
  it('SA creates an admin -> admin creates a member -> admin lists', () => __awaiter(this, void 0, void 0, function* () {
45
40
  const app = buildApp();
46
41
  // ADMIN role-create — should fail with 403 (admin can't create admins).
47
- const denied = yield (0, supertest_1.default)(app)
42
+ const denied = yield (0, loopbackRequest_1.default)(app)
48
43
  .post('/users')
49
44
  .set('Cookie', admin.cookie)
50
45
  .send({ email: 'flow-admin2@xenon.local', name: 'Admin 2', role: 'ADMIN' });
51
46
  (0, chai_1.expect)(denied.status).to.equal(403);
52
47
  // SA creates a member.
53
48
  const memberEmail = `flow-bob-${Date.now()}@xenon.local`;
54
- const created = yield (0, supertest_1.default)(app)
49
+ const created = yield (0, loopbackRequest_1.default)(app)
55
50
  .post('/users')
56
51
  .set('Cookie', sa.cookie)
57
52
  .send({ email: memberEmail, name: 'Bob', role: 'MEMBER' });
58
53
  (0, chai_1.expect)(created.status).to.equal(201);
59
54
  (0, chai_1.expect)(created.body.temporaryPassword).to.match(/^[A-Za-z0-9_-]{12,}$/);
60
55
  // Admin lists — sees the new member; does NOT see SA or other admins.
61
- const listAdmin = yield (0, supertest_1.default)(app).get('/users').set('Cookie', admin.cookie);
56
+ const listAdmin = yield (0, loopbackRequest_1.default)(app).get('/users').set('Cookie', admin.cookie);
62
57
  (0, chai_1.expect)(listAdmin.status).to.equal(200);
63
58
  const emailsAdmin = listAdmin.body.map((u) => u.email);
64
59
  (0, chai_1.expect)(emailsAdmin).to.include(memberEmail);
65
60
  (0, chai_1.expect)(emailsAdmin).to.not.include(sa.user.email);
66
61
  // SA lists — sees everyone.
67
- const listSA = yield (0, supertest_1.default)(app).get('/users').set('Cookie', sa.cookie);
62
+ const listSA = yield (0, loopbackRequest_1.default)(app).get('/users').set('Cookie', sa.cookie);
68
63
  const emailsSA = listSA.body.map((u) => u.email);
69
64
  (0, chai_1.expect)(emailsSA).to.include(sa.user.email);
70
65
  (0, chai_1.expect)(emailsSA).to.include(admin.user.email);
@@ -15,13 +15,15 @@ Object.defineProperty(exports, "__esModule", { value: true });
15
15
  require("reflect-metadata");
16
16
  const chai_1 = require("chai");
17
17
  const express_1 = __importDefault(require("express"));
18
- const supertest_1 = __importDefault(require("supertest"));
18
+ const loopbackRequest_1 = __importDefault(require("../helpers/loopbackRequest"));
19
19
  const authMiddleware_1 = require("../../src/middleware/authMiddleware");
20
20
  const users_1 = require("../../src/app/routers/users");
21
21
  const prisma_1 = require("../../src/prisma");
22
22
  const seedUser_1 = require("../helpers/seedUser");
23
+ const scratch_database_1 = require("../helpers/scratch-database");
23
24
  describe('users lockout protections (integration)', function () {
24
25
  this.timeout(60000);
26
+ (0, scratch_database_1.useScratchDatabase)({ wholeSuite: true });
25
27
  function buildApp() {
26
28
  const app = (0, express_1.default)();
27
29
  app.use(express_1.default.json());
@@ -32,7 +34,7 @@ describe('users lockout protections (integration)', function () {
32
34
  it('SA cannot delete themselves', () => __awaiter(this, void 0, void 0, function* () {
33
35
  const me = yield (0, seedUser_1.seedUser)('SUPER_ADMIN', { name: 'Lockout Self' });
34
36
  try {
35
- const r = yield (0, supertest_1.default)(buildApp()).delete(`/users/${me.user.id}`).set('Cookie', me.cookie);
37
+ const r = yield (0, loopbackRequest_1.default)(buildApp()).delete(`/users/${me.user.id}`).set('Cookie', me.cookie);
36
38
  (0, chai_1.expect)(r.status).to.equal(400);
37
39
  (0, chai_1.expect)(r.body.error).to.match(/yourself/);
38
40
  }
@@ -43,7 +45,7 @@ describe('users lockout protections (integration)', function () {
43
45
  it('SA cannot demote themselves', () => __awaiter(this, void 0, void 0, function* () {
44
46
  const me = yield (0, seedUser_1.seedUser)('SUPER_ADMIN', { name: 'Lockout Demote-Self' });
45
47
  try {
46
- const r = yield (0, supertest_1.default)(buildApp())
48
+ const r = yield (0, loopbackRequest_1.default)(buildApp())
47
49
  .patch(`/users/${me.user.id}`)
48
50
  .set('Cookie', me.cookie)
49
51
  .send({ role: 'MEMBER' });
@@ -121,4 +121,70 @@ describe('EmailService', () => {
121
121
  }
122
122
  });
123
123
  });
124
+ // A reset link Xenon sends points at XENON_PUBLIC_URL, never the request's
125
+ // Host, so with SMTP but no address set no link is emailed at all. Said once
126
+ // at startup, so an operator upgrading learns why self-service reset went.
127
+ describe('warnAboutPublicUrl', () => {
128
+ it('warns when Xenon could send links but has no XENON_PUBLIC_URL to point them at', () => {
129
+ const orig = {
130
+ url: config_1.config.smtpUrl,
131
+ fb: config_1.config.passwordResetLogFallback,
132
+ pub: config_1.config.publicUrl,
133
+ };
134
+ const svc = new EmailService_1.EmailService();
135
+ const warn = sinon_1.default.stub(svc, 'warnLog');
136
+ try {
137
+ config_1.config.smtpUrl = undefined;
138
+ config_1.config.passwordResetLogFallback = false;
139
+ config_1.config.publicUrl = undefined;
140
+ svc.warnAboutPublicUrl();
141
+ (0, chai_1.expect)(warn.called, 'nothing could be sent anyway').to.equal(false);
142
+ config_1.config.smtpUrl = 'smtp://mail.example.com:587';
143
+ config_1.config.publicUrl = 'https://xenon.example.com';
144
+ svc.warnAboutPublicUrl();
145
+ (0, chai_1.expect)(warn.called).to.equal(false);
146
+ config_1.config.publicUrl = 'not a url';
147
+ svc.warnAboutPublicUrl();
148
+ (0, chai_1.expect)(warn.calledOnce).to.equal(true);
149
+ (0, chai_1.expect)(warn.firstCall.args[0]).to.match(/XENON_PUBLIC_URL/);
150
+ }
151
+ finally {
152
+ config_1.config.smtpUrl = orig.url;
153
+ config_1.config.passwordResetLogFallback = orig.fb;
154
+ config_1.config.publicUrl = orig.pub;
155
+ }
156
+ });
157
+ // The variable also drives each device's dashboard_link, so a value
158
+ // Xenon refuses is said at startup whether or not mail is set up. It was
159
+ // silent without SMTP. The value itself is not logged: it may carry a
160
+ // user name and password, one of the reasons it is refused.
161
+ it('warns, once, when XENON_PUBLIC_URL is set but refused, mail or not', () => {
162
+ const orig = {
163
+ url: config_1.config.smtpUrl,
164
+ fb: config_1.config.passwordResetLogFallback,
165
+ pub: config_1.config.publicUrl,
166
+ };
167
+ const svc = new EmailService_1.EmailService();
168
+ const warn = sinon_1.default.stub(svc, 'warnLog');
169
+ try {
170
+ config_1.config.smtpUrl = undefined;
171
+ config_1.config.passwordResetLogFallback = false;
172
+ config_1.config.publicUrl = 'https://user:secret@lab.example.com/qa';
173
+ svc.warnAboutPublicUrl();
174
+ (0, chai_1.expect)(warn.calledOnce).to.equal(true);
175
+ (0, chai_1.expect)(warn.firstCall.args[0]).to.match(/XENON_PUBLIC_URL is set/);
176
+ (0, chai_1.expect)(warn.firstCall.args[0]).to.match(/dashboard/);
177
+ (0, chai_1.expect)(warn.firstCall.args[0]).to.not.include('secret');
178
+ warn.resetHistory();
179
+ config_1.config.smtpUrl = 'smtp://mail.example.com:587';
180
+ svc.warnAboutPublicUrl();
181
+ (0, chai_1.expect)(warn.calledOnce, 'one warning with SMTP too').to.equal(true);
182
+ }
183
+ finally {
184
+ config_1.config.smtpUrl = orig.url;
185
+ config_1.config.passwordResetLogFallback = orig.fb;
186
+ config_1.config.publicUrl = orig.pub;
187
+ }
188
+ });
189
+ });
124
190
  });
@@ -13,7 +13,8 @@ require("reflect-metadata");
13
13
  const chai_1 = require("chai");
14
14
  const types_1 = require("../../src/services/healing/types");
15
15
  const ResilioTreeHealingProvider_1 = require("../../src/services/healing/ResilioTreeHealingProvider");
16
- const { JSDOMParser, Path } = require('resiliotree');
16
+ const xmldom_1 = require("@xmldom/xmldom");
17
+ const resilioPath_1 = require("../../src/services/healing/resilioPath");
17
18
  describe('ResilioTreeHealingProvider', () => {
18
19
  let provider;
19
20
  let mockEtalonService;
@@ -43,32 +44,26 @@ describe('ResilioTreeHealingProvider', () => {
43
44
  };
44
45
  provider = new ResilioTreeHealingProvider_1.ResilioTreeHealingProvider(mockEtalonService);
45
46
  });
46
- it('should heal an element when a ResilioTree path exists', () => __awaiter(void 0, void 0, void 0, function* () {
47
- const parser = new JSDOMParser();
48
- const root = parser.parse(sourceXml);
49
- // Find the button in source
50
- const bodyNode = root.children[0];
51
- const hierarchyNode = bodyNode.children[0];
52
- const frameLayout = hierarchyNode.children[0];
53
- const linearLayout = frameLayout.children[0];
54
- const buttonNode = linearLayout.children[0];
55
- const path = new Path([root, bodyNode, hierarchyNode, frameLayout, linearLayout, buttonNode]);
47
+ // The button's id and text both changed, so it is another element as far as
48
+ // its path can tell: Resilio leaves it to Fuzzy XML rather than guess. (This
49
+ // test used to expect a heal, from a path made by an HTML parse and a driver
50
+ // stub that accepted any XPath, including the lowercase one Resilio wrote,
51
+ // which no real driver matches. See resilio-path-healing.spec.ts.)
52
+ it('leaves an element whose id and text changed to the next tier', () => __awaiter(void 0, void 0, void 0, function* () {
53
+ const doc = new xmldom_1.DOMParser().parseFromString(sourceXml, 'text/xml');
54
+ const button = doc.getElementsByTagName('android.widget.Button')[0];
56
55
  mockEtalonService.getSignature = () => __awaiter(void 0, void 0, void 0, function* () {
57
56
  return ({
58
57
  selector: "//android.widget.Button[@text='Submit']",
59
58
  strategy: 'xpath',
60
59
  attributes: { text: 'Submit', 'resource-id': 'com.example:id/submit_btn' },
61
60
  nodeName: 'android.widget.Button',
62
- path: path.toJSON(),
61
+ path: (0, resilioPath_1.resilioPathOf)(button, sourceXml),
63
62
  lastSeen: Date.now(),
64
63
  });
65
64
  });
66
65
  const mockDriver = {
67
- findElement: (strategy, selector) => __awaiter(void 0, void 0, void 0, function* () {
68
- (0, chai_1.expect)(strategy).to.equal('xpath');
69
- (0, chai_1.expect)(selector.toLowerCase()).to.contain('android.widget.button');
70
- return { ELEMENT: 'healed-element-123' };
71
- }),
66
+ findElement: () => __awaiter(void 0, void 0, void 0, function* () { return ({ ELEMENT: 'any-element' }); }),
72
67
  };
73
68
  const context = {
74
69
  sessionId: 'test-session',
@@ -77,11 +72,43 @@ describe('ResilioTreeHealingProvider', () => {
77
72
  selector: "//android.widget.Button[@text='Submit']",
78
73
  pageSource: brokenXml,
79
74
  };
80
- const result = yield provider.heal(context);
81
- (0, chai_1.expect)(result).to.not.be.null;
75
+ (0, chai_1.expect)(yield provider.heal(context)).to.equal(null);
76
+ }));
77
+ it('heals the same element after the page changed around it', () => __awaiter(void 0, void 0, void 0, function* () {
78
+ const doc = new xmldom_1.DOMParser().parseFromString(sourceXml, 'text/xml');
79
+ const button = doc.getElementsByTagName('android.widget.Button')[0];
80
+ mockEtalonService.getSignature = () => __awaiter(void 0, void 0, void 0, function* () {
81
+ return ({
82
+ selector: "//android.widget.Button[@text='Submit']",
83
+ strategy: 'xpath',
84
+ attributes: {},
85
+ nodeName: 'android.widget.Button',
86
+ path: (0, resilioPath_1.resilioPathOf)(button, sourceXml),
87
+ lastSeen: Date.now(),
88
+ });
89
+ });
90
+ // The button moved into a new row; it is still "Submit".
91
+ const movedXml = sourceXml
92
+ .replace('<android.widget.Button index="0"', '<android.widget.LinearLayout index="0"><android.widget.Button index="0"')
93
+ .replace('resource-id="com.example:id/submit_btn" />', 'resource-id="com.example:id/submit_btn" /></android.widget.LinearLayout>');
94
+ const asked = [];
95
+ const result = yield provider.heal({
96
+ sessionId: 'test-session',
97
+ driver: {
98
+ findElement: (_using, selector) => __awaiter(void 0, void 0, void 0, function* () {
99
+ asked.push(selector);
100
+ return { ELEMENT: 'healed-element-123' };
101
+ }),
102
+ },
103
+ strategy: 'xpath',
104
+ selector: "//android.widget.Button[@text='Submit']",
105
+ pageSource: movedXml,
106
+ });
82
107
  (0, chai_1.expect)(result === null || result === void 0 ? void 0 : result.id).to.equal('healed-element-123');
83
108
  (0, chai_1.expect)(result === null || result === void 0 ? void 0 : result.tier).to.equal(types_1.HealingTier.TIER_1_RECOVERY);
84
109
  (0, chai_1.expect)(result === null || result === void 0 ? void 0 : result.message).to.contain('ResilioTree');
110
+ // The element's own id, which it kept.
111
+ (0, chai_1.expect)(asked[0]).to.equal("//*[@resource-id='com.example:id/submit_btn']");
85
112
  }));
86
113
  it('should return null if no signature/path is found', () => __awaiter(void 0, void 0, void 0, function* () {
87
114
  const context = {
@@ -0,0 +1,130 @@
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 AIService_1 = require("../../src/services/AIService");
19
+ const CircuitBreaker_1 = require("../../src/services/CircuitBreaker");
20
+ const HealingOrchestrator_1 = require("../../src/services/healing/HealingOrchestrator");
21
+ const fake_ai_provider_1 = require("../helpers/fake-ai-provider");
22
+ /**
23
+ * An AI call made while a test command runs has a time limit.
24
+ *
25
+ * The LLM and visual healing tiers run inside a failing findElement, the
26
+ * visual assertion and the screen description inside an execute script, and
27
+ * an ai-icon find inside findElement or device control's Test locator. Only
28
+ * Ollama had a time limit (30 s). The OpenAI and Anthropic SDKs wait up to 10
29
+ * minutes a try, with two retries, and Gemini has none, so one provider that
30
+ * didn't answer held the command, and the test, for as long, past the
31
+ * client's own timeout.
32
+ */
33
+ describe('AI calls made during a test command', () => {
34
+ const ai = (0, fake_ai_provider_1.useFakeAiProviders)();
35
+ const SCREEN = 'c2NyZWVuc2hvdA==';
36
+ afterEach(() => sinon_1.default.restore());
37
+ /** What a call came to: its answer, or its error. */
38
+ const outcomeOf = (p) => p.then((value) => ({ value, error: undefined }), (error) => ({ value: undefined, error }));
39
+ const calls = [
40
+ {
41
+ name: 'an LLM heal finds nothing',
42
+ call: (s) => s.healLocator({ selector: '//x', strategy: 'xpath', xml: '<hierarchy/>' }),
43
+ expectOutcome: ({ value, error }) => {
44
+ (0, chai_1.expect)(error).to.equal(undefined);
45
+ (0, chai_1.expect)(value).to.equal(null);
46
+ },
47
+ },
48
+ {
49
+ name: 'a visual heal or ai-icon find finds nothing',
50
+ call: (s) => s.visualFind(SCREEN, 'the cart icon'),
51
+ expectOutcome: ({ value, error }) => {
52
+ (0, chai_1.expect)(error).to.equal(undefined);
53
+ (0, chai_1.expect)(value).to.equal(null);
54
+ },
55
+ },
56
+ {
57
+ name: "Test locator fails, saying the provider didn't answer",
58
+ call: (s) => s.visualFind(SCREEN, 'the cart icon', { throwOnError: true }),
59
+ expectOutcome: ({ error }) => {
60
+ (0, chai_1.expect)(error === null || error === void 0 ? void 0 : error.message).to.equal("The AI provider didn't answer within 30 s");
61
+ },
62
+ },
63
+ {
64
+ name: "a visual assertion fails, saying the provider didn't answer",
65
+ call: (s) => s.assertVisual(SCREEN, 'The cart is empty'),
66
+ expectOutcome: ({ error }) => {
67
+ (0, chai_1.expect)(error === null || error === void 0 ? void 0 : error.message).to.equal("The AI provider didn't answer within 30 s, so the condition was not checked. Try again later.");
68
+ },
69
+ },
70
+ {
71
+ name: "a screen description fails, saying the provider didn't answer",
72
+ call: (s) => s.describeScreen(SCREEN),
73
+ expectOutcome: ({ error }) => {
74
+ (0, chai_1.expect)(error === null || error === void 0 ? void 0 : error.message).to.equal("The AI provider didn't answer within 30 s");
75
+ },
76
+ },
77
+ ];
78
+ for (const { name, call, expectOutcome } of calls) {
79
+ it(`gives up at the limit, cancelling the request: ${name}`, () => __awaiter(void 0, void 0, void 0, function* () {
80
+ var _a, _b;
81
+ const { model } = ai.use('anthropic');
82
+ ai.answer('hang');
83
+ const clock = sinon_1.default.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'] });
84
+ const service = new AIService_1.AIService();
85
+ const outcome = outcomeOf(call(service));
86
+ yield clock.tickAsync(AIService_1.AI_CALL_TIMEOUT_MS - 1);
87
+ (0, chai_1.expect)(ai.calls).to.have.length(1);
88
+ (0, chai_1.expect)((_a = ai.calls[0].signal) === null || _a === void 0 ? void 0 : _a.aborted).to.equal(false);
89
+ yield clock.tickAsync(1);
90
+ (0, chai_1.expect)((_b = ai.calls[0].signal) === null || _b === void 0 ? void 0 : _b.aborted).to.equal(true);
91
+ expectOutcome(yield outcome);
92
+ const breaker = CircuitBreaker_1.CIRCUIT_BREAKERS.snapshot().find((b) => b.key === `ai:anthropic:${model}`);
93
+ (0, chai_1.expect)(breaker === null || breaker === void 0 ? void 0 : breaker.consecutiveFailures).to.equal(1);
94
+ }));
95
+ }
96
+ it("Test connection fails at the limit, saying the provider didn't answer", () => __awaiter(void 0, void 0, void 0, function* () {
97
+ var _a;
98
+ ai.use('openai');
99
+ ai.answer('hang');
100
+ const clock = sinon_1.default.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'] });
101
+ const result = new AIService_1.AIService().testConnection({});
102
+ yield clock.tickAsync(AIService_1.AI_CALL_TIMEOUT_MS);
103
+ (0, chai_1.expect)(yield result).to.deep.equal({
104
+ success: false,
105
+ message: "Connection failed: The AI provider didn't answer within 30 s",
106
+ });
107
+ (0, chai_1.expect)((_a = ai.calls[0].signal) === null || _a === void 0 ? void 0 : _a.aborted).to.equal(true);
108
+ }));
109
+ it('a failing findElement is answered within two time limits, after both AI tiers', () => __awaiter(void 0, void 0, void 0, function* () {
110
+ ai.use('gemini');
111
+ ai.answer('hang');
112
+ const clock = sinon_1.default.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'] });
113
+ const driver = {
114
+ getPageSource: () => __awaiter(void 0, void 0, void 0, function* () { return '<hierarchy/>'; }),
115
+ getScreenshot: () => __awaiter(void 0, void 0, void 0, function* () { return SCREEN; }),
116
+ findElement: sinon_1.default.stub().rejects(new Error('no such element')),
117
+ };
118
+ let healed = 'not answered';
119
+ const healing = new HealingOrchestrator_1.HealingOrchestrator({})
120
+ // The visual tier (4), then the LLM tier (5).
121
+ .attemptHealing('sess-time-limit', driver, 'id', 'login', [4, 5])
122
+ .then((result) => (healed = result));
123
+ yield clock.tickAsync(AIService_1.AI_CALL_TIMEOUT_MS);
124
+ (0, chai_1.expect)(ai.calls).to.have.length(2);
125
+ yield clock.tickAsync(AIService_1.AI_CALL_TIMEOUT_MS);
126
+ yield healing;
127
+ (0, chai_1.expect)(healed).to.equal(null);
128
+ (0, chai_1.expect)(ai.calls.map((c) => { var _a; return (_a = c.signal) === null || _a === void 0 ? void 0 : _a.aborted; })).to.deep.equal([true, true]);
129
+ }));
130
+ });