@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
@@ -0,0 +1,213 @@
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 crypto_1 = __importDefault(require("crypto"));
18
+ const fs_1 = __importDefault(require("fs"));
19
+ const os_1 = __importDefault(require("os"));
20
+ const path_1 = __importDefault(require("path"));
21
+ const sharp_1 = __importDefault(require("sharp"));
22
+ const sinon_1 = __importDefault(require("sinon"));
23
+ const tesseract_js_1 = __importDefault(require("tesseract.js"));
24
+ const typedi_1 = require("typedi");
25
+ const package_json_1 = __importDefault(require("../../package.json"));
26
+ const ocrData_1 = require("../../src/services/ocr/ocrData");
27
+ const OcrHealingProvider_1 = require("../../src/services/healing/OcrHealingProvider");
28
+ const OmniVisionService_1 = require("../../src/services/omni-vision/OmniVisionService");
29
+ const container_registration_1 = require("../helpers/container-registration");
30
+ /**
31
+ * tesseract.js downloads its English data (eng.traineddata) from
32
+ * cdn.jsdelivr.net the first time a worker starts, and caches it in the
33
+ * server's working directory. A server with no internet couldn't run OCR at
34
+ * all: the first OCR call never finished, and every later one waited for it.
35
+ * One with internet left a 5 MB eng.traineddata wherever it was started (the
36
+ * home directory, ~/.appium).
37
+ *
38
+ * The data now ships with the plugin, read from there and cached nowhere.
39
+ */
40
+ describe('OCR without the internet', () => {
41
+ // The upstream file, @tesseract.js-data/eng@1.0.0 4.0.0_best_int/eng.traineddata.gz:
42
+ // what tesseract.js 7 downloads by default (LSTM only).
43
+ const UPSTREAM_SHA256 = '45b4cb346724ac1774f1c36f42f182b887bcdb28ebe63e6fff90ac41f3fcff91';
44
+ const tmpDirs = [];
45
+ const tmpDir = () => {
46
+ const dir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'xenon-ocr-'));
47
+ tmpDirs.push(dir);
48
+ return dir;
49
+ };
50
+ afterEach(() => {
51
+ sinon_1.default.restore();
52
+ for (const dir of tmpDirs.splice(0))
53
+ fs_1.default.rmSync(dir, { recursive: true, force: true });
54
+ });
55
+ /** A white image with `text` on it, as a phone's screenshot would have. */
56
+ const imageOf = (text) => (0, sharp_1.default)({ create: { width: 900, height: 220, channels: 3, background: '#ffffff' } })
57
+ .composite([
58
+ {
59
+ input: Buffer.from(`<svg xmlns="http://www.w3.org/2000/svg" width="900" height="220">` +
60
+ `<text x="40" y="140" font-family="Helvetica, Arial, sans-serif" font-size="96" fill="#000">${text}</text>` +
61
+ `</svg>`),
62
+ },
63
+ ])
64
+ .png()
65
+ .toBuffer();
66
+ describe('the English data', () => {
67
+ it('ships with the plugin, byte for byte the file tesseract.js would download', () => {
68
+ (0, chai_1.expect)(ocrData_1.OCR_DATA_FILE).to.equal(path_1.default.join(ocrData_1.OCR_DATA_DIR, 'eng.traineddata.gz'));
69
+ const sha = crypto_1.default.createHash('sha256').update(fs_1.default.readFileSync(ocrData_1.OCR_DATA_FILE)).digest('hex');
70
+ (0, chai_1.expect)(sha).to.equal(UPSTREAM_SHA256);
71
+ (0, chai_1.expect)(ocrData_1.OCR_DATA_SHA256).to.equal(UPSTREAM_SHA256);
72
+ const readme = fs_1.default.readFileSync(path_1.default.join(ocrData_1.OCR_DATA_DIR, 'README.md'), 'utf8');
73
+ (0, chai_1.expect)(readme).to.include(UPSTREAM_SHA256);
74
+ });
75
+ it('is copied into lib/ by the build, which is what the npm package ships', () => {
76
+ const copy = package_json_1.default.scripts['build:copy'];
77
+ (0, chai_1.expect)(copy).to.include('mkdir -p lib/src/services/ocr/vendor');
78
+ (0, chai_1.expect)(copy).to.include('cp src/services/ocr/vendor/*.gz');
79
+ (0, chai_1.expect)(package_json_1.default.files).to.include('lib');
80
+ });
81
+ it('is read from there, never fetched, and cached nowhere', () => {
82
+ const options = (0, ocrData_1.ocrWorkerOptions)();
83
+ (0, chai_1.expect)(options.langPath).to.equal(ocrData_1.OCR_DATA_DIR);
84
+ (0, chai_1.expect)(String(options.langPath)).not.to.match(/^[a-z]+:\/\//i);
85
+ (0, chai_1.expect)(options.cacheMethod).to.equal('none');
86
+ (0, chai_1.expect)(options.gzip).to.equal(true);
87
+ });
88
+ });
89
+ describe('a real OCR worker', () => {
90
+ let cwd;
91
+ beforeEach(() => {
92
+ cwd = process.cwd();
93
+ });
94
+ afterEach(() => {
95
+ process.chdir(cwd);
96
+ });
97
+ it('reads text, and writes nothing into the working directory', () => __awaiter(void 0, void 0, void 0, function* () {
98
+ const workDir = tmpDir();
99
+ process.chdir(workDir);
100
+ const worker = yield (0, ocrData_1.createOcrWorker)();
101
+ try {
102
+ const { data } = yield worker.recognize(yield imageOf('Sign in'));
103
+ (0, chai_1.expect)(data.text).to.match(/sign\s*in/i);
104
+ }
105
+ finally {
106
+ yield worker.terminate();
107
+ }
108
+ (0, chai_1.expect)(fs_1.default.readdirSync(workDir)).to.deep.equal([]);
109
+ }));
110
+ it('fails with a reason, rather than wait for ever, when the data is missing', () => __awaiter(void 0, void 0, void 0, function* () {
111
+ const started = Date.now();
112
+ const failure = yield (0, ocrData_1.createOcrWorker)(tmpDir()).then(() => chai_1.expect.fail('a worker with no data should not start'), (err) => err);
113
+ (0, chai_1.expect)(failure.message).to.include('eng.traineddata.gz');
114
+ (0, chai_1.expect)(Date.now() - started).to.be.below(5000);
115
+ }));
116
+ // Given data it can't load, tesseract.js 7 throws from its own message
117
+ // handler, and the server exits on an uncaught exception: the data is
118
+ // checked before a worker sees it.
119
+ for (const [damage, bytes] of [
120
+ ['not tesseract data at all', () => Buffer.from('not tesseract data')],
121
+ ['cut short', () => fs_1.default.readFileSync(ocrData_1.OCR_DATA_FILE).subarray(0, 100000)],
122
+ ]) {
123
+ it(`fails with a reason, before any worker starts, when the data is ${damage}`, () => __awaiter(void 0, void 0, void 0, function* () {
124
+ const dir = tmpDir();
125
+ fs_1.default.writeFileSync(path_1.default.join(dir, 'eng.traineddata.gz'), bytes());
126
+ const createWorker = sinon_1.default.spy(tesseract_js_1.default, 'createWorker');
127
+ const failure = yield (0, ocrData_1.createOcrWorker)(dir).then(() => chai_1.expect.fail('a worker with damaged data should not start'), (err) => err);
128
+ (0, chai_1.expect)(failure.message).to.include('damaged');
129
+ (0, chai_1.expect)(createWorker.called).to.equal(false);
130
+ }));
131
+ }
132
+ });
133
+ describe('a worker that will not start', () => {
134
+ /** A directory holding a good copy of the data, so this test's memory is its own. */
135
+ const dataDir = () => {
136
+ const dir = tmpDir();
137
+ fs_1.default.copyFileSync(ocrData_1.OCR_DATA_FILE, path_1.default.join(dir, 'eng.traineddata.gz'));
138
+ return dir;
139
+ };
140
+ it('is remembered for a minute: OCR fails at once rather than start another', () => __awaiter(void 0, void 0, void 0, function* () {
141
+ // tesseract.js hands back no worker when its start fails, so its thread
142
+ // can't be ended; starting one per OCR call would leave one per call.
143
+ const clock = sinon_1.default.useFakeTimers({ now: Date.now(), toFake: ['Date'] });
144
+ const createWorker = sinon_1.default
145
+ .stub(tesseract_js_1.default, 'createWorker')
146
+ .callsFake((_langs, _oem, options) => {
147
+ setTimeout(() => options.errorHandler('the OCR engine failed to load'), 5);
148
+ return new Promise(() => undefined);
149
+ });
150
+ const dir = dataDir();
151
+ const reason = (p) => p.then(() => chai_1.expect.fail('the worker should not start'), (err) => err.message);
152
+ (0, chai_1.expect)(yield reason((0, ocrData_1.createOcrWorker)(dir))).to.include('the OCR engine failed to load');
153
+ (0, chai_1.expect)(yield reason((0, ocrData_1.createOcrWorker)(dir))).to.include('the OCR engine failed to load');
154
+ (0, chai_1.expect)(createWorker.callCount).to.equal(1);
155
+ clock.tick(60001);
156
+ yield reason((0, ocrData_1.createOcrWorker)(dir));
157
+ (0, chai_1.expect)(createWorker.callCount).to.equal(2);
158
+ }));
159
+ });
160
+ describe('every place that runs OCR', () => {
161
+ let restoreContainer;
162
+ beforeEach(() => {
163
+ restoreContainer = (0, container_registration_1.saveRegistrations)(OmniVisionService_1.OmniVisionService);
164
+ });
165
+ afterEach(() => {
166
+ restoreContainer();
167
+ });
168
+ it("the OCR healing tier reads the shipped data, with Omni-Vision's worker", () => __awaiter(void 0, void 0, void 0, function* () {
169
+ // The tier reads the screenshot through Omni-Vision (findTextInScreenshot),
170
+ // so it shares that worker and leaves it running for the next read.
171
+ const worker = {
172
+ setParameters: sinon_1.default.stub().resolves(),
173
+ recognize: sinon_1.default.stub().resolves({ data: { text: '', words: [], hocr: '', tsv: '' } }),
174
+ terminate: sinon_1.default.stub().resolves(),
175
+ };
176
+ const createWorker = sinon_1.default.stub(tesseract_js_1.default, 'createWorker').resolves(worker);
177
+ typedi_1.Container.set(OmniVisionService_1.OmniVisionService, new OmniVisionService_1.OmniVisionService());
178
+ yield new OcrHealingProvider_1.OcrHealingProvider().heal({
179
+ screenshotBase64: (yield imageOf('Login')).toString('base64'),
180
+ selector: "//*[@text='Login']",
181
+ strategy: 'xpath',
182
+ driver: {},
183
+ });
184
+ (0, chai_1.expect)(createWorker.calledOnce).to.equal(true);
185
+ (0, chai_1.expect)(createWorker.firstCall.args[0]).to.equal('eng');
186
+ (0, chai_1.expect)(createWorker.firstCall.args[2]).to.include({
187
+ langPath: ocrData_1.OCR_DATA_DIR,
188
+ cacheMethod: 'none',
189
+ });
190
+ (0, chai_1.expect)(worker.recognize.calledOnce).to.equal(true);
191
+ (0, chai_1.expect)(worker.terminate.called).to.equal(false);
192
+ }));
193
+ it("Omni-Vision's worker reads the shipped data", () => __awaiter(void 0, void 0, void 0, function* () {
194
+ const worker = {
195
+ setParameters: sinon_1.default.stub().resolves(),
196
+ recognize: sinon_1.default.stub().resolves({ data: { text: '', words: [], hocr: '', tsv: '' } }),
197
+ terminate: sinon_1.default.stub().resolves(),
198
+ };
199
+ const createWorker = sinon_1.default.stub(tesseract_js_1.default, 'createWorker').resolves(worker);
200
+ typedi_1.Container.set(OmniVisionService_1.OmniVisionService, new OmniVisionService_1.OmniVisionService());
201
+ const driver = {
202
+ getScreenshot: sinon_1.default.stub().resolves((yield imageOf('Login')).toString('base64')),
203
+ };
204
+ yield typedi_1.Container.get(OmniVisionService_1.OmniVisionService).findByText(driver, 'Login');
205
+ (0, chai_1.expect)(createWorker.calledOnce).to.equal(true);
206
+ (0, chai_1.expect)(createWorker.firstCall.args[0]).to.equal('eng');
207
+ (0, chai_1.expect)(createWorker.firstCall.args[2]).to.include({
208
+ langPath: ocrData_1.OCR_DATA_DIR,
209
+ cacheMethod: 'none',
210
+ });
211
+ }));
212
+ });
213
+ });
@@ -0,0 +1,301 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
36
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
37
+ return new (P || (P = Promise))(function (resolve, reject) {
38
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
39
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
40
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
41
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
42
+ });
43
+ };
44
+ var __importDefault = (this && this.__importDefault) || function (mod) {
45
+ return (mod && mod.__esModule) ? mod : { "default": mod };
46
+ };
47
+ Object.defineProperty(exports, "__esModule", { value: true });
48
+ require("reflect-metadata");
49
+ const chai_1 = require("chai");
50
+ const sinon_1 = __importDefault(require("sinon"));
51
+ const typedi_1 = require("typedi");
52
+ const plugin_1 = require("../../src/plugin");
53
+ const NodeDevices_1 = __importDefault(require("../../src/device-managers/NodeDevices"));
54
+ const DeviceService = __importStar(require("../../src/data-service/device-service"));
55
+ const ActiveLeases = __importStar(require("../../src/services/lease/activeLeases"));
56
+ const device_utils_1 = require("../../src/device-utils");
57
+ const CommandInterceptor_1 = require("../../src/interceptors/CommandInterceptor");
58
+ const HealingOrchestrator_1 = require("../../src/services/healing/HealingOrchestrator");
59
+ const types_1 = require("../../src/services/healing/types");
60
+ const OmniVisionService_1 = require("../../src/services/omni-vision/OmniVisionService");
61
+ const AutowaitService_1 = require("../../src/services/autowait/AutowaitService");
62
+ const SessionLifecycleService_1 = require("../../src/services/SessionLifecycleService");
63
+ const SessionMetricsService_1 = require("../../src/services/metrics/SessionMetricsService");
64
+ const OrphanSweeper_1 = require("../../src/services/OrphanSweeper");
65
+ const PhoneNetworkRestore_1 = require("../../src/services/network/PhoneNetworkRestore");
66
+ const SelfHealingSwitch_1 = require("../../src/services/settings/SelfHealingSwitch");
67
+ const AIService_1 = require("../../src/services/AIService");
68
+ const event_manager_1 = require("../../src/dashboard/event-manager");
69
+ const IPluginArgs_1 = require("../../src/interfaces/IPluginArgs");
70
+ const container_registration_1 = require("../helpers/container-registration");
71
+ const scratch_database_1 = require("../helpers/scratch-database");
72
+ /**
73
+ * An element Xenon finds in a screenshot (a `-custom:ai-*` find, or an OCR or
74
+ * Visual AI heal) is a box on the screen it remembers by id. Through 2.14 it
75
+ * remembered every one, from every session, until the server restarted:
76
+ * device control's locator test and an icon `smartTap` added one per call
77
+ * that nothing ever read, and any session could act on another's. Now an
78
+ * element belongs to the session that found it and goes when that session
79
+ * ends, and the store has a size limit.
80
+ *
81
+ * OCR, the AI provider and the drivers are stubbed: no Tesseract, no AI call,
82
+ * no phone.
83
+ */
84
+ const W3C = 'element-6066-11e4-a52e-4f735466cecf';
85
+ const WORDS = [
86
+ { text: 'Sign', confidence: 90, bbox: { x0: 100, y0: 200, x1: 150, y1: 224 } },
87
+ { text: 'in', confidence: 90, bbox: { x0: 158, y0: 200, x1: 180, y1: 224 } },
88
+ ];
89
+ const box = (id) => ({
90
+ id,
91
+ rect: { x: 0, y: 0, width: 10, height: 10 },
92
+ confidence: 0.9,
93
+ });
94
+ describe('Elements found in a screenshot belong to their session', () => {
95
+ (0, scratch_database_1.useScratchDatabase)();
96
+ let restoreContainer;
97
+ let omni;
98
+ const driverFor = (sessionId) => ({
99
+ sessionId,
100
+ caps: { platformName: 'Android' },
101
+ getScreenshot: sinon_1.default.stub().resolves('aGVsbG8='),
102
+ performActions: sinon_1.default.stub().resolves(),
103
+ releaseActions: sinon_1.default.stub().resolves(),
104
+ });
105
+ beforeEach(() => {
106
+ restoreContainer = (0, container_registration_1.saveRegistrations)(OmniVisionService_1.OmniVisionService, HealingOrchestrator_1.HealingOrchestrator, SelfHealingSwitch_1.SelfHealingSwitch);
107
+ typedi_1.Container.set(OmniVisionService_1.OmniVisionService, new OmniVisionService_1.OmniVisionService());
108
+ typedi_1.Container.set(SelfHealingSwitch_1.SelfHealingSwitch, new SelfHealingSwitch_1.SelfHealingSwitch());
109
+ omni = typedi_1.Container.get(OmniVisionService_1.OmniVisionService);
110
+ sinon_1.default.stub(omni, 'performOcr').resolves({ text: '', words: WORDS });
111
+ sinon_1.default.stub(event_manager_1.DASHBORD_EVENT_MANAGER, 'afterSessionCommand').resolves();
112
+ });
113
+ afterEach(() => {
114
+ restoreContainer();
115
+ sinon_1.default.restore();
116
+ });
117
+ const run = (driver, command, args, next = () => null) => typedi_1.Container.get(CommandInterceptor_1.CommandInterceptor).handle(() => __awaiter(void 0, void 0, void 0, function* () { return next(); }), driver, command, [...args, driver.sessionId], Object.assign({}, IPluginArgs_1.DefaultPluginArgs), false);
118
+ const failure = (promise) => __awaiter(void 0, void 0, void 0, function* () {
119
+ try {
120
+ yield promise;
121
+ }
122
+ catch (err) {
123
+ return err;
124
+ }
125
+ throw new Error('expected the command to fail');
126
+ });
127
+ const held = () => omni.elements.size;
128
+ it("answers another session's command on it with no such element", () => __awaiter(void 0, void 0, void 0, function* () {
129
+ const a = driverFor('sess-a');
130
+ const b = driverFor('sess-b');
131
+ const found = yield run(a, 'findElement', ['-custom:ai-text', 'Sign in']);
132
+ const err = yield failure(run(b, 'click', [found[W3C]]));
133
+ (0, chai_1.expect)(err.error).to.equal('no such element');
134
+ (0, chai_1.expect)(b.performActions.called, "tapped on the other session's phone").to.equal(false);
135
+ yield run(a, 'click', [found[W3C]]);
136
+ (0, chai_1.expect)(a.performActions.calledOnce).to.equal(true);
137
+ }));
138
+ it('keeps a healed element for the session it healed in', () => __awaiter(void 0, void 0, void 0, function* () {
139
+ const attemptHealing = sinon_1.default.stub().resolves({
140
+ id: 'healed_ocr_1_abc',
141
+ tier: types_1.HealingTier.TIER_3_LOCAL_OCR,
142
+ confidence: 0.9,
143
+ originalSelector: "//*[@text='Sign in']",
144
+ originalStrategy: 'xpath',
145
+ recommendedSelector: 'ocr:text="Sign in"',
146
+ recommendedStrategy: 'xenon:visual',
147
+ text: 'Sign in',
148
+ rect: { x: 100, y: 200, width: 80, height: 24 },
149
+ });
150
+ typedi_1.Container.set(HealingOrchestrator_1.HealingOrchestrator, { attemptHealing });
151
+ const a = driverFor('sess-a');
152
+ yield run(a, 'findElement', ['xpath', "//*[@text='Sign in']"], () => {
153
+ throw Object.assign(new Error('NoSuchElement'), { name: 'NoSuchElementError' });
154
+ });
155
+ (0, chai_1.expect)(yield run(a, 'getText', ['healed_ocr_1_abc'])).to.equal('Sign in');
156
+ const err = yield failure(run(driverFor('sess-b'), 'getText', ['healed_ocr_1_abc']));
157
+ (0, chai_1.expect)(err.error).to.equal('no such element');
158
+ }));
159
+ it("keeps nothing for device control's locator test", () => __awaiter(void 0, void 0, void 0, function* () {
160
+ // As POST /control/<udid>/test-locator calls it: the ids are only shown.
161
+ const manual = { sessionId: 'manual_R5CT', getScreenshot: sinon_1.default.stub().resolves('aGVsbG8=') };
162
+ const found = yield omni.findByText(manual, 'Sign in', { throwOnError: true });
163
+ (0, chai_1.expect)(found).to.have.length(1);
164
+ (0, chai_1.expect)(held()).to.equal(0);
165
+ }));
166
+ it('keeps nothing for an icon smartTap, which taps at once', () => __awaiter(void 0, void 0, void 0, function* () {
167
+ sinon_1.default.stub(AIService_1.AI_SERVICE, 'visualFind').resolves({ x: 50, y: 50 });
168
+ const result = yield omni.omniClickByIcon(driverFor('sess-a'), { icon: 'the cart' });
169
+ (0, chai_1.expect)(result.clicked).to.equal(true);
170
+ (0, chai_1.expect)(held()).to.equal(0);
171
+ }));
172
+ it('keeps the newest 1,000 for a session', () => {
173
+ for (let i = 0; i < 1001; i++)
174
+ omni.remember('sess-a', box(`omni_ocr_${i}`));
175
+ (0, chai_1.expect)(omni.getVirtualElement('omni_ocr_0', 'sess-a')).to.equal(undefined);
176
+ (0, chai_1.expect)(omni.getVirtualElement('omni_ocr_1', 'sess-a')).to.not.equal(undefined);
177
+ (0, chai_1.expect)(omni.getVirtualElement('omni_ocr_1000', 'sess-a')).to.not.equal(undefined);
178
+ });
179
+ it('keeps the newest 10,000 in all', () => {
180
+ for (let s = 0; s < 11; s++) {
181
+ for (let i = 0; i < 1000; i++)
182
+ omni.remember(`sess-${s}`, box(`omni_ocr_${s}_${i}`));
183
+ }
184
+ (0, chai_1.expect)(held()).to.equal(10000);
185
+ (0, chai_1.expect)(omni.getVirtualElement('omni_ocr_0_999', 'sess-0')).to.equal(undefined);
186
+ (0, chai_1.expect)(omni.getVirtualElement('omni_ocr_1_0', 'sess-1')).to.not.equal(undefined);
187
+ });
188
+ it('gives icon matches ids of their own, as text matches have', () => __awaiter(void 0, void 0, void 0, function* () {
189
+ sinon_1.default.stub(Date, 'now').returns(1700000000000);
190
+ sinon_1.default.stub(AIService_1.AI_SERVICE, 'visualFind').resolves({ x: 50, y: 50 });
191
+ const a = driverFor('sess-a');
192
+ const first = yield omni.findByIcon(a, 'the cart');
193
+ const second = yield omni.findByIcon(a, 'the cart');
194
+ (0, chai_1.expect)(first === null || first === void 0 ? void 0 : first.id).to.not.equal(second === null || second === void 0 ? void 0 : second.id);
195
+ }));
196
+ });
197
+ /**
198
+ * Everything a session leaves in this server's memory for its commands goes
199
+ * when the session ends, however it ends: its virtual elements, and its
200
+ * autowait settings (`xenon: setAutowaitProperties`). Those were meant to be
201
+ * cleared by the interceptor on `deleteSession`, but XenonPlugin answers
202
+ * `deleteSession` itself, so Appium never hands that command to the
203
+ * interceptor: they were kept for good.
204
+ */
205
+ describe("A session's virtual elements and autowait settings go when it ends", function () {
206
+ this.timeout(30000);
207
+ const scratch = (0, scratch_database_1.useScratchDatabase)();
208
+ let restoreRegs;
209
+ let omni;
210
+ const seed = (sessionId) => {
211
+ omni.remember(sessionId, box(`omni_ocr_${sessionId}`));
212
+ typedi_1.Container.get(AutowaitService_1.AutowaitService).setProps(sessionId, { timeoutMs: 1234 });
213
+ };
214
+ const expectForgotten = (sessionId) => {
215
+ (0, chai_1.expect)(omni.getVirtualElement(`omni_ocr_${sessionId}`, sessionId), 'element').to.equal(undefined);
216
+ (0, chai_1.expect)(typedi_1.Container.get(AutowaitService_1.AutowaitService).getProps(sessionId, Object.assign({}, IPluginArgs_1.DefaultPluginArgs)).timeoutMs, 'autowait timeout').to.equal(IPluginArgs_1.DefaultPluginArgs.autowait.timeoutMs);
217
+ };
218
+ beforeEach(() => {
219
+ restoreRegs = (0, container_registration_1.saveRegistrations)(OmniVisionService_1.OmniVisionService, AutowaitService_1.AutowaitService, PhoneNetworkRestore_1.PhoneNetworkRestore, SessionMetricsService_1.SessionMetricsService);
220
+ typedi_1.Container.set(OmniVisionService_1.OmniVisionService, new OmniVisionService_1.OmniVisionService());
221
+ typedi_1.Container.set(AutowaitService_1.AutowaitService, new AutowaitService_1.AutowaitService());
222
+ omni = typedi_1.Container.get(OmniVisionService_1.OmniVisionService);
223
+ typedi_1.Container.set(PhoneNetworkRestore_1.PhoneNetworkRestore, {
224
+ restoreSession: sinon_1.default.stub().resolves(),
225
+ restoreAll: sinon_1.default.stub().resolves(),
226
+ });
227
+ typedi_1.Container.set(SessionMetricsService_1.SessionMetricsService, { stop: sinon_1.default.stub().resolves() });
228
+ sinon_1.default.stub(DeviceService, 'releaseSessionDevices').resolves();
229
+ sinon_1.default.stub(event_manager_1.DASHBORD_EVENT_MANAGER, 'onSessionStopped').resolves();
230
+ });
231
+ afterEach(() => {
232
+ sinon_1.default.restore();
233
+ restoreRegs();
234
+ });
235
+ it('when the test deletes the session', () => __awaiter(this, void 0, void 0, function* () {
236
+ seed('s-deleted');
237
+ yield typedi_1.Container.get(SessionLifecycleService_1.SessionLifecycleService).deleteSession(() => __awaiter(this, void 0, void 0, function* () { return null; }), 's-deleted');
238
+ expectForgotten('s-deleted');
239
+ }));
240
+ it('when Appium ends the session itself (new-command timeout)', () => __awaiter(this, void 0, void 0, function* () {
241
+ seed('s-timeout');
242
+ const plugin = { pluginArgs: {}, xenonLog: { withSession: () => ({ info() { } }) } };
243
+ yield plugin_1.XenonPlugin.prototype.onUnexpectedShutdown.call(plugin, { sessionId: 's-timeout', caps: { udid: 'R5CT' } }, new Error('New command timeout'));
244
+ expectForgotten('s-timeout');
245
+ }));
246
+ it('when Appium ends the session on a node', () => __awaiter(this, void 0, void 0, function* () {
247
+ seed('s-node');
248
+ sinon_1.default.stub(NodeDevices_1.default.prototype, 'unblockDevice').resolves(undefined);
249
+ const plugin = { pluginArgs: { hub: 'http://hub:4723' } };
250
+ yield plugin_1.XenonPlugin.prototype.onUnexpectedShutdown.call(plugin, { sessionId: 's-node', caps: {} }, null);
251
+ expectForgotten('s-node');
252
+ }));
253
+ it("when Xenon's idle check releases the phone", () => __awaiter(this, void 0, void 0, function* () {
254
+ seed('s-idle');
255
+ const idle = {
256
+ udid: 'R5CT',
257
+ host: 'http://127.0.0.1:4723',
258
+ busy: true,
259
+ userBlocked: false,
260
+ session_id: 's-idle',
261
+ claimSessionId: 's-idle',
262
+ lastCmdExecutedAt: Date.now() - 10 * 60 * 1000,
263
+ };
264
+ sinon_1.default.stub(DeviceService, 'getAllDevices').resolves([idle]);
265
+ sinon_1.default.stub(ActiveLeases, 'activeLeasesByDevice').resolves(new Map());
266
+ sinon_1.default.stub(DeviceService, 'releaseSessionDevice').resolves(true);
267
+ yield (0, device_utils_1.releaseBlockedDevices)(60);
268
+ expectForgotten('s-idle');
269
+ }));
270
+ it('when a graceful shutdown drains the session', () => __awaiter(this, void 0, void 0, function* () {
271
+ seed('s-down');
272
+ yield typedi_1.Container.get(SessionLifecycleService_1.SessionLifecycleService).stopSessionForShutdown('s-down', 'shutdown');
273
+ expectForgotten('s-down');
274
+ }));
275
+ it("when the session's heartbeat goes stale", () => __awaiter(this, void 0, void 0, function* () {
276
+ seed('s-stale');
277
+ yield scratch.db.session.deleteMany({});
278
+ yield scratch.db.session.create({
279
+ data: {
280
+ id: 's-stale',
281
+ status: 'running',
282
+ desired_capabilities: '{}',
283
+ session_capabilities: '{}',
284
+ node_id: 'n',
285
+ has_live_video: false,
286
+ device_udid: 'R5CT',
287
+ device_platform: 'android',
288
+ device_version: '14',
289
+ last_heartbeat_at: new Date(Date.now() - 60 * 60 * 1000),
290
+ },
291
+ });
292
+ yield new OrphanSweeper_1.OrphanSweeper().sweep({ heartbeatIntervalMs: 1000 });
293
+ expectForgotten('s-stale');
294
+ }));
295
+ it("leaves other sessions' elements alone", () => __awaiter(this, void 0, void 0, function* () {
296
+ seed('s-ends');
297
+ seed('s-stays');
298
+ yield typedi_1.Container.get(SessionLifecycleService_1.SessionLifecycleService).deleteSession(() => __awaiter(this, void 0, void 0, function* () { return null; }), 's-ends');
299
+ (0, chai_1.expect)(omni.getVirtualElement('omni_ocr_s-stays', 's-stays')).to.not.equal(undefined);
300
+ }));
301
+ });
@@ -92,10 +92,8 @@ describe('Omni-Vision taps in points on iOS', () => {
92
92
  (0, chai_1.expect)(tappedAt(ios)).to.deep.equal({ x: 5, y: 11 });
93
93
  }));
