@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,207 @@
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 event_manager_1 = require("../../src/dashboard/event-manager");
53
+ const assets = __importStar(require("../../src/dashboard/asset-manager"));
54
+ const sessionService = __importStar(require("../../src/dashboard/services/session-service"));
55
+ const SessionManager_1 = require("../../src/sessions/SessionManager");
56
+ const SessionMetricsService_1 = require("../../src/services/metrics/SessionMetricsService");
57
+ const SessionDeviceLogs_1 = require("../../src/services/logcat/SessionDeviceLogs");
58
+ const SessionLifecycleService_1 = require("../../src/services/SessionLifecycleService");
59
+ const SocketServer_1 = require("../../src/services/SocketServer");
60
+ const TracingService_1 = require("../../src/services/TracingService");
61
+ const MetricsService_1 = require("../../src/services/MetricsService");
62
+ const device_store_1 = require("../../src/data-service/device-store");
63
+ const PluginContext_1 = require("../../src/PluginContext");
64
+ const IPluginArgs_1 = require("../../src/interfaces/IPluginArgs");
65
+ const pendingSessions = __importStar(require("../../src/data-service/pending-sessions-service"));
66
+ const deviceService = __importStar(require("../../src/data-service/device-service"));
67
+ const deviceUtils = __importStar(require("../../src/device-utils"));
68
+ const config_1 = require("../../src/config");
69
+ const prisma_1 = require("../../src/prisma");
70
+ const container_registration_1 = require("../helpers/container-registration");
71
+ const loki_stores_1 = require("../helpers/loki-stores");
72
+ /**
73
+ * A session's device log is recorded from about when its phone was given to
74
+ * it, once the session's row exists (the lines point at it), and written to
75
+ * the end however the session ends: every ending reaches onSessionStopped.
76
+ */
77
+ describe('EventManager: an Android session’s device log', () => {
78
+ let restore;
79
+ let deviceLogs;
80
+ let order;
81
+ beforeEach(() => {
82
+ restore = (0, container_registration_1.saveRegistrations)(SessionDeviceLogs_1.SessionDeviceLogs, SessionMetricsService_1.SessionMetricsService, SocketServer_1.SocketServer);
83
+ order = [];
84
+ deviceLogs = {
85
+ start: sinon_1.default.stub().callsFake(() => __awaiter(void 0, void 0, void 0, function* () { return order.push('device logs'); })),
86
+ stop: sinon_1.default.stub().resolves(),
87
+ };
88
+ typedi_1.Container.set(SessionDeviceLogs_1.SessionDeviceLogs, deviceLogs);
89
+ typedi_1.Container.set(SessionMetricsService_1.SessionMetricsService, {
90
+ appliesTo: () => false,
91
+ start: sinon_1.default.stub(),
92
+ stop: sinon_1.default.stub().resolves(),
93
+ });
94
+ typedi_1.Container.set(SocketServer_1.SocketServer, {
95
+ emitToDashboard: sinon_1.default.stub(),
96
+ emitToDashboardForDevices: sinon_1.default.stub().resolves(),
97
+ hasScopedDashboard: sinon_1.default.stub().returns(false),
98
+ });
99
+ });
100
+ afterEach(() => {
101
+ sinon_1.default.restore();
102
+ restore();
103
+ });
104
+ it("starts once the session's row is written, from when the phone was given to it", () => __awaiter(void 0, void 0, void 0, function* () {
105
+ sinon_1.default.stub(assets, 'prepareDirectory');
106
+ sinon_1.default.stub(sessionService, 'getOrCreateNewBuild').resolves({ id: 'b-logs' });
107
+ sinon_1.default.stub(TracingService_1.TracingService.prototype, 'getTraceId').returns('t-1');
108
+ sinon_1.default.stub(MetricsService_1.MetricsService.prototype, 'incrementSessionStart');
109
+ sinon_1.default.stub(prisma_1.prisma.session, 'create').callsFake(() => __awaiter(void 0, void 0, void 0, function* () {
110
+ order.push('row');
111
+ return {};
112
+ }));
113
+ const session = {
114
+ getId: () => 's-logs-1',
115
+ getCapabilities: () => ({ platformName: 'Android' }),
116
+ getLiveVideoUrl: () => null,
117
+ apiKeyId: null,
118
+ userId: null,
119
+ allocatedAt: 1700000000000,
120
+ };
121
+ const device = { udid: 'phone-l', platform: 'android', host: 'h', name: 'S9', sdk: '10' };
122
+ yield event_manager_1.DASHBORD_EVENT_MANAGER.onSessionStarted({}, session, device);
123
+ (0, chai_1.expect)(order).to.deep.equal(['row', 'device logs']);
124
+ (0, chai_1.expect)(deviceLogs.start.firstCall.args[0]).to.deep.equal({
125
+ sessionId: 's-logs-1',
126
+ device,
127
+ since: 1700000000000,
128
+ });
129
+ }));
130
+ it('writes it to the end when the session stops, even one no longer in memory', () => __awaiter(void 0, void 0, void 0, function* () {
131
+ sinon_1.default.stub(SessionManager_1.SESSION_MANAGER, 'getSession').returns(undefined);
132
+ sinon_1.default.stub(device_store_1.DeviceStoreFactory, 'getStore').returns({ getDevices: () => __awaiter(void 0, void 0, void 0, function* () { return []; }) });
133
+ sinon_1.default
134
+ .stub(prisma_1.prisma.session, 'findFirst')
135
+ .resolves({ id: 's-logs-2', status: 'running', device_udid: 'phone-l' });
136
+ sinon_1.default.stub(prisma_1.prisma.session, 'update').resolves({});
137
+ sinon_1.default.stub(prisma_1.prisma.sessionLog, 'findFirst').resolves(null);
138
+ let statusWritten = false;
139
+ prisma_1.prisma.session.update.callsFake(() => __awaiter(void 0, void 0, void 0, function* () {
140
+ statusWritten = true;
141
+ return {};
142
+ }));
143
+ deviceLogs.stop.callsFake(() => __awaiter(void 0, void 0, void 0, function* () {
144
+ // The session page reads the device log again once the session has
145
+ // ended: by then every line is written.
146
+ (0, chai_1.expect)(statusWritten, 'the session was marked ended first').to.equal(false);
147
+ }));
148
+ yield event_manager_1.DASHBORD_EVENT_MANAGER.onSessionStopped('s-logs-2');
149
+ (0, chai_1.expect)(deviceLogs.stop.calledOnceWithExactly('s-logs-2')).to.equal(true);
150
+ (0, chai_1.expect)(statusWritten).to.equal(true);
151
+ }));
152
+ });
153
+ describe('createSession: when the phone was given to the session', () => {
154
+ (0, loki_stores_1.useLokiStores)();
155
+ let context;
156
+ let saved;
157
+ let authDisabledBefore;
158
+ beforeEach(() => {
159
+ authDisabledBefore = config_1.config.authDisabled;
160
+ config_1.config.authDisabled = true;
161
+ context = typedi_1.Container.get(PluginContext_1.PluginContext);
162
+ saved = { pluginArgs: context.pluginArgs, nodeId: context.nodeId };
163
+ context.pluginArgs = Object.assign({}, IPluginArgs_1.DefaultPluginArgs, { bindHostOrIp: '127.0.0.1' });
164
+ context.nodeId = 'node-here';
165
+ });
166
+ afterEach(() => {
167
+ config_1.config.authDisabled = authDisabledBefore;
168
+ Object.assign(context, saved);
169
+ sinon_1.default.restore();
170
+ });
171
+ it('is noted on the session, between the allocation and the driver starting', () => __awaiter(void 0, void 0, void 0, function* () {
172
+ const device = {
173
+ udid: 'u1',
174
+ host: 'h1',
175
+ platform: 'android',
176
+ nodeId: 'node-here',
177
+ teamId: null,
178
+ };
179
+ let allocatedBy = 0;
180
+ let driverAt = 0;
181
+ sinon_1.default.stub(pendingSessions, 'addNewPendingSession').resolves();
182
+ sinon_1.default.stub(pendingSessions, 'removePendingSession').resolves();
183
+ sinon_1.default.stub(deviceService, 'updateDeviceProgress').resolves();
184
+ sinon_1.default.stub(deviceService, 'updatedAllocatedDevice').resolves();
185
+ sinon_1.default.stub(deviceUtils, 'allocateDeviceForSession').callsFake(() => __awaiter(void 0, void 0, void 0, function* () {
186
+ allocatedBy = Date.now();
187
+ return device;
188
+ }));
189
+ const svc = new SessionLifecycleService_1.SessionLifecycleService();
190
+ const instance = {};
191
+ sinon_1.default.stub(svc, 'createSessionInstance').returns(instance);
192
+ sinon_1.default.stub(svc, 'applyPostSessionLogic').resolves();
193
+ const next = sinon_1.default.stub().callsFake(() => __awaiter(void 0, void 0, void 0, function* () {
194
+ driverAt = Date.now();
195
+ // A slow driver start: the session's log still counts from before it.
196
+ yield new Promise((r) => setTimeout(r, 20));
197
+ return { value: ['s-alloc', { platformName: 'Android' }] };
198
+ }));
199
+ yield svc.createSession(next, {}, {
200
+ alwaysMatch: { platformName: 'Android', 'appium:automationName': 'UiAutomator2' },
201
+ firstMatch: [{}],
202
+ });
203
+ (0, chai_1.expect)(instance.allocatedAt).to.be.a('number');
204
+ (0, chai_1.expect)(instance.allocatedAt).to.be.at.least(allocatedBy);
205
+ (0, chai_1.expect)(instance.allocatedAt).to.be.at.most(driverAt);
206
+ }));
207
+ });
@@ -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 sinon_1 = __importDefault(require("sinon"));
18
+ const express_1 = __importDefault(require("express"));
19
+ const loopbackRequest_1 = __importDefault(require("../helpers/loopbackRequest"));
20
+ const index_1 = require("../../src/app/index");
21
+ const IPluginArgs_1 = require("../../src/interfaces/IPluginArgs");
22
+ const InternalHttpClient_1 = require("../../src/InternalHttpClient");
23
+ const config_1 = require("../../src/config");
24
+ const device_store_1 = require("../../src/data-service/device-store");
25
+ const dashboardPluginLink_1 = require("../../src/app/dashboardPluginLink");
26
+ const grid_1 = __importDefault(require("../../src/app/routers/grid"));
27
+ // The store grid.ts took when it was loaded (it keeps the one it got then).
28
+ const gridStore = device_store_1.DeviceStoreFactory.getStore();
29
+ /**
30
+ * Where Xenon looks for the appium-dashboard-plugin, and the `dashboard_link`
31
+ * it hands back with each device.
32
+ *
33
+ * Through 2.14 both came from the Host header of the first /xenon/api request
34
+ * after startup, before the login (a GET /xenon/api/health was enough):
35
+ * Xenon sent a GET to `<that host>/dashboard/api/ping`, and if the answer had
36
+ * `pong`, kept `<that host>/dashboard` for the life of the process. From then
37
+ * on every device list fetched `<that host>/dashboard/api/sessions`, and gave
38
+ * every user a `dashboard_link` to that host. A forged Host made Xenon call a
39
+ * server of the sender's choosing (blind SSRF) and point everyone at it.
40
+ *
41
+ * Both now come from configuration: the plugin is reached on this server's own
42
+ * address and port, and the link is `/dashboard` under XENON_PUBLIC_URL, or a
43
+ * path on the server the dashboard is open on.
44
+ */
45
+ describe('the dashboard-plugin link never comes from a request', () => {
46
+ const EVIL = 'attacker.example:8080';
47
+ let calls;
48
+ let authDisabledBefore;
49
+ let publicUrlBefore;
50
+ beforeEach(() => __awaiter(void 0, void 0, void 0, function* () {
51
+ authDisabledBefore = config_1.config.authDisabled;
52
+ publicUrlBefore = config_1.config.publicUrl;
53
+ config_1.config.authDisabled = true;
54
+ calls = [];
55
+ sinon_1.default.stub(InternalHttpClient_1.InternalHttpClient, 'get').callsFake(((url) => __awaiter(void 0, void 0, void 0, function* () {
56
+ calls.push(url);
57
+ if (url.endsWith('/dashboard/api/ping'))
58
+ return { pong: true };
59
+ return { result: { rows: [] } };
60
+ })));
61
+ sinon_1.default
62
+ .stub(gridStore, 'getAllDevices')
63
+ .resolves([{ udid: 'phone-1', host: 'http://127.0.0.1:4723', platform: 'android' }]);
64
+ }));
65
+ afterEach(() => {
66
+ config_1.config.authDisabled = authDisabledBefore;
67
+ config_1.config.publicUrl = publicUrlBefore;
68
+ sinon_1.default.restore();
69
+ });
70
+ // This server's own address, as ServerManager passes it from Appium's CLI
71
+ // arguments. InternalHttpClient is stubbed, so nothing is sent anywhere.
72
+ function app() {
73
+ const a = (0, express_1.default)();
74
+ a.use('/xenon', (0, index_1.createRouter)(Object.assign(Object.assign({}, IPluginArgs_1.DefaultPluginArgs), { bindHostOrIp: '127.0.0.1' }), {
75
+ address: '0.0.0.0',
76
+ port: 4999,
77
+ }));
78
+ return a;
79
+ }
80
+ // The old check was a layer ahead of the login, first on the router, so a
81
+ // public request was enough: GET /xenon/api/health with a forged Host made
82
+ // Xenon ping that host. Now a public request makes no outbound call at all.
83
+ it('asks nothing of anyone for a public request, whatever Host it names', () => __awaiter(void 0, void 0, void 0, function* () {
84
+ yield (0, loopbackRequest_1.default)(app()).get('/xenon/api/health').set('Host', EVIL);
85
+ (0, chai_1.expect)(calls).to.deep.equal([]);
86
+ }));
87
+ // Lets the middleware's ping (started by a request, never awaited by one)
88
+ // finish.
89
+ const settle = () => new Promise((resolve) => setImmediate(resolve));
90
+ // The middleware ServerManager installs, with the device list, on a router
91
+ // of their own: the process-wide /xenon router may already answer /device
92
+ // from a layer another spec put ahead of it.
93
+ describe('the device list', () => {
94
+ function gridApp(ownServer) {
95
+ const r = express_1.default.Router();
96
+ r.use((req, _res, next) => {
97
+ req.auth = {
98
+ kind: 'user-session',
99
+ userId: 'u1',
100
+ role: 'SUPER_ADMIN',
101
+ scopes: 'admin',
102
+ };
103
+ next();
104
+ });
105
+ r.use((0, dashboardPluginLink_1.dashboardPluginMiddlewareFor)(ownServer));
106
+ grid_1.default.register(r, Object.assign({}, IPluginArgs_1.DefaultPluginArgs));
107
+ const a = (0, express_1.default)();
108
+ a.use('/xenon/api', r);
109
+ return a;
110
+ }
111
+ /** A first request starts the ping; the second sees what it found. */
112
+ function devices(a) {
113
+ return __awaiter(this, void 0, void 0, function* () {
114
+ yield (0, loopbackRequest_1.default)(a).get('/xenon/api/device').set('Host', EVIL);
115
+ yield settle();
116
+ return (0, loopbackRequest_1.default)(a).get('/xenon/api/device').set('Host', EVIL);
117
+ });
118
+ }
119
+ it("fetches the plugin's sessions from this server, never the request's host", () => __awaiter(void 0, void 0, void 0, function* () {
120
+ yield devices(gridApp({ address: '0.0.0.0', port: 4999 }));
121
+ (0, chai_1.expect)(calls.length, 'the ping and the sessions fetch ran').to.be.greaterThan(1);
122
+ (0, chai_1.expect)(calls[0]).to.equal('http://127.0.0.1:4999/dashboard/api/ping');
123
+ for (const url of calls.slice(1)) {
124
+ (0, chai_1.expect)(url).to.match(/^http:\/\/127\.0\.0\.1:4999\/dashboard\/api\/sessions\?/);
125
+ }
126
+ }));
127
+ it('links a device to /dashboard under XENON_PUBLIC_URL, or on the server it is open on', () => __awaiter(void 0, void 0, void 0, function* () {
128
+ config_1.config.publicUrl = 'https://xenon.example.com';
129
+ const withPublic = yield devices(gridApp({ port: 4999 }));
130
+ const phone = withPublic.body.find((d) => d.udid === 'phone-1');
131
+ (0, chai_1.expect)(phone.dashboard_link).to.match(/^https:\/\/xenon\.example\.com\/dashboard\?device_udid=phone-1&/);
132
+ config_1.config.publicUrl = undefined;
133
+ const without = yield devices(gridApp({ port: 4999 }));
134
+ (0, chai_1.expect)(without.body.find((d) => d.udid === 'phone-1').dashboard_link).to.match(/^\/dashboard\?device_udid=phone-1&/);
135
+ }));
136
+ // Appium serves HTTPS with --ssl-certificate-path and --ssl-key-path. A
137
+ // plain-HTTP ping got ECONNRESET, retried three times (about 4 s) while
138
+ // every signed-in request waited on it, every 30 s once the back-off
139
+ // capped, and the plugin's link was lost.
140
+ it('pings over HTTPS when Appium serves it, once, with a short time limit', () => __awaiter(void 0, void 0, void 0, function* () {
141
+ yield devices(gridApp({ address: '0.0.0.0', port: 4999, tls: true }));
142
+ const ping = InternalHttpClient_1.InternalHttpClient.get.firstCall;
143
+ (0, chai_1.expect)(ping.args[0]).to.equal('https://127.0.0.1:4999/dashboard/api/ping');
144
+ (0, chai_1.expect)(ping.args[1]).to.include({ retry: false, timeout: dashboardPluginLink_1.DASHBOARD_PING_TIMEOUT_MS });
145
+ (0, chai_1.expect)(dashboardPluginLink_1.DASHBOARD_PING_TIMEOUT_MS).to.be.at.most(2000);
146
+ }));
147
+ });
148
+ describe('dashboardPluginMiddleware', () => {
149
+ const run = (mw_1, ...args_1) => __awaiter(void 0, [mw_1, ...args_1], void 0, function* (mw, host = EVIL) {
150
+ const req = { headers: { host }, get: () => host, protocol: 'http' };
151
+ yield new Promise((resolve) => mw(req, {}, resolve));
152
+ return req;
153
+ });
154
+ it('reaches the plugin on the server it is given, whatever the request', () => __awaiter(void 0, void 0, void 0, function* () {
155
+ const got = [];
156
+ const mw = (0, dashboardPluginLink_1.dashboardPluginMiddleware)({
157
+ ownServerOrigin: 'http://127.0.0.1:4723',
158
+ get: (url) => __awaiter(void 0, void 0, void 0, function* () {
159
+ got.push(url);
160
+ return { pong: true };
161
+ }),
162
+ publicBase: () => null,
163
+ });
164
+ yield run(mw);
165
+ yield settle();
166
+ const req = yield run(mw);
167
+ (0, chai_1.expect)(got).to.deep.equal(['http://127.0.0.1:4723/dashboard/api/ping']);
168
+ (0, chai_1.expect)(req['dashboard-plugin-url']).to.equal('http://127.0.0.1:4723/dashboard');
169
+ (0, chai_1.expect)(req['dashboard-plugin-link']).to.equal('/dashboard');
170
+ yield run(mw, 'other.example');
171
+ (0, chai_1.expect)(got, 'pinged once, while it answers').to.have.length(1);
172
+ }));
173
+ // A request never waits for the ping: one the plugin doesn't answer
174
+ // (a hung server, the wrong scheme) would hold every signed-in request.
175
+ it('never holds a request while the ping is out', () => __awaiter(void 0, void 0, void 0, function* () {
176
+ const mw = (0, dashboardPluginLink_1.dashboardPluginMiddleware)({
177
+ ownServerOrigin: 'http://127.0.0.1:4723',
178
+ get: () => new Promise(() => undefined),
179
+ });
180
+ const outcome = yield Promise.race([
181
+ run(mw).then(() => 'answered'),
182
+ new Promise((resolve) => setTimeout(() => resolve('held'), 200)),
183
+ ]);
184
+ (0, chai_1.expect)(outcome).to.equal('answered');
185
+ }));
186
+ it('backs off after a failed ping, and links nothing meanwhile', () => __awaiter(void 0, void 0, void 0, function* () {
187
+ let t = 0;
188
+ const get = sinon_1.default.stub().rejects(new Error('ECONNREFUSED'));
189
+ const mw = (0, dashboardPluginLink_1.dashboardPluginMiddleware)({
190
+ ownServerOrigin: 'http://127.0.0.1:4723',
191
+ get,
192
+ now: () => t,
193
+ });
194
+ yield run(mw);
195
+ yield settle();
196
+ const req = yield run(mw);
197
+ (0, chai_1.expect)(req['dashboard-plugin-url']).to.equal('');
198
+ (0, chai_1.expect)(req['dashboard-plugin-link']).to.equal('');
199
+ (0, chai_1.expect)(get.callCount, 'no second ping inside the back-off').to.equal(1);
200
+ t = 1000;
201
+ yield run(mw);
202
+ (0, chai_1.expect)(get.callCount).to.equal(2);
203
+ }));
204
+ it('finds the server on 127.0.0.1 for a wildcard bind address, over HTTPS when asked', () => {
205
+ (0, chai_1.expect)((0, dashboardPluginLink_1.ownServerOrigin)('0.0.0.0', 4723)).to.equal('http://127.0.0.1:4723');
206
+ (0, chai_1.expect)((0, dashboardPluginLink_1.ownServerOrigin)(undefined, 4723)).to.equal('http://127.0.0.1:4723');
207
+ (0, chai_1.expect)((0, dashboardPluginLink_1.ownServerOrigin)('::', 4723)).to.equal('http://127.0.0.1:4723');
208
+ (0, chai_1.expect)((0, dashboardPluginLink_1.ownServerOrigin)('10.0.0.5', 4723)).to.equal('http://10.0.0.5:4723');
209
+ (0, chai_1.expect)((0, dashboardPluginLink_1.ownServerOrigin)('fe80::1', 4723)).to.equal('http://[fe80::1]:4723');
210
+ (0, chai_1.expect)((0, dashboardPluginLink_1.ownServerOrigin)('0.0.0.0', 4723, true)).to.equal('https://127.0.0.1:4723');
211
+ });
212
+ });
213
+ });
@@ -0,0 +1,180 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const chai_1 = require("chai");
4
+ const deviceLogBook_1 = require("../../src/services/logcat/deviceLogBook");
5
+ const logcatParse_1 = require("../../src/services/logcat/logcatParse");
6
+ const pad = (n, w = 2) => String(n).padStart(w, '0');
7
+ /** A threadtime line logged at `at` by this machine's local clock. */
8
+ function lineAt(at, level = 'I', msg = 'hello', tag = 'Tag', pid = 4127) {
9
+ const t = new Date(at);
10
+ return (`${pad(t.getMonth() + 1)}-${pad(t.getDate())} ${pad(t.getHours())}:${pad(t.getMinutes())}:` +
11
+ `${pad(t.getSeconds())}.${pad(t.getMilliseconds(), 3)} ${pid} ${pid} ${level} ${tag}: ${msg}`);
12
+ }
13
+ function recAt(at, level = 'I', msg = 'hello', tag = 'Tag') {
14
+ const rec = (0, logcatParse_1.parseThreadtimeLine)(lineAt(at, level, msg, tag), new Date(at));
15
+ if (!rec)
16
+ throw new Error('not a line');
17
+ return rec;
18
+ }
19
+ // The dashboard's own reading of a device log line (web log-derive.ts).
20
+ const DASHBOARD_THREADTIME = /^\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}\.\d+\s+\d+\s+\d+\s+([VDIWEF])\s/;
21
+ // Mid-January, midday: no daylight-saving change within hours of it.
22
+ const T0 = new Date(2026, 0, 15, 12, 0, 0, 0).getTime();
23
+ describe('DeviceLogBook', () => {
24
+ describe('formatThreadtime', () => {
25
+ it('prints a record as logcat printed it, so it parses back the same', () => {
26
+ const line = '01-15 12:00:00.120 4127 4130 E AndroidRuntime: FATAL EXCEPTION: main';
27
+ const rec = (0, logcatParse_1.parseThreadtimeLine)(line, new Date(T0));
28
+ const printed = (0, deviceLogBook_1.formatThreadtime)(rec);
29
+ (0, chai_1.expect)(printed).to.equal(line);
30
+ (0, chai_1.expect)((0, logcatParse_1.parseThreadtimeLine)(printed, new Date(T0))).to.deep.equal(rec);
31
+ });
32
+ it("pads a short tag to logcat's eight columns, and the dashboard reads its level", () => {
33
+ var _a;
34
+ const printed = (0, deviceLogBook_1.formatThreadtime)(recAt(T0, 'F', 'boom', 'DEBUG'));
35
+ (0, chai_1.expect)(printed).to.match(/ F DEBUG {3}: boom$/);
36
+ (0, chai_1.expect)((_a = DASHBOARD_THREADTIME.exec(printed)) === null || _a === void 0 ? void 0 : _a[1]).to.equal('F');
37
+ });
38
+ it('keeps a wrapped message whole', () => {
39
+ const rec = Object.assign(Object.assign({}, recAt(T0)), { message: 'first\nwrapped remainder' });
40
+ (0, chai_1.expect)((0, deviceLogBook_1.formatThreadtime)(rec)).to.match(/Tag {5}: first\nwrapped remainder$/);
41
+ });
42
+ });
43
+ describe('the session window', () => {
44
+ it('keeps lines from shortly before the phone was given to the session, and not before', () => {
45
+ const book = new deviceLogBook_1.DeviceLogBook({ since: T0, clock: deviceLogBook_1.UNKNOWN_CLOCK });
46
+ (0, chai_1.expect)(book.add(recAt(T0 - deviceLogBook_1.WINDOW_SLACK_MS - 1))).to.deep.equal([]);
47
+ (0, chai_1.expect)(book.add(recAt(T0 - deviceLogBook_1.WINDOW_SLACK_MS))).to.have.length(1);
48
+ (0, chai_1.expect)(book.add(recAt(T0 + 60000))).to.have.length(1);
49
+ });
50
+ it('stamps each line with its own time, and keeps the text the phone printed', () => {
51
+ const book = new deviceLogBook_1.DeviceLogBook({ since: T0, clock: deviceLogBook_1.UNKNOWN_CLOCK });
52
+ const [row] = book.add(recAt(T0 + 1234, 'W', 'low memory'));
53
+ (0, chai_1.expect)(row.timestamp.getTime()).to.equal(T0 + 1234);
54
+ (0, chai_1.expect)(row.message).to.equal(lineAt(T0 + 1234, 'W', 'low memory').replace('Tag:', 'Tag :'));
55
+ });
56
+ it('reads a phone in another time zone at the moment it logged, not hours off', () => {
57
+ // The phone is 5 h 30 min ahead: its 17:30 is this server's 12:00.
58
+ const ahead = 5.5 * 3600000;
59
+ const book = new deviceLogBook_1.DeviceLogBook({ since: T0, clock: { zoneShiftMs: -ahead, skewMs: 0 } });
60
+ // Printed an hour before the session by the phone's clock: left out.
61
+ (0, chai_1.expect)(book.add(recAt(T0 + ahead - 3600000))).to.deep.equal([]);
62
+ const [row] = book.add(recAt(T0 + ahead + 5000));
63
+ (0, chai_1.expect)(row.timestamp.getTime()).to.equal(T0 + 5000);
64
+ // The text is the phone's own, as it printed it.
65
+ (0, chai_1.expect)(row.message).to.match(/^01-15 17:30:05\.000 /);
66
+ });
67
+ it("cuts and stamps by this server's clock when the phone's runs behind", () => {
68
+ // The phone's clock is 40 s slow: a line it stamps 30 s before the
69
+ // session came 10 s after the session began, and one it stamps 43 s
70
+ // before came 3 s before.
71
+ const book = new deviceLogBook_1.DeviceLogBook({ since: T0, clock: { zoneShiftMs: 0, skewMs: 40000 } });
72
+ (0, chai_1.expect)(book.add(recAt(T0 - 43000))).to.deep.equal([]);
73
+ const [row] = book.add(recAt(T0 - 30000));
74
+ (0, chai_1.expect)(row.timestamp.getTime()).to.equal(T0 + 10000);
75
+ // The text keeps the time the phone printed.
76
+ (0, chai_1.expect)(row.message).to.match(/^01-15 11:59:30\.000 /);
77
+ });
78
+ it("ignores the stream's own notes", () => {
79
+ const book = new deviceLogBook_1.DeviceLogBook({ since: T0, clock: deviceLogBook_1.UNKNOWN_CLOCK });
80
+ (0, chai_1.expect)(book.add(Object.assign(Object.assign({}, recAt(T0)), { tag: 'xenon', synthetic: true }))).to.deep.equal([]);
81
+ });
82
+ });
83
+ describe('after an interruption', () => {
84
+ it('counts only lines newer than the last it saw, by content at that millisecond', () => {
85
+ const book = new deviceLogBook_1.DeviceLogBook({ since: T0, clock: deviceLogBook_1.UNKNOWN_CLOCK });
86
+ book.add(recAt(T0 + 1000, 'I', 'one'));
87
+ book.add(recAt(T0 + 2000, 'E', 'stack line 1'));
88
+ book.add(recAt(T0 + 2000, 'E', 'stack line 2'));
89
+ const [note] = book.interrupted(new Date(T0 + 3000));
90
+ (0, chai_1.expect)(note.message).to.equal("Xenon: The phone's log was interrupted. Lines may be missing from here.");
91
+ // The new stream's history repeats what was kept...
92
+ (0, chai_1.expect)(book.add(recAt(T0 + 1000, 'I', 'one'))).to.deep.equal([]);
93
+ (0, chai_1.expect)(book.add(recAt(T0 + 2000, 'E', 'stack line 1'))).to.deep.equal([]);
94
+ (0, chai_1.expect)(book.add(recAt(T0 + 2000, 'E', 'stack line 2'))).to.deep.equal([]);
95
+ // ...and has a line of that same millisecond that hadn't arrived yet.
96
+ (0, chai_1.expect)(book.add(recAt(T0 + 2000, 'E', 'stack line 3'))).to.have.length(1);
97
+ (0, chai_1.expect)(book.add(recAt(T0 + 4000, 'I', 'after'))).to.have.length(1);
98
+ (0, chai_1.expect)(book.keptLines).to.equal(5);
99
+ });
100
+ it('keeps the session window when nothing had arrived yet', () => {
101
+ const book = new deviceLogBook_1.DeviceLogBook({ since: T0, clock: deviceLogBook_1.UNKNOWN_CLOCK });
102
+ book.interrupted(new Date(T0));
103
+ (0, chai_1.expect)(book.add(recAt(T0 - 60000))).to.deep.equal([]);
104
+ (0, chai_1.expect)(book.add(recAt(T0))).to.have.length(1);
105
+ });
106
+ });
107
+ describe('the limit', () => {
108
+ it('keeps every line up to the limit, then errors only, then nothing, and says so', () => {
109
+ const book = new deviceLogBook_1.DeviceLogBook({
110
+ since: T0,
111
+ clock: deviceLogBook_1.UNKNOWN_CLOCK,
112
+ lineLimit: 3,
113
+ errorLimit: 2,
114
+ });
115
+ const kept = [];
116
+ const add = (level, msg, at) => kept.push(...book.add(recAt(T0 + at, level, msg)).map((r) => r.message));
117
+ add('D', 'a', 1);
118
+ add('I', 'b', 2);
119
+ add('W', 'c', 3);
120
+ add('I', 'chatter', 4); // past the limit: left out, after the note
121
+ add('E', 'FATAL EXCEPTION: main', 5);
122
+ add('D', 'more chatter', 6);
123
+ add('F', 'abort', 7);
124
+ add('E', 'one error too many', 8);
125
+ (0, chai_1.expect)(kept.map((m) => m.replace(/^.*Tag {5}: /, ''))).to.deep.equal([
126
+ 'a',
127
+ 'b',
128
+ 'c',
129
+ 'Xenon: This session reached 3 device log lines. From here on, only errors are kept.',
130
+ 'FATAL EXCEPTION: main',
131
+ 'abort',
132
+ ]);
133
+ (0, chai_1.expect)(book.finish(new Date(T0 + 9)).map((r) => r.message)).to.deep.equal([
134
+ "Xenon: 3 device log lines were left out to keep this session's log within its limit.",
135
+ ]);
136
+ });
137
+ it('says nothing at the end when nothing was left out', () => {
138
+ const book = new deviceLogBook_1.DeviceLogBook({ since: T0, clock: deviceLogBook_1.UNKNOWN_CLOCK });
139
+ book.add(recAt(T0));
140
+ (0, chai_1.expect)(book.finish(new Date(T0))).to.deep.equal([]);
141
+ });
142
+ it('is 10,000 lines, then 2,000 errors', () => {
143
+ (0, chai_1.expect)(deviceLogBook_1.DEVICE_LOG_LINE_LIMIT).to.equal(10000);
144
+ (0, chai_1.expect)(deviceLogBook_1.DEVICE_LOG_ERROR_LIMIT).to.equal(2000);
145
+ });
146
+ });
147
+ describe('parseDeviceClock', () => {
148
+ const fields = (at) => {
149
+ const t = new Date(at);
150
+ return `${pad(t.getMonth() + 1)}-${pad(t.getDate())}.${pad(t.getHours())}:${pad(t.getMinutes())}:${pad(t.getSeconds())}`;
151
+ };
152
+ it("finds a phone's time zone against this server's, to the quarter hour", () => {
153
+ const secs = Math.floor(T0 / 1000);
154
+ const ahead = 5.5 * 3600000;
155
+ // The phone prints its local time, 5 h 30 min ahead of this server's.
156
+ const out = `${secs}.348157848.${fields(secs * 1000 + ahead)}\n`;
157
+ const clock = (0, deviceLogBook_1.parseDeviceClock)(out, T0 + 300);
158
+ (0, chai_1.expect)(clock === null || clock === void 0 ? void 0 : clock.zoneShiftMs).to.equal(-ahead);
159
+ (0, chai_1.expect)(clock === null || clock === void 0 ? void 0 : clock.skewMs).to.equal(300 - 348);
160
+ });
161
+ it('finds a phone in the same zone, its clock 2.8 s slow, as the lab S9+ was', () => {
162
+ const secs = Math.floor(T0 / 1000) - 3;
163
+ const clock = (0, deviceLogBook_1.parseDeviceClock)(`${secs}.200000000.${fields(secs * 1000)}`, T0);
164
+ (0, chai_1.expect)(clock === null || clock === void 0 ? void 0 : clock.zoneShiftMs).to.equal(0);
165
+ (0, chai_1.expect)(clock === null || clock === void 0 ? void 0 : clock.skewMs).to.equal(2800);
166
+ });
167
+ it("takes the middle of the second when the phone's date prints no nanoseconds", () => {
168
+ const secs = Math.floor(T0 / 1000) + 7;
169
+ for (const nanos of ['%N', 'N', '']) {
170
+ const clock = (0, deviceLogBook_1.parseDeviceClock)(`${secs}.${nanos}.${fields(secs * 1000)}`, T0);
171
+ (0, chai_1.expect)(clock === null || clock === void 0 ? void 0 : clock.skewMs, nanos).to.equal(-7500);
172
+ }
173
+ });
174
+ it("is null for anything that isn't the date it asked for", () => {
175
+ (0, chai_1.expect)((0, deviceLogBook_1.parseDeviceClock)('', T0)).to.equal(null);
176
+ (0, chai_1.expect)((0, deviceLogBook_1.parseDeviceClock)('date: bad format', T0)).to.equal(null);
177
+ (0, chai_1.expect)((0, deviceLogBook_1.parseDeviceClock)('%s.%N.%m-%d.%H:%M:%S', T0)).to.equal(null);
178
+ });
179
+ });
180
+ });