94
94
  it('gives the -custom:ai-text locator an element in points on iOS', () => __awaiter(void 0, void 0, void 0, function* () {
95
- var _a;
96
95
  const [found] = yield service.findByText(ios, 'OK');
97
96
  (0, chai_1.expect)(found.rect).to.deep.equal({ x: 4, y: 10, width: 2, height: 2 });
98
- (0, chai_1.expect)((_a = service.getVirtualElement(found.id)) === null || _a === void 0 ? void 0 : _a.rect).to.deep.equal(found.rect);
99
97
  }));
100
98
  it("doesn't tap on iOS when the window size can't be read", () => __awaiter(void 0, void 0, void 0, function* () {
101
99
  ios.getWindowRect.rejects(new Error('WDA is not running'));
@@ -70,16 +70,14 @@ describe('OmniVisionService Unit Tests', () => {
70
70
  (0, chai_1.expect)(result === null || result === void 0 ? void 0 : result.id).to.contain('omni_ai_');
71
71
  (0, chai_1.expect)(result === null || result === void 0 ? void 0 : result.rect).to.deep.equal({ x: 30, y: 30, width: 40, height: 40 });
72
72
  }));
73
- it('getVirtualElement should retrieve stored elements', () => __awaiter(void 0, void 0, void 0, function* () {
73
+ it('getVirtualElement gives back an element kept for its session, to that session only', () => __awaiter(void 0, void 0, void 0, function* () {
74
74
  sinon_1.default.stub(AIService_1.AI_SERVICE, 'visualFind').resolves({ x: 50, y: 50 });
75
75
  const created = yield omniService.findByIcon(mockDriver, 'back');
76
- if (created) {
77
- const retrieved = omniService.getVirtualElement(created.id);
78
- (0, chai_1.expect)(retrieved).to.deep.equal(created);
79
- }
80
- else {
76
+ if (!created)
81
77
  throw new Error('Failed to create mock virtual element');
82
- }
78
+ omniService.remember('session-1', created);
79
+ (0, chai_1.expect)(omniService.getVirtualElement(created.id, 'session-1')).to.deep.equal(created);
80
+ (0, chai_1.expect)(omniService.getVirtualElement(created.id, 'session-2')).to.equal(undefined);
83
81
  }));
84
82
  it('analyzeScreen should combine OCR and AI insights', () => __awaiter(void 0, void 0, void 0, function* () {
85
83
  const mockWorker = {
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const chai_1 = require("chai");
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const root = path_1.default.join(__dirname, '..', '..');
10
+ const readJson = (file) => JSON.parse(fs_1.default.readFileSync(file, 'utf8'));
11
+ const pkg = readJson(path_1.default.join(root, 'package.json'));
12
+ const lock = readJson(path_1.default.join(root, 'package-lock.json'));
13
+ const appium = readJson(require.resolve('appium/package.json'));
14
+ /**
15
+ * The plugin runs inside Appium, so it supports the Node versions Appium 3
16
+ * does and no others. `engines` used to say `^14.17.0 || ^16.13.0 || >=18.0.0`
17
+ * while every Appium 3 release needs 20.19, 22.12 or 24 and later.
18
+ */
19
+ describe('package.json engines', () => {
20
+ it("are the installed Appium 3's, the Appium the plugin is built and tested against", () => {
21
+ (0, chai_1.expect)(appium.version).to.match(/^3\./);
22
+ (0, chai_1.expect)(pkg.engines).to.deep.equal({ node: appium.engines.node, npm: appium.engines.npm });
23
+ });
24
+ it('match the copy in package-lock.json', () => {
25
+ (0, chai_1.expect)(lock.packages[''].engines).to.deep.equal(pkg.engines);
26
+ });
27
+ });
@@ -3,8 +3,16 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const chai_1 = require("chai");
4
4
  const passwordResetMode_1 = require("../../src/services/passwordResetMode");
5
5
  describe('passwordResetMode', () => {
6
- it('is email when an SMTP URL is configured', () => {
7
- (0, chai_1.expect)((0, passwordResetMode_1.passwordResetMode)({ smtpUrl: 'smtp://mail.example.com:587' })).to.equal('email');
6
+ it('is email when an SMTP URL and XENON_PUBLIC_URL are configured', () => {
7
+ (0, chai_1.expect)((0, passwordResetMode_1.passwordResetMode)({
8
+ smtpUrl: 'smtp://mail.example.com:587',
9
+ publicUrl: 'https://xenon.example.com',
10
+ })).to.equal('email');
11
+ });
12
+ // The emailed link points at XENON_PUBLIC_URL, never at the request's Host:
13
+ // without it Xenon emails no link, so the form must not promise one.
14
+ it('is admin with SMTP but no XENON_PUBLIC_URL', () => {
15
+ (0, chai_1.expect)((0, passwordResetMode_1.passwordResetMode)({ smtpUrl: 'smtp://mail.example.com:587' })).to.equal('admin');
8
16
  });
9
17
  it('is admin without SMTP, even though the log fallback defaults on', () => {
10
18
  // The fallback writes the link to the server log, which the person who